As part of automating away the tedious bits of React Native development I'm trying to run and detach the Android emulator as part of a package.json
script when I start a development build of the Android app from VSCode but the emulator is killed whenever the script command completes ("Terminal will be reused..."). I'm on a MacBook M3. My setup is slightly more complicated than a simple npx react-native run-android
; the following is the bare essentials:
I have a command in the scripts
section of package.json
:
{
...
"scripts" : {
"Run Android build" : "make run_android"
}
}
This calls a task-runner Makefile command that sources a shell file and runs a function:
run_android:
@. "./tasks.sh"; \
run_android
This then calls a function in a zsh shell file:
function start_emulator {
# Only start the emulator if it's not already running
if pgrep -x "qemu-system-aarch64" >/dev/null; then
echo "Emulator is running"
# soft_powercycle_android_emulator # ...for instance
else
# How do I properly detach the emulator process?
emulator -avd Pixel_7_Pro_API_34 -no-boot-anim </dev/null &>/dev/null &
sleep 1
adb wait-for-device
while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
sleep 1
done
fi
}
function run_android {
start_emulator
npx react-native run-android
}
This indirection gives me a clean(er) package.json
, configurability via the Makefile, and composability via the use of shell functions. It's worked well for me so far.
The above emulator ... &
line allows the emulator to start-up while the rest of the function waits for it to fully boot. The React Native build then runs, installs, and the NPM task ends, closing the emulator. The emulator does display a "Saving state..." dialogue which leads me to believe it's being sent some form of termination signal (SIGKILL, SIGTERM et al).
In addition to the above naive &
I've tried various combinations of nohup
, disown
, subshell, and setsid
, none of which work; the emulator is killed every time. What does work is modifying the emulator line to the following:
screen -d -m emulator -avd Pixel_7_Pro_API_34 -no-boot-anim </dev/null &>/dev/null
i.e. screen
properly takes parentage of the emulator process and allows it to continue running. This seems less than ideal, and may be leaving emulator zombie processes around; I've certainly seen evidence of them but am not sure where they originate from, screen
, or the emulator. It's also not my explicit intention to ever return to the emulator process so screen
seems the wrong approach - a hammer when a nut-cracker will do.
An example of the convoluted thing that the internet swears will work is:
( nohup emulator -avd Pixel_7_Pro_API_34 -no-boot-anim </dev/null &>/dev/null & ) & disown
...But it still kills the emulator, sending it a some signal and showing me a brief "Saving state..." overlay.
I've also looked into VSCode Tasks, but I'd prefer to keep the config in packages.json
.
If I run the following command from an iTerm shell...
( nohup emulator -avd Pixel_7_Pro_API_34 -no-boot-anim & ) & disown
I can see that the process is indeed detached and reparented to launchd
, and that it shares a TTY with iTerm:
$ ps -ef | pstree -g 3 -f - -s ttys231
─┬─ 00001 0 Thu11pm ?? 69:24.50 /sbin/launchd
├─┬─ 01809 502 Thu11pm ?? 70:14.10 /Applications/iTerm.app/Contents/MacOS/iTerm2
│ └─┬─ 01877 502 Thu11pm ?? 0:00.01 /Users/rmacharg/Library/Application Support/iTerm2/iTermServer-3.5.11 /Users/rmacharg/Library/Application Support/iTerm2/iterm2-daemon-1.socket
│ └─┬─ 13302 0 4:23pm ttys231 0:00.01 login -fp rmacharg
│ └─── 13304 502 4:23pm ttys231 0:00.13 -zsh
└─┬─ 13654 502 4:23pm ttys231 1:09.54 /Users/rmacharg/Library/Android/sdk/emulator/qemu/darwin-aarch64/qemu-system-aarch64 -avd Pixel_7_Pro_API_34 -no-boot-anim
└─── 13663 502 4:23pm ttys231 0:00.67 /Users/rmacharg/Library/Android/sdk/emulator/netsimd --host-dns=[REDACTED],192.168.1.1
If I then close the iTerm window and check again the emulator is now missing a TTY ('??'), and remains running:
$ ps -ef | pstree -g 3 -f - -s qemu
─┬─ 00001 0 Thu11pm ?? 69:26.07 /sbin/launchd
└─┬─ 13654 502 4:23pm ?? 2:45.76 /Users/rmacharg/Library/Android/sdk/emulator/qemu/darwin-aarch64/qemu-system-aarch64 -avd Pixel_7_Pro_API_34 -no-boot-anim
└─── 13663 502 4:23pm ?? 0:06.70 /Users/rmacharg/Library/Android/sdk/emulator/netsimd --host-dns=[REDACTED],192.168.1.1
Running the same under VSCode looks similar, but as stated above when the VSCode terminal exits it still causes the emulator to be killed.
My hunch is that VSCode is either killing everything associated with the TTY, or the destruction of the TTY is causing the emulator process to end, anad that despite issuing nohup
, disown
etc, they make no difference.
What's the correct invocation to completely detach the emulator process from the VSCode terminal such that it continues to run even when the terminal, er, terminates?
Is this a VSCode configuration issue, or a lack of understanding of shell process/job control on my part?
Is there any straightforward way to work out which SIGnal is being sent to cause the emulator to die? Can these be trapped/ignored at any level? Can they be traced up (or down) from the node
script runner to the emulator
command?
If none of the above provide answers and I absolutely must use VSCode Tasks, what's the equivalent config to achieve the detached emulator? Is it even possible? Assume a similar Makefile task-runner setup.
Thanks in advance!
For anyone landing on the question, and in the absence of a better answer... what I've gone with in the end is installing daemonize
(homepage, homebrew) which does exactly what I want - detach a process from a VSCode-derived terminal such that it survives the terminal being closed. The emulator
line, above, now looks like:
daemonize =emulator -avd Pixel_7_Pro_API_34 -no-boot-anim
It's also possible to remove the if pgrep -x "qemu-system-aarch64" >/dev/null; then...
condition by using the -l
lockfile option but to achieve the logging I have in place I'd have to implement error handling so left it as-is for now.
In the question I noted that screen
may have been causing zombie processes. Having left a daemonize
d emulator running overnight I also see zombie processes, so this appears to be a separate issue.
Another option is to install util-linux
and use the setsid
it provides. This has the same effect as the above. The emulator
line would then be:
/opt/homebrew/Cellar/util-linux/2.40.4/bin/setsid nohup emulator -avd Pixel_7_Pro_API_34 -no-boot-anim </dev/null > /dev/null 2>&1 &
The setsid
command is not installed in the default Homebrew PATH, and while it can be easily dug out (e.g. brew ls util-linux | grep bin/setsid
) I prefer the simplicity of daemonize
. Horses for courses.
I'd still be interested in how to start the emulator directly in zsh
without relying on a separate third-party tool, however well it works.