Skip to content

fix(server): don't crash at startup when the server socket can't be created - #315

Merged
ErikBjare merged 1 commit into
ActivityWatch:masterfrom
TimeToBuildBob:fix/degoogled-startup-crash
Oct 10, 2026
Merged

ErikBjare merged 1 commit into
ActivityWatch:masterfrom
TimeToBuildBob:fix/degoogled-startup-crash

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes a startup crash on GrapheneOS when the app's Network permission is off. Related reports: ActivityWatch/activitywatch#1003, ActivityWatch/activitywatch#1180, #124.

Root cause

RustInterface.startServerTask() probes the server port with a ServerSocket before starting Rust, but it only catches BindException. With GrapheneOS's per-app Network permission revoked, socket() fails with EACCES. That surfaces as a plain SocketException, the parent of BindException, so it escapes. The probe runs inside the Dispatchers.IO coroutine in BackgroundService.onStartCommand, which has no exception handler, so the uncaught exception kills the app at launch.

The probe now returns FREE / IN_USE / SOCKET_DENIED. A denied socket logs an actionable error ("check the app's Network permission") and the server is not started, instead of the app crashing.

How the older reports fit

Testing

  • New ServerPortProbeTest: free port, port held by another socket (a real ServerSocket), SocketException (EACCES), and SecurityException. I ran it locally with :mobile:testStandardDebugUnitTest.
  • Repro on an Android 16 emulator (release 0.14.1). The app starts cleanly with Chrome disabled, under bionic malloc-debug (guard fill free_track verify_pointers, as a stand-in for hardened_malloc), and under CheckJNI. None of these produce allocator or JNI errors.

Not in this PR: a user-facing "Network permission required" screen. Currently the denial is only logged, and the UI shows the usual server-unreachable state.

Reproduce

  • Device (GrapheneOS): Settings → Apps → ActivityWatch → Permissions → turn Network off → launch the app. On master the uncaught java.net.SocketException from the port probe in RustInterface.startServerTask kills the app at launch. With this PR the app starts and logcat shows the "check the app's Network permission" error. (I have no GrapheneOS device, so this path follows from the code above and the reports; I have not run it myself.)
  • Stock Android cannot reproduce it: INTERNET is an install-time permission there and can't be revoked, so the device path needs GrapheneOS.
  • Without a device: ./gradlew :mobile:testStandardDebugUnitTest --tests '*ServerPortProbeTest'. socketCreationDeniedDoesNotEscape injects the EACCES SocketException, the same exception GrapheneOS raises.

…reated

The port probe in startServerTask() only caught BindException. On
GrapheneOS with the app's Network permission off, socket() fails with
EACCES, surfacing as a plain SocketException (BindException's parent),
which escaped the IO coroutine in BackgroundService and killed the app.

Extract the probe into probeServerPort() returning FREE/IN_USE/
SOCKET_DENIED and log an actionable error instead of crashing.

Git-Session-Id: 8192
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds error handling for server startup socket creation.

The PR appears safe to merge, though restoring Network permission will not revive the server until the service starts again.

Findings

  1. P2 Server stays stopped after permission ▶

Summary

The PR classifies server-port probe failures so socket-creation denial prevents server startup without crashing the app.

  • Adds probe tests for free, occupied, and denied sockets.
  • Recovery after permission is restored still requires another service start.

Reviews (1) · Last reviewed commit: "fix(server): don't crash at startup when..."

Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt
@TimeToBuildBob

TimeToBuildBob commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Safe to merge — no P0/P1 findings on latest review

Updated after inline dispositions on finding threads — this is the current state; the verdict below is frozen at review time and is kept as the historical record of that pass.

Finding disposition
Finding Severity State
mobile/src/main/java/net/activitywatch/android/RustInterface.kt:44 P2 rejected

The PR refactors the server-port probe in RustInterface.kt into a new probeServerPort() function that returns a PortProbe enum (FREE, IN_USE, SOCKET_DENIED), catching BindException, IOException, and SecurityException. startServerTask() now switches on the probe result and returns early for IN_USE and SOCKET_DENIED instead of only catching BindException. A new test file ServerPortProbeTest.kt covers the probe's four outcomes.

Needs a look — P2 only

Confidence 4/5

1 finding · ⚠️ 1 P2

⚠️ P2 medium — mobile/src/main/java/net/activitywatch/android/RustInterface.kt:44

The probeServerPort() function catches java.io.IOException broadly and treats it as SOCKET_DENIED, but the default open lambda is java.net.ServerSocket(it), and ServerSocket's constructor can throw other IOExceptions that are not permission denials. For example, if the port is already in use, the constructor throws BindException (caught first), but if the port number is out of range (e.g., 0 is valid, but negative or >65535), it throws IllegalArgumentException, which is not caught. More importantly, on some systems, binding to a privileged port (<1024) may throw SocketException with EACCES, which is correctly caught, but other IOExceptions like "Too many open files" (IOException with EMFILE) would also be classified as SOCKET_DENIED, logging a misleading message about Network permission. The observable consequence is that a transient resource exhaustion (EMFILE) or other non-permission IO failure is reported as a permission denial, and the server is not started, even though the user's Network permission is fine. The fix would be to catch SocketException specifically (or check the cause/message for EACCES) rather than all IOExceptions, or at least log the actual exception message. However, the PR's stated goal is to prevent crashes, and this misclassification is a lower-impact issue. The test for SocketException uses a message with EACCES, but the code does not inspect the message, so it would also classify any SocketException as denied. This is a real defect in the classification logic, but the impact is a misleading log and a failure to start the server on a non-permission IO error. Severity P2.

} catch (e: java.net.SocketException) {

How this was verified: Checked the catch blocks in probeServerPort() and the default open lambda. ServerSocket constructor throws IOException for various reasons; only BindException is distinguished. The test only covers SocketException with EACCES message, but the code does not inspect the message.

Consensus: 2/3 passes agreed — pass 2 looked and disagreed
Distinct keys: 1 (general)

Files changed (2) — the diff as I read it
  • mobile/src/main/java/net/activitywatch/android/RustInterface.kt — Adds PortProbe enum and probeServerPort() function; rewrites startServerTask() to switch on probe result and handle SOCKET_DENIED.
  • mobile/src/test/java/net/activitywatch/android/ServerPortProbeTest.kt — Adds unit tests for probeServerPort() covering FREE, IN_USE, SocketException, SecurityException, and a real ServerSocket.

Reviewed 5a44cecef81c · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 1028s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Comment thread mobile/src/main/java/net/activitywatch/android/RustInterface.kt
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