Skip to content

Commit fb4ef57

Browse files
committed
Merge branch 'error-badge'
2 parents 61be634 + bf042fb commit fb4ef57

13 files changed

Lines changed: 181 additions & 65 deletions

lib/app/app.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'package:stream_transform/stream_transform.dart';
1111

1212
import '../generated/l10n.dart';
1313
import 'cubits/cubits.dart'
14-
show LocaleCubit, LocaleState, MountCubit, ReposCubit;
14+
show LocaleCubit, LocaleState, MountCubit, ReposCubit, ErrorCubit;
1515
import 'pages/pages.dart';
1616
import 'utils/dirs.dart';
1717
import 'utils/log.dart' as log;
@@ -74,6 +74,9 @@ Future<HomeWidget> _initHomeWidget(List<String> args) async {
7474
await log.init(dirs);
7575

7676
final (server, session) = await _initServerAndSession(dirs, windowManager);
77+
78+
final errorCubit = ErrorCubit(session);
79+
7780
final settings = await loadAndMigrateSettings(session);
7881
final localeCubit = LocaleCubit(settings);
7982

@@ -85,6 +88,7 @@ Future<HomeWidget> _initHomeWidget(List<String> args) async {
8588
settings: settings,
8689
packageInfo: packageInfo,
8790
localeCubit: localeCubit,
91+
errorCubit: errorCubit,
8892
);
8993
}
9094

@@ -141,6 +145,7 @@ class HomeWidget extends StatefulWidget {
141145
required this.settings,
142146
required this.packageInfo,
143147
required this.localeCubit,
148+
required this.errorCubit,
144149
super.key,
145150
});
146151

@@ -151,6 +156,7 @@ class HomeWidget extends StatefulWidget {
151156
final Settings settings;
152157
final PackageInfo packageInfo;
153158
final LocaleCubit localeCubit;
159+
final ErrorCubit errorCubit;
154160

155161
@override
156162
State<HomeWidget> createState() => _HomeWidgetState();
@@ -218,6 +224,7 @@ class _HomeWidgetState extends State<HomeWidget>
218224
mainPage: MainPage(
219225
localeCubit: widget.localeCubit,
220226
mountCubit: mountCubit,
227+
errorCubit: widget.errorCubit,
221228
packageInfo: widget.packageInfo,
222229
receivedMedia: receivedMediaController.stream,
223230
reposCubit: reposCubit,

lib/app/cubits/cubits.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export 'error.dart';
12
export 'locale.dart';
23
export 'connectivity_info.dart';
34
export 'entry_bottom_sheet.dart';

lib/app/cubits/error.dart

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:equatable/equatable.dart';
5+
import 'package:ouisync/ouisync.dart' as native;
6+
import 'package:loggy/loggy.dart' show Loggy;
7+
import 'package:flutter/foundation.dart' show FlutterError, PlatformDispatcher;
8+
import 'utils.dart' show CubitActions;
9+
import '../utils/log.dart' show AppLogger;
10+
11+
// Watch if an error happened in different parts of the app and the ouisync library.
12+
// Only captures errors that can't be gracefuly handled, the cubit indicates to
13+
// the user that the log should be captured.
14+
class ErrorCubit extends Cubit<ErrorCubitState> with CubitActions, AppLogger {
15+
ErrorCubit(native.Session session) : super(ErrorCubitState(false)) {
16+
// TODO: This may no longer be useful. Previously, when we were
17+
// communicating with the library over FFI we could detect panics through
18+
// the state monitor. But now that we communicate with the service over the
19+
// socket, when a panic happens the socket disconnects and thus we no
20+
// longer receive events over it. However, we now receive
21+
// `native.ClientException`s when the socket disconnects which is handled
22+
// below.
23+
unawaited(
24+
_RustPanicDetectionRunner(this, loggy, session.rootStateMonitor).init(),
25+
);
26+
27+
// These are printed in utils/log.dart. Here we just mark the state with
28+
// error for the error badge indicator to light up.
29+
final defaultFlutterOnError = FlutterError.onError;
30+
FlutterError.onError = (details) {
31+
_emitError();
32+
33+
if (defaultFlutterOnError != null) {
34+
defaultFlutterOnError(details);
35+
}
36+
};
37+
38+
final defaultPlatformOnError = PlatformDispatcher.instance.onError;
39+
PlatformDispatcher.instance.onError = (exception, stack) {
40+
_emitError();
41+
42+
if (defaultPlatformOnError != null) {
43+
return defaultPlatformOnError(exception, stack);
44+
} else {
45+
// We're not writing the log, returning `false` means someone else will.
46+
return false;
47+
}
48+
};
49+
}
50+
51+
void _emitError() {
52+
emitUnlessClosed(ErrorCubitState(true));
53+
}
54+
}
55+
56+
class ErrorCubitState extends Equatable {
57+
final bool errorHappened;
58+
59+
ErrorCubitState(this.errorHappened);
60+
61+
@override
62+
List<Object> get props => [errorHappened];
63+
}
64+
65+
class _RustPanicDetectionRunner {
66+
final ErrorCubit _errorCubit;
67+
final Loggy _loggy;
68+
final native.StateMonitor _serviceStateMonitor;
69+
70+
_RustPanicDetectionRunner(
71+
this._errorCubit,
72+
this._loggy,
73+
native.StateMonitor rootStateMonitor,
74+
) : _serviceStateMonitor = rootStateMonitor.child(
75+
native.MonitorId.expectUnique("Service"),
76+
);
77+
78+
Future<void> init() async {
79+
try {
80+
await _load();
81+
82+
await for (final _ in _serviceStateMonitor.changes) {
83+
await _load();
84+
}
85+
} catch (e) {
86+
_loggy.error("Rust panic detection: $e");
87+
_errorCubit._emitError();
88+
}
89+
}
90+
91+
Future<void> _load() async {
92+
final node = await _serviceStateMonitor.load();
93+
94+
if (node == null) {
95+
throw "Failed to find Service node in state monitor";
96+
}
97+
98+
final value = node.values['panic_counter'];
99+
100+
if (value == null) {
101+
throw "Failed to find panic_counter value in Service state monitor node";
102+
}
103+
104+
final count = int.tryParse(value);
105+
106+
if (count == null) {
107+
throw "Failed to parse panic_counter value ($value)";
108+
}
109+
110+
if (count > 0) {
111+
throw "Detected panic in rust code";
112+
}
113+
}
114+
}

lib/app/cubits/state_monitor.dart

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,3 @@ class StateMonitorCubit extends Cubit<StateMonitorNode?> with CubitActions {
2929
StateMonitorCubit child(MonitorId id) =>
3030
StateMonitorCubit(_monitor.child(id));
3131
}
32-
33-
class StateMonitorIntCubit extends Cubit<int?> with CubitActions {
34-
final StateMonitor _monitor;
35-
final String _name;
36-
37-
StateMonitorIntCubit(this._monitor, this._name) : super(null) {
38-
unawaited(_init());
39-
}
40-
41-
Future<void> _init() async {
42-
await _load();
43-
44-
await for (final _ in _monitor.changes) {
45-
await _load();
46-
}
47-
}
48-
49-
Future<void> _load() async {
50-
final node = await _monitor.load();
51-
emitUnlessClosed(node?.parseIntValue(_name));
52-
}
53-
}

lib/app/pages/main_page.dart

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'package:build_context_provider/build_context_provider.dart';
55
import 'package:collection/collection.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_bloc/flutter_bloc.dart';
8-
import 'package:ouisync/ouisync.dart' show EntryType, MonitorId, Session;
8+
import 'package:ouisync/ouisync.dart' show EntryType, Session;
99
import 'package:package_info_plus/package_info_plus.dart';
1010
import 'package:path/path.dart' as system_path;
1111
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
@@ -36,6 +36,7 @@ class MainPage extends StatefulWidget {
3636
required this.packageInfo,
3737
required this.receivedMedia,
3838
required this.reposCubit,
39+
required this.errorCubit,
3940
required this.session,
4041
required this.settings,
4142
required this.windowManager,
@@ -48,6 +49,7 @@ class MainPage extends StatefulWidget {
4849
final PackageInfo packageInfo;
4950
final Stream<List<SharedMediaFile>> receivedMedia;
5051
final ReposCubit reposCubit;
52+
final ErrorCubit errorCubit;
5153
final MountCubit mountCubit;
5254
final LocaleCubit localeCubit;
5355
final Dirs dirs;
@@ -58,7 +60,6 @@ class MainPage extends StatefulWidget {
5860

5961
class _MainPageState extends State<MainPage>
6062
with TickerProviderStateMixin, AppLogger {
61-
late final StateMonitorIntCubit panicCounter;
6263
late final PowerControl powerControl = PowerControl(
6364
widget.session,
6465
widget.settings,
@@ -85,13 +86,6 @@ class _MainPageState extends State<MainPage>
8586
void initState() {
8687
super.initState();
8788

88-
panicCounter = StateMonitorIntCubit(
89-
widget.reposCubit.rootStateMonitor.child(
90-
MonitorId.expectUnique("Service"),
91-
),
92-
"panic_counter",
93-
);
94-
9589
upgradeExists = UpgradeExistsCubit(widget.session, widget.settings);
9690

9791
sortListCubit = SortListCubit.create(
@@ -121,7 +115,6 @@ class _MainPageState extends State<MainPage>
121115
unawaited(upgradeExists.close());
122116
unawaited(sortListCubit.close());
123117
unawaited(powerControl.close());
124-
unawaited(panicCounter.close());
125118

126119
super.dispose();
127120
}
@@ -319,7 +312,7 @@ class _MainPageState extends State<MainPage>
319312
reposCubit: widget.reposCubit,
320313
repoPicker: RepositoriesBar(
321314
mount: widget.mountCubit,
322-
panicCounter: panicCounter,
315+
errorCubit: widget.errorCubit,
323316
powerControl: powerControl,
324317
reposCubit: widget.reposCubit,
325318
upgradeExists: upgradeExists,
@@ -331,7 +324,7 @@ class _MainPageState extends State<MainPage>
331324

332325
Widget _buildAppSettingsIcon() => NotificationBadge(
333326
mount: widget.mountCubit,
334-
panicCounter: panicCounter,
327+
errorCubit: widget.errorCubit,
335328
powerControl: powerControl,
336329
upgradeExists: upgradeExists,
337330
moveDownwards: 5,
@@ -1009,7 +1002,7 @@ class _MainPageState extends State<MainPage>
10091002
session: widget.session,
10101003
localeCubit: widget.localeCubit,
10111004
mount: widget.mountCubit,
1012-
panicCounter: panicCounter,
1005+
errorCubit: widget.errorCubit,
10131006
powerControl: powerControl,
10141007
reposCubit: widget.reposCubit,
10151008
upgradeExists: upgradeExists,

lib/app/pages/settings_page.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class SettingsPage extends StatefulWidget {
1414
required this.session,
1515
required this.localeCubit,
1616
required this.mount,
17-
required this.panicCounter,
17+
required this.errorCubit,
1818
required this.powerControl,
1919
required this.reposCubit,
2020
required this.upgradeExists,
@@ -25,7 +25,7 @@ class SettingsPage extends StatefulWidget {
2525
final Session session;
2626
final LocaleCubit localeCubit;
2727
final MountCubit mount;
28-
final StateMonitorIntCubit panicCounter;
28+
final ErrorCubit errorCubit;
2929
final PowerControl powerControl;
3030
final ReposCubit reposCubit;
3131
final UpgradeExistsCubit upgradeExists;
@@ -73,7 +73,7 @@ class _SettingsPageState extends State<SettingsPage> {
7373
widget.session,
7474
localeCubit: widget.localeCubit,
7575
mount: widget.mount,
76-
panicCounter: widget.panicCounter,
76+
errorCubit: widget.errorCubit,
7777
powerControl: widget.powerControl,
7878
reposCubit: widget.reposCubit,
7979
connectivityInfo: connectivityInfo,

lib/app/utils/log.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,32 @@ ${'-' * (48 + package.appName.length)}''';
5656
};
5757

5858
if (captureErrors) {
59+
final defaultFlutterOnError = FlutterError.onError;
5960
FlutterError.onError = (details) {
6061
logger.error(
6162
'Unhandled flutter exception: ',
6263
details.exception,
6364
details.stack,
6465
);
6566

66-
FlutterError.presentError(details);
67+
if (defaultFlutterOnError != null) {
68+
defaultFlutterOnError(details);
69+
} else {
70+
FlutterError.presentError(details);
71+
}
6772
};
6873

6974
// NOTE: if sentry is used, it will override these methods but still call
7075
// them after processing the events, so they are not lost
76+
final defaultPlatformOnError = PlatformDispatcher.instance.onError;
7177
PlatformDispatcher.instance.onError = (exception, stack) {
72-
logger.error('Unhandled platform exception: ', exception, stack);
78+
bool captured = false;
79+
if (defaultPlatformOnError != null) {
80+
captured = defaultPlatformOnError(exception, stack);
81+
}
82+
if (!captured) {
83+
logger.error('Unhandled platform exception: ', exception, stack);
84+
}
7385
return true;
7486
};
7587
}

lib/app/widgets/bars/repositories_bar.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ class RepositoriesBar extends StatelessWidget
1515
implements PreferredSizeWidget {
1616
const RepositoriesBar({
1717
required this.mount,
18-
required this.panicCounter,
18+
required this.errorCubit,
1919
required this.powerControl,
2020
required this.reposCubit,
2121
required this.upgradeExists,
2222
super.key,
2323
});
2424

2525
final MountCubit mount;
26-
final StateMonitorIntCubit panicCounter;
26+
final ErrorCubit errorCubit;
2727
final PowerControl powerControl;
2828
final ReposCubit reposCubit;
2929
final UpgradeExistsCubit upgradeExists;
@@ -110,7 +110,7 @@ class RepositoriesBar extends StatelessWidget
110110
// TODO: Why does the badge appear to move quickly after entering this screen?
111111
Widget _buildBackButton() => NotificationBadge(
112112
mount: mount,
113-
panicCounter: panicCounter,
113+
errorCubit: errorCubit,
114114
powerControl: powerControl,
115115
upgradeExists: upgradeExists,
116116
moveDownwards: 5,

0 commit comments

Comments
 (0)