androidreact-nativevisual-studio-codeandroid-emulatorzsh

How to completely detach a process when running a package.json script VSCode?


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:

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)

Thanks in advance!


Solution

  • 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 daemonized 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.