Skip to content

MacOS startup fix, iOS service recovery and file open/share on background - #464

Closed
panfilov-vladislav wants to merge 15 commits into
masterfrom
fix-macos-spinning-ios-crash-on-background
Closed

panfilov-vladislav wants to merge 15 commits into
masterfrom
fix-macos-spinning-ios-crash-on-background

Conversation

@panfilov-vladislav

Copy link
Copy Markdown
Collaborator

Summary

  • Show startup errors on screen instead of an endless spinner (StartupErrorScreen).
  • Bump the ouisync submodule to pick up the QUIC busy-loop fix, service listener rebind, file-based native logging and dead-service detection.
  • Reload repositories and reset the connection after reconnect or error.
  • Capture error messages and stack traces, viewable from Settings.
  • Bump version to 0.9.5+80.

Vlad Panfilov added 15 commits October 20, 2026 09:05
windowManager.close() simulates a click on the native close button
(performClose), which is a no-op when the window is hidden (e.g. after
"Hide" from the tray menu), silently preventing the app from exiting.
Run the shutdown directly instead, independent of window visibility.
PowerControl only called bindNetwork() when the computed NetworkMode
changed, which is driven by connectivity_plus's connectivity *type*
(wifi/mobile/none) and local interface address. Backgrounding can kill
the network stack (e.g. iOS tearing down sockets) without the type
ever changing, so nothing ever re-triggered bindNetwork() on resume,
and the ouisync-net layer's rebind-on-death logic never got a chance
to run.

Observe app lifecycle and call _setNetworkMode directly (bypassing the
mode-unchanged guard) on resume, so a fresh bindNetwork is always sent.
Flutter's VM service discovery over the device's local network tunnel
(used by `flutter attach`/`flutter run` on physical iOS devices) relies
on mDNS and the Local Network permission, which the OS won't even
prompt for without these Info.plist keys declared.
ErrorCubit only ever tracked a boolean "something went wrong" flag, with no
way to see what actually happened short of digging through the log file.
Record the message and stack trace for each unhandled exception (capped at
the last 20), and let tapping the error row in Settings > Logs open a
dialog listing them, with a button to copy the text to the clipboard.
_setNetworkMode deliberately rethrows on failure for callers reacting to a
user-driven mode change, but didChangeAppLifecycleState called it unguarded.
Right after resuming, the connection to the backend may still be
mid-reconnect, so a failure here is expected and transient; swallow it
instead of letting it surface as an unhandled exception.
Most of what trips ErrorCubit in practice is a symptom of the local
Client connection being dead or wedged after the app resumes from the
background (dropped sockets, subscriptions that silently stopped
delivering). Rather than only reacting to errors the Client already
knows how to self-heal from, proactively force a reset whenever an
error is recorded, debounced so a burst of errors around one disconnect
triggers a single reset instead of piling up overlapping ones.
Appends the native (Rust) logs to the same file the Dart side logs to,
so they show up in the app's Save/Share logs - on iOS/macOS the native
side's stdout isn't otherwise captured anywhere once the app is
backgrounded.

Also listens for Session.serviceUnresponsive and restarts the native
service (stop/start + reset the client connection) when it fires: a
refused connection after several attempts means the native
process/thread died outright (observed after some iOS
background/foreground cycles), not just that its socket was torn
down, so nothing but restarting it will recover.
Every RepoCubit holds a Repository bound to a native handle from
whatever service process opened it. After the service has been fully
restarted (see previous commit) rather than just having its socket
momentarily drop, that handle no longer refers to anything meaningful
in the new process - at best this shows as stale/empty content (the
new process happens to reuse the same handle number for something
else), at worst an operation on it just hangs forever (nothing is
listening on the other end to ever reply).

Merely re-fetching through the same stale Repository object doesn't
fix this; every repo needs to be closed and reopened against the new
process, which is exactly what ReposCubit._reload() already does for
the "store dirs changed" case - so reuse it on Session.reconnected too.
When app startup failed, the FutureBuilder in App only checked for data,
so the loading spinner stayed on screen and the cause was hidden. Log the
failure under the Startup logger and show the error and stack trace on a
new StartupErrorScreen instead.
Comment thread lib/app/app.dart
// threshold within the same tick, each firing their own event - guard against the resulting
// pile-up of concurrent restarts (harmless in practice, since `server.start()` just rejects the
// extra ones with `serviceAlreadyRunning`, but there's no reason to let them race).
bool _restartingUnresponsiveService = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this being global. There should be only one Session instance per app anyway so putting this into _AppComponents should be sufficient. Or remove it altogether and throttle or debounce the serviceUnresponsive stream instead (stream_transform might be useful for this).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants