Skip to content

Commit d2fbb61

Browse files
committed
run on main thread by default
1 parent 2db12f1 commit d2fbb61

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.squareup.stoic.target.runtime
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import java.util.concurrent.CountDownLatch
6+
import java.util.concurrent.TimeUnit
7+
import java.util.concurrent.TimeoutException
8+
9+
/**
10+
* Utility for executing code on the main thread with a timeout.
11+
*/
12+
object MainThreadExecutor {
13+
private val mainHandler = Handler(Looper.getMainLooper())
14+
15+
/**
16+
* Executes the given block on the main thread, blocking the calling thread until completion.
17+
*
18+
* If the main thread cannot be acquired within the timeout, captures the main thread's
19+
* stack trace and throws a TimeoutException with that stack trace as the cause.
20+
*
21+
* @param timeoutMillis Maximum time to wait for the main thread (default: 5000ms)
22+
* @param block The code to execute on the main thread
23+
* @return The result of executing the block
24+
* @throws TimeoutException if the main thread cannot be acquired within the timeout
25+
*/
26+
fun <T> runOnMainThread(timeoutMillis: Long = 5000, block: () -> T): T {
27+
// If we're already on the main thread, just execute directly
28+
if (Looper.myLooper() == Looper.getMainLooper()) {
29+
return block()
30+
}
31+
32+
val latch = CountDownLatch(1)
33+
var result: T? = null
34+
var exception: Throwable? = null
35+
36+
mainHandler.post {
37+
try {
38+
result = block()
39+
} catch (e: Throwable) {
40+
exception = e
41+
} finally {
42+
latch.countDown()
43+
}
44+
}
45+
46+
val completed = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
47+
48+
if (!completed) {
49+
// Timeout - capture main thread stack trace
50+
val mainThread = Looper.getMainLooper().thread
51+
val mainThreadStackTrace = mainThread.stackTrace
52+
53+
val stackTraceException = Exception("Main thread stack trace at timeout").apply {
54+
stackTrace = mainThreadStackTrace
55+
}
56+
57+
throw TimeoutException(
58+
"Failed to acquire main thread within ${timeoutMillis}ms. " +
59+
"Main thread may be blocked or busy."
60+
).apply {
61+
initCause(stackTraceException)
62+
}
63+
}
64+
65+
exception?.let { throw it }
66+
67+
@Suppress("UNCHECKED_CAST")
68+
return result as T
69+
}
70+
}

target/app-sdk/src/main/kotlin/com/squareup/stoic/target/runtime/StoicPluginServer.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,13 @@ class StoicPluginServer(
251251
var exitCode = -1
252252
val t = thread {
253253
exitCode = pluginStoic.callWith {
254-
plugin.run(startPlugin.pluginArgs)
254+
// Run plugin on main thread to ensure thread-safe execution
255+
// Use a mutable variable to capture the return value from runOnMainLooper
256+
var result = -1
257+
pluginStoic.runOnMainLooper(timeoutMs = 5000) {
258+
result = plugin.run(startPlugin.pluginArgs)
259+
}
260+
result
255261
}
256262

257263
// We write PluginFinished to signal the client to send StreamClosed(STDIN), which signals us

target/app-sdk/src/main/kotlin/com/squareup/stoic/target/runtime/StoicUnixDomainSocketServer.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ private fun startServer(stoicDir: String, context: Context?) {
7777
val appContext = context?.applicationContext ?: retrieveApplicationContextViaReflection()
7878
val embeddedPlugins = if (appContext != null) {
7979
Log.d("stoic", "Loading Stoic config...")
80-
StoicConfigLoader.loadConfig(appContext).getEmbeddedPlugins(appContext)
80+
// Run on main thread to ensure thread-safe initialization
81+
MainThreadExecutor.runOnMainThread {
82+
StoicConfigLoader.loadConfig(appContext).getEmbeddedPlugins(appContext)
83+
}
8184
} else {
8285
Log.w("stoic", "No context available - skipping config loading")
8386
emptyMap()

0 commit comments

Comments
 (0)