The problem
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 ofpackage.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).
What I've tried
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
.
Update
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.
The question(s)
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 theemulator
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!