0

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 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).

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 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!

1 Answer 1

0

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.

2
  • 1
    Great idea. FYI, since you are using zsh, you can write the line a bit simpler as daemonize =emulator -avd Pixel_7_Pro_API_34 -no-boot-anim. Commented Apr 7 at 11:47
  • Thanks for the hint; I've updated the answer to incorporate it. Commented Apr 7 at 13:22

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.