mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
fix(windows): tolerate unavailable log files at startup
This commit is contained in:
parent
69a310c78f
commit
04ca3ce53f
29
lib/services/logger/file_log_writer.dart
Normal file
29
lib/services/logger/file_log_writer.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// Best-effort file output for errors that have already been logged to console.
|
||||
///
|
||||
/// File errors must not escape into the same zone that reports app errors.
|
||||
class FileLogWriter {
|
||||
final Future<File?> _file;
|
||||
|
||||
FileLogWriter(Future<File> Function() getFile) : _file = _resolve(getFile);
|
||||
|
||||
static Future<File?> _resolve(Future<File> Function() getFile) async {
|
||||
try {
|
||||
return await getFile();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> write(String message) async {
|
||||
final file = await _file;
|
||||
if (file == null) return;
|
||||
|
||||
try {
|
||||
await file.writeAsString(message, mode: FileMode.writeOnlyAppend);
|
||||
} catch (_) {
|
||||
// The console remains available if permissions change after startup.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:logging/logging.dart' as logging;
|
||||
|
||||
import 'file_log_writer.dart';
|
||||
|
||||
final _loggingToLoggerLevel = {
|
||||
logging.Level.ALL: Level.all,
|
||||
logging.Level.FINEST: Level.trace,
|
||||
@ -26,7 +28,7 @@ final _loggingToLoggerLevel = {
|
||||
|
||||
class AppLogger {
|
||||
static late final Logger log;
|
||||
static late final File logFile;
|
||||
static FileLogWriter? _fileLogWriter;
|
||||
|
||||
static initialize(bool verbose) {
|
||||
log = Logger(
|
||||
@ -39,17 +41,15 @@ class AppLogger {
|
||||
logging.hierarchicalLoggingEnabled = true;
|
||||
logging.Logger('YoutubeExplode.StreamsClient')
|
||||
..level = logging.Level.SEVERE
|
||||
..onRecord.listen(
|
||||
(record) {
|
||||
log.log(
|
||||
_loggingToLoggerLevel[record.level] ?? Level.info,
|
||||
record.message,
|
||||
error: record.error,
|
||||
stackTrace: record.stackTrace,
|
||||
time: record.time,
|
||||
);
|
||||
},
|
||||
);
|
||||
..onRecord.listen((record) {
|
||||
log.log(
|
||||
_loggingToLoggerLevel[record.level] ?? Level.info,
|
||||
record.message,
|
||||
error: record.error,
|
||||
stackTrace: record.stackTrace,
|
||||
time: record.time,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static R? runZoned<R>(R Function() body) {
|
||||
@ -70,17 +70,14 @@ class AppLogger {
|
||||
Isolate.current.addErrorListener(
|
||||
RawReceivePort((pair) async {
|
||||
final isolateError = pair as List<dynamic>;
|
||||
reportError(
|
||||
isolateError.first.toString(),
|
||||
isolateError.last,
|
||||
);
|
||||
reportError(isolateError.first.toString(), isolateError.last);
|
||||
}).sendPort,
|
||||
);
|
||||
}
|
||||
|
||||
_initInternalPackageLoggers();
|
||||
|
||||
getLogsPath().then((value) => logFile = value);
|
||||
_fileLogWriter = FileLogWriter(getLogsPath);
|
||||
|
||||
return body();
|
||||
},
|
||||
@ -91,7 +88,11 @@ class AppLogger {
|
||||
}
|
||||
|
||||
static Future<File> getLogsPath() async {
|
||||
String dir = (await getApplicationDocumentsDirectory()).path;
|
||||
// Documents can be protected by Windows Controlled Folder Access.
|
||||
String dir = (kIsWindows
|
||||
? await getApplicationSupportDirectory()
|
||||
: await getApplicationDocumentsDirectory())
|
||||
.path;
|
||||
if (kIsAndroid) {
|
||||
dir = (await getExternalStorageDirectory())?.path ?? "";
|
||||
}
|
||||
@ -119,11 +120,10 @@ class AppLogger {
|
||||
log.e(message, error: error, stackTrace: stackTrace);
|
||||
|
||||
if (kReleaseMode) {
|
||||
await logFile.writeAsString(
|
||||
await _fileLogWriter?.write(
|
||||
"[${DateTime.now()}]---------------------\n"
|
||||
"$error\n$stackTrace\n"
|
||||
"----------------------------------------\n",
|
||||
mode: FileMode.writeOnlyAppend,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1767,7 +1767,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
|
||||
@ -164,6 +164,7 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
path_provider_platform_interface: ^2.1.2
|
||||
json_serializable: ^6.6.2
|
||||
freezed: ^2.5.2
|
||||
process_run: ^0.14.2
|
||||
|
||||
87
test/services/logger/file_log_writer_test.dart
Normal file
87
test/services/logger/file_log_writer_test.dart
Normal file
@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotube/services/logger/file_log_writer.dart';
|
||||
|
||||
void main() {
|
||||
late Directory directory;
|
||||
|
||||
setUp(() async {
|
||||
directory = await Directory.systemTemp.createTemp('spotube-log-test-');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await directory.delete(recursive: true);
|
||||
});
|
||||
|
||||
test('appends errors without replacing earlier entries', () async {
|
||||
final file = File('${directory.path}/errors.log');
|
||||
final writer = FileLogWriter(() async => file);
|
||||
|
||||
await writer.write('first\n');
|
||||
await writer.write('second\n');
|
||||
|
||||
expect(await file.readAsString(), 'first\nsecond\n');
|
||||
});
|
||||
|
||||
test('waits for file initialization before writing', () async {
|
||||
final file = File('${directory.path}/errors.log');
|
||||
final ready = Completer<File>();
|
||||
final writer = FileLogWriter(() => ready.future);
|
||||
final write = writer.write('startup error\n');
|
||||
|
||||
expect(await file.exists(), isFalse);
|
||||
ready.complete(file);
|
||||
await write;
|
||||
|
||||
expect(await file.readAsString(), 'startup error\n');
|
||||
});
|
||||
|
||||
test('contains synchronous initialization errors', () async {
|
||||
final writer = FileLogWriter(() {
|
||||
throw const FileSystemException('Permission denied');
|
||||
});
|
||||
|
||||
await expectLater(writer.write('error\n'), completes);
|
||||
});
|
||||
|
||||
test(
|
||||
'contains asynchronous initialization errors before any report',
|
||||
() async {
|
||||
final writer = FileLogWriter(() async {
|
||||
throw const FileSystemException('Permission denied');
|
||||
});
|
||||
|
||||
// Initialization must be safe even when no error report is being written.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
await expectLater(writer.write('error\n'), completes);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'contains initialization failures with a pending error report',
|
||||
() async {
|
||||
final ready = Completer<File>();
|
||||
final writer = FileLogWriter(() => ready.future);
|
||||
final write = writer.write('startup error\n');
|
||||
|
||||
ready.completeError(const FileSystemException('Permission denied'));
|
||||
|
||||
await expectLater(write, completes);
|
||||
},
|
||||
);
|
||||
|
||||
test('contains write errors after successful initialization', () async {
|
||||
final file = File('${directory.path}/errors.log');
|
||||
final writer = FileLogWriter(() async => file);
|
||||
await writer.write('first\n');
|
||||
|
||||
// A directory at the log path fails writes on every supported desktop OS.
|
||||
await file.delete();
|
||||
await Directory(file.path).create();
|
||||
|
||||
await expectLater(writer.write('second\n'), completes);
|
||||
await expectLater(writer.write('third\n'), completes);
|
||||
});
|
||||
}
|
||||
49
test/services/logger/logger_test.dart
Normal file
49
test/services/logger/logger_test.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class _ProtectedDocumentsPathProvider extends PathProviderPlatform {
|
||||
final String supportPath;
|
||||
int documentsRequests = 0;
|
||||
|
||||
_ProtectedDocumentsPathProvider(this.supportPath);
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async => supportPath;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationDocumentsPath() async {
|
||||
documentsRequests++;
|
||||
throw const FileSystemException('Documents access denied');
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test(
|
||||
'Windows logging does not require access to Documents',
|
||||
() async {
|
||||
final directory = await Directory.systemTemp.createTemp(
|
||||
'spotube-path-test-',
|
||||
);
|
||||
final original = PathProviderPlatform.instance;
|
||||
final provider = _ProtectedDocumentsPathProvider(directory.path);
|
||||
PathProviderPlatform.instance = provider;
|
||||
addTearDown(() async {
|
||||
PathProviderPlatform.instance = original;
|
||||
await directory.delete(recursive: true);
|
||||
});
|
||||
|
||||
final file = await AppLogger.getLogsPath();
|
||||
|
||||
expect(file.path, join(directory.path, '.spotube_logs'));
|
||||
expect(await file.exists(), isTrue);
|
||||
expect(provider.documentsRequests, 0);
|
||||
},
|
||||
skip: !Platform.isWindows,
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user