Invoking scripts that handle CTRL-C from Kotlin/Java
Android has scripts for collecting profiles (record_android_trace
for
perfetto and
app_profiler.py
for
simpleperf)
that collect profiling information until CTRL-C is pressed. After CTRL-C they
will flush the profiling data and pull it from the device.
If you try to invoke these scripts from inside a Kotlin script (or any Kotlin/Java program in general) then the inner script will run inside the same process group as the Kotlin script, which means that any signal (such as the SIGINT that is generated in response to CTRL-C) will be received by both processes simultaneously. The Kotlin script will exit almost immediately, before the inner script has finished flushing/pulling.
How do you fix it?
val executable = "app_profiler.py"
val args = listOf<String>()
val process = ProcessBuilder("sh", "-i", "-c", "\"$0\" \"$@\"", executable, *args.toTypedArray())
.inheritIO() // Ensures stdout/stderr go to the console
.start()
.waitFor()
We run our inner script via the shell. -i
forces the shell to run
interactively, even though we are passing a command with -c
. In interactive
mode the shell handles job control - it runs its child in a separate process
group, as the terminal owner. This means the child receives signals, not the
parent (And the shell handles some other signals too - it's actually somewhat
involved to handle job control correctly)
The "$0" "$@"
stuff is just a small trick to avoid string splitting issues.
If you know your arguments exactly and they don't contain any spaces you can
provide your command to sh
as a single string.
I discovered this solution as I was researching processes/groups/signals/etc and trying to manually call the right syscalls. Eventually I realized I was basically building my own shell, so I looked into how I could use the shell to do it for me. It's amazing how much easier it is when you use the right tool for the job!
If you enjoyed this post, please let me know on Twitter or Bluesky.
Posted January 23, 2025.