From 09af4d1a68fb98d8b288bba5ec5d566abe7c5e8b Mon Sep 17 00:00:00 2001 From: Nash Aschenbath Date: Fri, 13 Nov 2026 23:44:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=87=AA=E5=AE=9A=E4=B9=89=E8=B5=84?= =?UTF-8?q?=E6=96=99=E7=9B=AE=E5=BD=95=E5=90=8C=E6=97=B6=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 16 +++++++- lib/pages/startup_page.dart | 18 ++++----- lib/services/app_log.dart | 14 ++++--- lib/services/startup_directory.dart | 16 ++++++++ test/app_log_test.dart | 57 +++++++++++++++++++++++++++++ test/startup_directory_test.dart | 55 ++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 lib/services/startup_directory.dart create mode 100644 test/startup_directory_test.dart diff --git a/lib/main.dart b/lib/main.dart index 8fbc0e5..0bd2add 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main(List 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', diff --git a/lib/pages/startup_page.dart b/lib/pages/startup_page.dart index 0d8c43c..4aacf75 100644 --- a/lib/pages/startup_page.dart +++ b/lib/pages/startup_page.dart @@ -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'; @@ -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}); @@ -73,17 +73,13 @@ class _StartupPageState extends State { 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); diff --git a/lib/services/app_log.dart b/lib/services/app_log.dart index 26161c1..e7e99fe 100644 --- a/lib/services/app_log.dart +++ b/lib/services/app_log.dart @@ -160,13 +160,17 @@ class AppLog { } } - Future initialize() => _initializing ??= _initialize(); + Future initialize({Directory? directory}) => + _initializing ??= _initialize(directory); - Future _initialize() async { + Future _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); diff --git a/lib/services/startup_directory.dart b/lib/services/startup_directory.dart new file mode 100644 index 0000000..9ab4ced --- /dev/null +++ b/lib/services/startup_directory.dart @@ -0,0 +1,16 @@ +import 'package:path/path.dart' as p; + +import 'app_log.dart'; + +String? startupDataDirectory(List 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]; +} diff --git a/test/app_log_test.dart b/test/app_log_test.dart index dd35436..1c3f3eb 100644 --- a/test/app_log_test.dart +++ b/test/app_log_test.dart @@ -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, diff --git a/test/startup_directory_test.dart b/test/startup_directory_test.dart new file mode 100644 index 0000000..ff84fed --- /dev/null +++ b/test/startup_directory_test.dart @@ -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().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); + }); +}