139 lines
4.2 KiB
Dart
139 lines
4.2 KiB
Dart
|
|
// Build a zip diagnostic pack the user can hand back for remote debugging.
|
||
|
|
|
||
|
|
import 'dart:convert';
|
||
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:archive/archive.dart';
|
||
|
|
import 'package:flutter/foundation.dart';
|
||
|
|
import 'package:path_provider/path_provider.dart';
|
||
|
|
|
||
|
|
import '../editor/canvas/input_diagnostics.dart';
|
||
|
|
import '../editor/input/pen_input_service.dart';
|
||
|
|
import 'badnote_log.dart';
|
||
|
|
import 'frame_sampler.dart';
|
||
|
|
import 'pen_event_ring.dart';
|
||
|
|
|
||
|
|
class DiagnosticExportResult {
|
||
|
|
DiagnosticExportResult({required this.zipPath, required this.bytes});
|
||
|
|
|
||
|
|
final String zipPath;
|
||
|
|
final int bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
class DiagnosticExport {
|
||
|
|
DiagnosticExport._();
|
||
|
|
static final DiagnosticExport instance = DiagnosticExport._();
|
||
|
|
|
||
|
|
/// Flush logs and write a zip under Documents/badnote_diagnostics/.
|
||
|
|
Future<DiagnosticExportResult> exportPack({
|
||
|
|
Duration penWindow = const Duration(minutes: 5),
|
||
|
|
}) async {
|
||
|
|
final log = BadNoteLog.instance;
|
||
|
|
await log.flush();
|
||
|
|
|
||
|
|
final meta = <String, Object?>{
|
||
|
|
'exportedAt': DateTime.now().toIso8601String(),
|
||
|
|
'sessionId': log.sessionId,
|
||
|
|
'platform': Platform.operatingSystem,
|
||
|
|
'osVersion': Platform.operatingSystemVersion,
|
||
|
|
'localHostname': Platform.localHostname,
|
||
|
|
'numberOfProcessors': Platform.numberOfProcessors,
|
||
|
|
'flutter': {
|
||
|
|
'foundationDebug': kDebugMode,
|
||
|
|
'foundationProfile': kProfileMode,
|
||
|
|
'foundationRelease': kReleaseMode,
|
||
|
|
},
|
||
|
|
'penNative': PenInputService.instance.debugSummary,
|
||
|
|
'penActive': PenInputService.instance.isActive,
|
||
|
|
'zoom': InputDiagnostics.instance.summary(),
|
||
|
|
'frames': FrameSampler.instance.summary(),
|
||
|
|
'instructions':
|
||
|
|
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip.',
|
||
|
|
};
|
||
|
|
|
||
|
|
final archive = Archive();
|
||
|
|
void addText(String name, String body) {
|
||
|
|
final bytes = utf8.encode(body);
|
||
|
|
archive.addFile(ArchiveFile(name, bytes.length, bytes));
|
||
|
|
}
|
||
|
|
|
||
|
|
addText('meta.json', const JsonEncoder.withIndent(' ').convert(meta));
|
||
|
|
addText(
|
||
|
|
'pen_events.json',
|
||
|
|
const JsonEncoder.withIndent(' ').convert(
|
||
|
|
PenEventRing.instance.toJsonList(window: penWindow),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
addText(
|
||
|
|
'frame_samples.json',
|
||
|
|
const JsonEncoder.withIndent(' ').convert(FrameSampler.instance.toJsonList()),
|
||
|
|
);
|
||
|
|
addText(
|
||
|
|
'log_ring.json',
|
||
|
|
const JsonEncoder.withIndent(' ').convert(log.snapshotRing()),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Include on-disk session NDJSON if present.
|
||
|
|
final sessionPath = log.path;
|
||
|
|
if (sessionPath != null) {
|
||
|
|
try {
|
||
|
|
final f = File(sessionPath);
|
||
|
|
if (await f.exists()) {
|
||
|
|
final bytes = await f.readAsBytes();
|
||
|
|
archive.addFile(
|
||
|
|
ArchiveFile('session.ndjson', bytes.length, bytes),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Legacy input log if it exists alongside.
|
||
|
|
try {
|
||
|
|
Directory dir;
|
||
|
|
try {
|
||
|
|
dir = await getApplicationDocumentsDirectory();
|
||
|
|
} catch (_) {
|
||
|
|
dir = await getTemporaryDirectory();
|
||
|
|
}
|
||
|
|
final legacy = File(
|
||
|
|
'${dir.path}${Platform.pathSeparator}badnote_input_log.txt',
|
||
|
|
);
|
||
|
|
if (await legacy.exists()) {
|
||
|
|
final bytes = await legacy.readAsBytes();
|
||
|
|
archive.addFile(
|
||
|
|
ArchiveFile('legacy_input_log.txt', bytes.length, bytes),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
|
||
|
|
final encoded = ZipEncoder().encode(archive);
|
||
|
|
if (encoded.isEmpty) {
|
||
|
|
throw StateError('Failed to encode diagnostic zip');
|
||
|
|
}
|
||
|
|
|
||
|
|
Directory outDir = log.directory ??
|
||
|
|
Directory(
|
||
|
|
'${(await getApplicationDocumentsDirectory()).path}'
|
||
|
|
'${Platform.pathSeparator}badnote_diagnostics',
|
||
|
|
);
|
||
|
|
if (!await outDir.exists()) {
|
||
|
|
await outDir.create(recursive: true);
|
||
|
|
}
|
||
|
|
final stamp = DateTime.now()
|
||
|
|
.toIso8601String()
|
||
|
|
.replaceAll(':', '-')
|
||
|
|
.replaceAll('.', '-');
|
||
|
|
final zipPath =
|
||
|
|
'${outDir.path}${Platform.pathSeparator}badnote_diag_$stamp.zip';
|
||
|
|
await File(zipPath).writeAsBytes(encoded, flush: true);
|
||
|
|
|
||
|
|
BadNoteLog.instance.info(
|
||
|
|
LogSubsystem.diag,
|
||
|
|
'export_pack',
|
||
|
|
fields: {'path': zipPath, 'bytes': encoded.length},
|
||
|
|
);
|
||
|
|
|
||
|
|
return DiagnosticExportResult(zipPath: zipPath, bytes: encoded.length);
|
||
|
|
}
|
||
|
|
}
|