Run Homebrew services from a dedicated user without logging into it (relevant for macOS only)

Wazakindjes c7d7af0927 Fix some fuckups LEL 2 weeks ago
cmd 780adc0ba8 Final shit MAYBE 3 weeks ago
.gitignore bc4ca91dba Ayy lmao 3 weeks ago
LICENSE 50b9d58655 Ayy first legit version pr0b LMOA 3 weeks ago
LICENSE_HOMEBREW 50b9d58655 Ayy first legit version pr0b LMOA 3 weeks ago
README.md c7d7af0927 Fix some fuckups LEL 2 weeks ago

README.md

The fuck is this

This is a qt lil h4cc to get homebrew-services to bnice when running things from a dedicated Homebrew user, without "physically" logging into said user (i.e. run shit completely backgrounded). This requires special handling on macOS because it runs daemons/services differently than papi Linux.

Since the need for such background services originated from my multi-user but single-Homebrew setup, I'll also be explaining about how to set that up.

U fukn w0t?¿÷¿/?

Kkkkkkkkkkkkkk so to clarify the exact use case this shit was made for:

  1. Having multiple user accounts on yo Mac, e.g. separate accounts for work and nothing pers0nnel.
  2. Both need access to brew. While you could have separate instances many pre-compiled "bottles" will instead have to be compiled from scratch, plus you'll have dupes of anything else regardless.
  3. So, there needs to be a third user (e.g. muhbrew) that will be used for managing the Homebrew instance and really doing anything else Homebrew-related.
  4. One problem that arises from this is having to run shit like su -l muhbrew -c 'brew install foo', which is not only fairly long but also will keep asking for a password.
  5. Another is that macOS's launchd/launchctl shit generally assumes there's a GUI session for the user launching services. SSHing into the third user's account or going through su/sudo doesn't spawn a GUI session, and "physically" logging into the account just for Homebrew is too much hassle. So we need to explicitly tell macOS to launch something in a true background session.

How it werks

  1. I made a PR (long since merged by now) on the main Homebrew repo to include a bit for rewriting the service's plist file. This adds a key LimitLoadToSessionType with all of these nuts values: Aqua, Background, LoginWindow, StandardIO, System. This was necessary to begin with because without that key, macOS only allows Aqua-based shit to run (which always requires a GUI session). su(do)ing to the Homebrew user doesn't spawn an Aqua session, rather it's Background.
  2. To get a bit more technical: launchctl uses "domains" for launching shit in. These contain the user's UID (e.g. 501), which for Aqua sessions results in gui/501 and for background-type shit it's user/501. The user domain exists when any process belonging to a user is spawned, e.g. a shell through SSH or su(do).
  3. This repo/tap will override/alias the original brew services command and make sure it passes along a domain suitable for Background sessions.

Installation

Multi-user setup

  1. Create a new macOS user account (non-admin) and name it whatever the fuck you want. You may need to log in to a GUI session for that user so macOS can run through the initial user setup bullshit, I dunno if e.g. the command-line build tools work properly without going through it. Also if you ever do need to log in to that user for troubleshooting, you won't have to wait for the setup to finish at that time. =]]
  2. Set up Homebrew per the recommended instructions on their own site, or maybe relocate it with the help of brew bundle.
  3. Make sure the Homebrew instance is available in the other users' PATHs as well.
  4. Create a new file /etc/sudoers.d/homebrew and add the following line for all non-Homebrew accounts (replace muhuser and muhbrew with the actual account names obviously (whoami)):
    muhuser ALL=NOPASSWD: /usr/bin/su -l muhbrew -c *
  5. Instead of simply aliasing the brew command, we'll need a function in the shell's profile. Obviously, do not add this for the Homebrew user itself. Also this was made for bash because I prefer it over zsh, so it may need some adjusting for other shells.

    brew() {
        sudo su -l muhbrew -c "source \$HOME/.bash_profile; brew $*"
    }
    

    Note how I'm doing sudo su instead of just straight sudo. Of course using just su wouldn't pass through sudoers config, and there's a major problem with using sudo. Either it doesn't set up the environment (not the problem I meant), or launchctl detects the wrong login session type (Aqua when it should've been Background) and the service won't even start. Like, sudo -u muhbrew bash -c 'brew etctwtceccetfec' will cause Aqua being wrongly detected. Finally, I'm using su -l to discard the current environment, which again is needed to get it detected as a Background session. That also means I have to manually source muhbrew's shell profile, since you would put your Homebrew-related environment variables in there and those would need to be exported for the brew command. I'm pretty sure sudo simply overrides the "interactivity" of su's shell and causes it to no longer read profiles automatically, but instead goes looking for an environment variable BASH_ENV (or ENV for sh, neither of which usually exist). May as well just source the file directly then. =]

With this setup you can run brew services commands as any of the configured user and the service will be able to start regardless of whether you're in an actual Aqua session, or doing some su(do) magic. The sudoers config actually allows any command to be executed as muhbrew, but your own accounts are likely admins while muhbrew is not, so there's not really any risk.

And a final note: keep in mind that although Homebrew installs a LaunchAgent that would normally auto-start the service when rebooting, it won't actually trigger if you're not logging in to an Aqua session for muhbrew. So you'll always have to manually run a brew services start command, or write a LaunchDaemon that runs the necessary brew commands.

Auto-start LaunchDaemon

Luckily I have such a LaunchDaemon. ;]];];];] It will be run as r00t so it doesn't really matter where you put the script.

  1. You'll need one script, named e.g. b00t.sh (extend the brew_svc array with other services you wanna start):

    #!/bin/bash
    brew_svc=(httpd)
    for svc in "${brew_svc[@]}"; do
        sudo su -l muhbrew -c "source \$HOME/.bashrc; brew services start $svc"
    done
    
  2. And the service definition, e.g. /Library/LaunchDaemons/com.jemoeder.lief.b00t.plist (adjust the file names and the plist's Label and Program properties as needed =]):

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
        <dict>
            <key>Label</key>
            <string>com.jemoeder.lief.b00t.plist</string>
    
            <key>Program</key>
            <string>/Users/muhuser/b00t/b00t.sh</string>
    
            <key>Disabled</key>
            <false/>
    
            <key>KeepAlive</key>
            <false/>
    
            <key>LaunchOnlyOnce</key>
            <true/>
    
            <key>RunAtLoad</key>
            <true/>
    
            <!--
            <key>StandardOutPath</key>
            <string>/tmp/keks.log</string>
            <key>StandardErrorPath</key>
            <string>/tmp/keks.err</string>
            -->
        </dict>
    </plist>
    
  3. Run launchctl load /Library/LaunchDaemons/com.jemoeder.lief.b00t.plist to register that shit. It will also immediately run.

  4. Since it's a Daemon instead of Agent it only runs when the cump00per boots up, and usually before users even have a chance to log in.

The actual background h4x

  1. First, make sure you have the services base command by running brew services (will automatically install em if needed).
  2. If you were already using services: stop everything currently running. This is needed to get the gui domain to fuck off.
  3. Tap that ass: brew tap wazakindjes/homebrew-services-bg https://gitgud.malvager.net/Wazakindjes/homebrew-services-bg
  4. ??¿¿÷?¿¿÷/?¿?/
  5. pr0fit

Now when you run any brew services command it will be overridden by the version from this repo, and do its magic to start shit in a user domain. [=[=[=[==[

Known issues

  • brew commands lists services twice under External commands, deal w/ it (it has to be eggzactly the same to even override it obviously). ;]];;]];];
  • When "physically" logging in to the Homebrew user after a reboot and before explicitly starting the service backgrounded, macOS will run it in the Aqua context and brew services stop will fail/seem to hang. launchctl bootout gui/$UID <service name> can be used to get it unstuck. The service name can be derived from the plist, e.g. ~/Library/LaunchAgents/homebrew.mxcl.httpd.plist becomes homebrew.mxcl.httpd (plist basename without extension lol).