Skip to content

Commit d394d74

Browse files
committed
fix(android): guard daemon startup against unexpected runtime crashes
1 parent 5aad53e commit d394d74

1 file changed

Lines changed: 74 additions & 40 deletions

File tree

app/src/main/java/com/zeroclaw/android/service/ZeroClawDaemonService.kt

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import com.zeroclaw.ffi.FfiException
3838
import com.zeroclaw.ffi.validateConfig
3939
import kotlinx.coroutines.CoroutineDispatcher
4040
import kotlinx.coroutines.CoroutineScope
41+
import kotlinx.coroutines.CancellationException
4142
import kotlinx.coroutines.Dispatchers
4243
import kotlinx.coroutines.Job
4344
import kotlinx.coroutines.SupervisorJob
@@ -219,49 +220,57 @@ class ZeroClawDaemonService : Service() {
219220
acquireWakeLock()
220221

221222
serviceScope.launch(ioDispatcher) {
222-
val settings = settingsRepository.settings.first()
223-
val effectiveSettings = resolveEffectiveDefaults(settings)
224-
val apiKey = apiKeyRepository.getByProviderFresh(effectiveSettings.defaultProvider)
225-
226-
val globalConfig =
227-
buildGlobalTomlConfig(effectiveSettings, apiKey)
228-
val baseToml = ConfigTomlBuilder.build(globalConfig)
229-
val channelsToml =
230-
ConfigTomlBuilder.buildChannelsToml(
231-
channelConfigRepository.getEnabledWithSecrets(),
232-
)
233-
val agentsToml = buildAgentsToml()
234-
val configToml = baseToml + channelsToml + agentsToml
235-
236-
if (!validateConfigOrStop(configToml)) return@launch
237-
238-
val conflict = bridge.detectMemoryConflict(effectiveSettings.memoryBackend)
239-
if (conflict is MemoryConflict.StaleData) {
240-
val shouldDelete = bridge.awaitConflictResolution(conflict)
241-
if (shouldDelete) {
242-
bridge.cleanupStaleMemory(conflict)
243-
logRepository.append(
244-
LogSeverity.INFO,
245-
TAG,
246-
"Cleaned up ${conflict.staleFileCount} stale " +
247-
"${conflict.staleBackend} memory files",
223+
try {
224+
val settings = settingsRepository.settings.first()
225+
val effectiveSettings = resolveEffectiveDefaults(settings)
226+
val apiKey = apiKeyRepository.getByProviderFresh(effectiveSettings.defaultProvider)
227+
228+
val globalConfig =
229+
buildGlobalTomlConfig(effectiveSettings, apiKey)
230+
val baseToml = ConfigTomlBuilder.build(globalConfig)
231+
val channelsToml =
232+
ConfigTomlBuilder.buildChannelsToml(
233+
channelConfigRepository.getEnabledWithSecrets(),
248234
)
249-
}
250-
}
235+
val agentsToml = buildAgentsToml()
236+
val configToml = baseToml + channelsToml + agentsToml
251237

252-
retryPolicy.reset()
253-
val validPort =
254-
if (settings.port in VALID_PORT_RANGE) {
255-
settings.port
256-
} else {
257-
AppSettings.DEFAULT_PORT
238+
if (!validateConfigOrStop(configToml)) return@launch
239+
240+
val conflict = bridge.detectMemoryConflict(effectiveSettings.memoryBackend)
241+
if (conflict is MemoryConflict.StaleData) {
242+
val shouldDelete = bridge.awaitConflictResolution(conflict)
243+
if (shouldDelete) {
244+
bridge.cleanupStaleMemory(conflict)
245+
logRepository.append(
246+
LogSeverity.INFO,
247+
TAG,
248+
"Cleaned up ${conflict.staleFileCount} stale " +
249+
"${conflict.staleBackend} memory files",
250+
)
251+
}
258252
}
259-
attemptStart(
260-
configToml = configToml,
261-
host = settings.host,
262-
port = validPort.toUShort(),
263-
memoryBackend = effectiveSettings.memoryBackend,
264-
)
253+
254+
retryPolicy.reset()
255+
val validPort =
256+
if (settings.port in VALID_PORT_RANGE) {
257+
settings.port
258+
} else {
259+
AppSettings.DEFAULT_PORT
260+
}
261+
attemptStart(
262+
configToml = configToml,
263+
host = settings.host,
264+
port = validPort.toUShort(),
265+
memoryBackend = effectiveSettings.memoryBackend,
266+
)
267+
} catch (e: CancellationException) {
268+
throw e
269+
} catch (e: Throwable) {
270+
handleUnexpectedStartupError(
271+
LogSanitizer.sanitizeLogMessage(e.message ?: e.javaClass.simpleName),
272+
)
273+
}
265274
}
266275
}
267276

@@ -661,6 +670,15 @@ class ZeroClawDaemonService : Service() {
661670
handleStartupExhausted(errorMsg)
662671
return@launch
663672
}
673+
} catch (e: CancellationException) {
674+
throw e
675+
} catch (e: Throwable) {
676+
handleStartupExhausted(
677+
LogSanitizer.sanitizeLogMessage(
678+
e.message ?: e.javaClass.simpleName,
679+
),
680+
)
681+
return@launch
664682
}
665683
}
666684
} finally {
@@ -696,6 +714,22 @@ class ZeroClawDaemonService : Service() {
696714
stopSelf()
697715
}
698716

717+
/**
718+
* Handles unexpected startup errors thrown before retry loop starts.
719+
*
720+
* This prevents fatal coroutine crashes from taking down the app process
721+
* when non-FFI runtime exceptions occur while preparing daemon config.
722+
*/
723+
private fun handleUnexpectedStartupError(errorMsg: String) {
724+
Log.e(TAG, "Daemon startup aborted by unexpected error: $errorMsg")
725+
logRepository.append(LogSeverity.ERROR, TAG, "Startup aborted: $errorMsg")
726+
activityRepository.record(ActivityType.DAEMON_ERROR, "Startup aborted: $errorMsg")
727+
notificationManager.updateNotification(ServiceState.ERROR, errorDetail = errorMsg)
728+
releaseWakeLock()
729+
stopForeground(STOP_FOREGROUND_DETACH)
730+
stopSelf()
731+
}
732+
699733
/**
700734
* Starts a coroutine that periodically polls the daemon for status.
701735
*

0 commit comments

Comments
 (0)