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:spotube/utils/platform.dart';
|
||||||
import 'package:logging/logging.dart' as logging;
|
import 'package:logging/logging.dart' as logging;
|
||||||
|
|
||||||
|
import 'file_log_writer.dart';
|
||||||
|
|
||||||
final _loggingToLoggerLevel = {
|
final _loggingToLoggerLevel = {
|
||||||
logging.Level.ALL: Level.all,
|
logging.Level.ALL: Level.all,
|
||||||
logging.Level.FINEST: Level.trace,
|
logging.Level.FINEST: Level.trace,
|
||||||
@ -26,7 +28,7 @@ final _loggingToLoggerLevel = {
|
|||||||
|
|
||||||
class AppLogger {
|
class AppLogger {
|
||||||
static late final Logger log;
|
static late final Logger log;
|
||||||
static late final File logFile;
|
static FileLogWriter? _fileLogWriter;
|
||||||
|
|
||||||
static initialize(bool verbose) {
|
static initialize(bool verbose) {
|
||||||
log = Logger(
|
log = Logger(
|
||||||
@ -39,17 +41,15 @@ class AppLogger {
|
|||||||
logging.hierarchicalLoggingEnabled = true;
|
logging.hierarchicalLoggingEnabled = true;
|
||||||
logging.Logger('YoutubeExplode.StreamsClient')
|
logging.Logger('YoutubeExplode.StreamsClient')
|
||||||
..level = logging.Level.SEVERE
|
..level = logging.Level.SEVERE
|
||||||
..onRecord.listen(
|
..onRecord.listen((record) {
|
||||||
(record) {
|
log.log(
|
||||||
log.log(
|
_loggingToLoggerLevel[record.level] ?? Level.info,
|
||||||
_loggingToLoggerLevel[record.level] ?? Level.info,
|
record.message,
|
||||||
record.message,
|
error: record.error,
|
||||||
error: record.error,
|
stackTrace: record.stackTrace,
|
||||||
stackTrace: record.stackTrace,
|
time: record.time,
|
||||||
time: record.time,
|
);
|
||||||
);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static R? runZoned<R>(R Function() body) {
|
static R? runZoned<R>(R Function() body) {
|
||||||
@ -70,17 +70,14 @@ class AppLogger {
|
|||||||
Isolate.current.addErrorListener(
|
Isolate.current.addErrorListener(
|
||||||
RawReceivePort((pair) async {
|
RawReceivePort((pair) async {
|
||||||
final isolateError = pair as List<dynamic>;
|
final isolateError = pair as List<dynamic>;
|
||||||
reportError(
|
reportError(isolateError.first.toString(), isolateError.last);
|
||||||
isolateError.first.toString(),
|
|
||||||
isolateError.last,
|
|
||||||
);
|
|
||||||
}).sendPort,
|
}).sendPort,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initInternalPackageLoggers();
|
_initInternalPackageLoggers();
|
||||||
|
|
||||||
getLogsPath().then((value) => logFile = value);
|
_fileLogWriter = FileLogWriter(getLogsPath);
|
||||||
|
|
||||||
return body();
|
return body();
|
||||||
},
|
},
|
||||||
@ -91,7 +88,11 @@ class AppLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<File> getLogsPath() async {
|
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) {
|
if (kIsAndroid) {
|
||||||
dir = (await getExternalStorageDirectory())?.path ?? "";
|
dir = (await getExternalStorageDirectory())?.path ?? "";
|
||||||
}
|
}
|
||||||
@ -119,11 +120,10 @@ class AppLogger {
|
|||||||
log.e(message, error: error, stackTrace: stackTrace);
|
log.e(message, error: error, stackTrace: stackTrace);
|
||||||
|
|
||||||
if (kReleaseMode) {
|
if (kReleaseMode) {
|
||||||
await logFile.writeAsString(
|
await _fileLogWriter?.write(
|
||||||
"[${DateTime.now()}]---------------------\n"
|
"[${DateTime.now()}]---------------------\n"
|
||||||
"$error\n$stackTrace\n"
|
"$error\n$stackTrace\n"
|
||||||
"----------------------------------------\n",
|
"----------------------------------------\n",
|
||||||
mode: FileMode.writeOnlyAppend,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1767,7 +1767,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
|||||||
@ -164,6 +164,7 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
path_provider_platform_interface: ^2.1.2
|
||||||
json_serializable: ^6.6.2
|
json_serializable: ^6.6.2
|
||||||
freezed: ^2.5.2
|
freezed: ^2.5.2
|
||||||
process_run: ^0.14.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