Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@
// d9e9626f1a3f89cd9ca85e18f61aaba02634e12a (GNU GPL v3).
// Source: https://github.com/bggRGjQaUbCoE/PiliPlus
// Desktop setup only; audio and account services are not initialized here.
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart' as p;
import 'package:window_manager/window_manager.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

import 'pages/startup_page.dart';
import 'services/app_log.dart';
import 'services/error_reporting.dart';
import 'services/startup_directory.dart';

Future<void> main(List<String> arguments) async {
AppLog.instance.trace("lib/main.dart::main");

WidgetsFlutterBinding.ensureInitialized();
await AppLog.instance.initialize();
String? dataDirectory;
try {
dataDirectory = startupDataDirectory(arguments);
} on FormatException {
// StartupPage presents argument errors in the directory recovery UI.
}
await AppLog.instance.initialize(
directory: dataDirectory == null
? null
: Directory(p.join(dataDirectory, 'logs')),
);
AppLog.instance.write(
AppLogLevel.info,
'lifecycle',
Expand Down
18 changes: 7 additions & 11 deletions lib/pages/startup_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

Expand All @@ -13,6 +12,7 @@ import '../storage/flutter_credential_store.dart';
import '../services/app_runtime.dart';
import '../services/account_service.dart';
import '../services/app_log.dart';
import '../services/startup_directory.dart';

class StartupPage extends StatefulWidget {
const StartupPage({required this.arguments, super.key});
Expand Down Expand Up @@ -73,17 +73,13 @@ class _StartupPageState extends State<StartupPage> {
try {
if (selected != null) _directory = selected;
if (_directory == null) {
final args = widget.arguments;
final index = args.indexOf('--data-dir');
if (index >= 0) {
if (index + 1 >= args.length || !p.isAbsolute(args[index + 1])) {
invalidDirectoryArgument = true;
throw const FormatException('资料目录必须是绝对路径');
}
_directory = args[index + 1];
} else {
_directory = (await getApplicationSupportDirectory()).path;
try {
_directory = startupDataDirectory(widget.arguments);
} on FormatException {
invalidDirectoryArgument = true;
rethrow;
}
_directory ??= (await getApplicationSupportDirectory()).path;
}
candidate = await AppStorage.open(_directory!);
AppRuntime.applyLogSettings(candidate.state['settings'] as Map);
Expand Down
14 changes: 9 additions & 5 deletions lib/services/app_log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,17 @@ class AppLog {
}
}

Future<void> initialize() => _initializing ??= _initialize();
Future<void> initialize({Directory? directory}) =>
_initializing ??= _initialize(directory);

Future<void> _initialize() async {
Future<void> _initialize(Directory? directory) async {
try {
_root ??= Directory(
p.join((await getApplicationSupportDirectory()).path, 'logs'),
);
_root =
directory ??
_root ??
Directory(
p.join((await getApplicationSupportDirectory()).path, 'logs'),
);
await _readProcessSession();
} catch (error) {
_fail(error);
Expand Down
16 changes: 16 additions & 0 deletions lib/services/startup_directory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:path/path.dart' as p;

import 'app_log.dart';

String? startupDataDirectory(List<String> arguments) {
AppLog.instance.trace(
"lib/services/startup_directory.dart::startupDataDirectory",
);

final index = arguments.indexOf('--data-dir');
if (index < 0) return null;
if (index + 1 >= arguments.length || !p.isAbsolute(arguments[index + 1])) {
throw const FormatException('资料目录必须是绝对路径');
}
return arguments[index + 1];
}
57 changes: 57 additions & 0 deletions test/app_log_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,63 @@ void main() {
expect(signals, 1);
});

test('startup directory override keeps bootstrap logs out of the default profile', () async {
final defaultLogs = Directory(p.join(root.path, 'default', 'logs'));
final profileLogs = Directory(
p.join(root.path, '资料目录 with spaces', 'logs'),
);
final ignoredLogs = Directory(p.join(root.path, 'another-profile', 'logs'));
final log = AppLog(
directory: defaultLogs,
sessionId: 'profile-bootstrap',
startedAt: epoch,
processId: 123,
);
openLogs.add(log);
log.trace('main');
await log.initialize(directory: profileLogs);
log.write(AppLogLevel.info, 'startup', 'profile-started');
final originalPath = log.currentFilePath;
await log.initialize(directory: ignoredLogs);
await log.flush();

expect(log.directory, profileLogs.path);
expect(p.dirname(log.currentFilePath!), profileLogs.path);
expect(log.currentFilePath, originalPath);
expect(await defaultLogs.exists(), isFalse);
expect(await ignoredLogs.exists(), isFalse);
expect(
(await records(await log.snapshot(log.currentFileName!)))
.map((line) => line['message']),
['main', 'profile-started'],
);
});

test('unavailable startup log directory retains diagnostics without changing profiles', () async {
final defaultLogs = Directory(p.join(root.path, 'default', 'logs'));
final obstruction = await File(p.join(root.path, 'profile')).create();
final profileLogs = Directory(p.join(obstruction.path, 'logs'));
final log = AppLog(
directory: defaultLogs,
sessionId: 'profile-unavailable',
startedAt: epoch,
processId: 123,
);
openLogs.add(log);
await log.initialize(directory: profileLogs);
log.write(AppLogLevel.error, 'startup', 'profile-failed');
final snapshot = await log.snapshot(log.currentFileName!);

expect(log.directory, profileLogs.path);
expect(log.persistenceError, isNotNull);
expect(snapshot.sourcePath, isNull);
expect(await defaultLogs.exists(), isFalse);
expect(
(await records(snapshot)).map((line) => line['message']),
contains('profile-failed'),
);
});

test('bootstrap retains only the main entry; applying INFO stops traces immediately', () async {
final log = AppLog(
directory: root,
Expand Down
55 changes: 55 additions & 0 deletions test/startup_directory_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:xmly_flutter/pages/startup_page.dart';
import 'package:xmly_flutter/services/app_log.dart';
import 'package:xmly_flutter/services/startup_directory.dart';

void main() {
test('keeps the default profile when no data directory was requested', () {
expect(startupDataDirectory(const []), isNull);
expect(startupDataDirectory(const ['--other-option']), isNull);
});

test('uses the same absolute profile path for startup and logging', () {
final directory = p.absolute('资料目录 with spaces');
expect(
startupDataDirectory(['--other-option', '--data-dir', directory]),
directory,
);
});

test('rejects missing and relative data directories', () {
for (final arguments in const [
['--data-dir'],
['--data-dir', ''],
['--data-dir', 'relative-profile'],
['--data-dir', '--other-option'],
]) {
expect(
() => startupDataDirectory(arguments),
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
'资料目录必须是绝对路径',
),
),
);
}
});

testWidgets('invalid directory arguments retain the recovery screen', (
tester,
) async {
await tester.pumpWidget(
const StartupPage(arguments: ['--data-dir', 'relative-profile']),
);
await tester.runAsync(AppLog.instance.flush);
await tester.pumpAndSettle();

expect(find.textContaining('资料目录必须是绝对路径'), findsOneWidget);
expect(find.text('重试'), findsOneWidget);
expect(find.text('选择其他资料目录'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}