Open freakboy3742 opened 1 month ago
fwiw i have implemented this feature as part of my briefcase app. while there are a few ways to do it and theoretically the preferred method these days is via Apple's SMJobBless()
that approach requires the execution of Swift or objective C code, which could require compiling an entirely separate small app in the bundle.
i used the old method, which involves a postinstall
script to execute two steps:
.plist
file (which is already in the app bundle at Contents/Library/LaunchAgents
) to ~/Library/LaunchAgents
launchctl bootstrap
If it's helpful here's the contents of my postinstall
script which will install an app in user space (gui/501
). it should work for a system level daemon as well with some changes.
#!/bin/bash
# Create .plist file in ~/Library/LaunchAgents and bootstrap. Output of this script can be seen in /var/log/install.log
set -e
# Arg list: https://discussions.apple.com/thread/2184714?sortBy=best
# $1 is the full path of the .pkg file
# $2 is the path to the installed app, usually just '/Applications/'
# $3 is the mountpoint of the installer disk, usually just '/'
# $4 is root directory of currently booted system, usually just '/'
APP_BASENAME='IllmaticApp'
BUNDLE_ID='com.illmatic.app'
APP_EXECUTABLE_PATH="$2/$APP_BASENAME.app/Contents/Macos/$APP_BASENAME"
INSTALLED_LAUNCH_AGENT_PATH="$HOME/Library/LaunchAgents/$BUNDLE_ID.plist"
# In the env vars accessed by this script $USER = the_actual_user but $UID = 0 (root).
INSTALLING_USER_UID=$(id -u "$USER")
LAUNCHD_DOMAIN="gui/$INSTALLING_USER_UID"
echo "Post installation process started by user $USER..."
echo "-> Checking for and removing existing service..."
INSTALLED_SERVICE=$(launchctl print $LAUNCHD_DOMAIN | grep $BUNDLE_ID | awk '{print $3}')
if [ ! -z $INSTALLED_SERVICE ]; then
echo " -> Existing service found; running launchctl bootout $DOMAIN/$BUNDLE_ID"
launchctl bootout "$DOMAIN/$BUNDLE_ID"
fi
echo "-> Generating .plist at $INSTALLED_LAUNCH_AGENT_PATH"
"$APP_EXECUTABLE_PATH" --generate-plist
echo "-> Running: chown $USER $INSTALLED_LAUNCH_AGENT_PATH"
chown "$USER" "$INSTALLED_LAUNCH_AGENT_PATH"
echo "-> Running: launchctl bootstrap $LAUNCHD_DOMAIN $INSTALLED_LAUNCH_AGENT_PATH"
launchctl bootstrap "$LAUNCHD_DOMAIN" "$INSTALLED_LAUNCH_AGENT_PATH"
echo "Post installation process finished."
The .plist
file is generated by postinstall
calling to the just installed app ("$APP_EXECUTABLE_PATH" --generate-plist
) which has something like this in app.py
:
def main():
if args.generate_plist:
generate_briefcase_plist()
return
and this in plist_helper.py
:
import plistlib
from os.path import join, expanduser
from .app_metadata_helper import MAIN_APP_NAME, get_app_name, get_bundle_identifier, installed_executable_path
# Directories
LAUNCHAGENTS_DIR = expanduser(f"~/Library/LaunchAgents")
LAUNCHAGENT_PLIST_PATH = join(LAUNCHAGENTS_DIR, get_bundle_identifier() + '.plist')
APPLICATION_SUPPORT_DIR = expanduser(f"~/Library/Application Support/{MAIN_APP_NAME}")
APPLICATION_LOG_DIR = join(APPLICATION_SUPPORT_DIR, 'log')
def generate_briefcase_plist():
"""Run with briefcase run -u -- --generate-plist"""
build_logfile_path = lambda stream: join(APPLICATION_LOG_DIR, f"{get_app_name()}.{stream}.log")
plist_contents = {
"EnvironmentVariables": {
"PATH": f"{installed_executable_path().parent}:/usr/bin:/bin",
},
"KeepAlive": True,
"Label": get_bundle_identifier(),
"ProgramArguments": [str(installed_executable_path())],
"RunAtLoad": True,
"StandardErrorPath": build_logfile_path("stderr"),
"StandardOutPath": build_logfile_path("stdout"),
"WorkingDirectory": str(installed_executable_path().parent),
}
with open(LAUNCHAGENT_PLIST_PATH, "w") as _plist_file:
_plist_file.write(plistlib.dumps(plist_contents).decode())
print(f"Wrote {get_app_name()} .plist config to '{LAUNCHAGENT_PLIST_PATH}'")
And here's the generated .plist
file (launchctl
is very finicky)
<?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>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Applications/IllmaticApp.app/Contents/MacOS:/usr/bin:/bin</string>
</dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.illmatic.app</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/IllmaticApp.app/Contents/MacOS/IllmaticApp</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/uzor/Library/Application Support/IllmaticApp/log/IllmaticApp.stderr.log</string>
<key>StandardOutPath</key>
<string>/Users/uzor/Library/Application Support/IllmaticApp/log/IllmaticApp.stdout.log</string>
<key>WorkingDirectory</key>
<string>/Applications/IllmaticApp.app/Contents/MacOS</string>
</dict>
</plist>
As far as i can tell the main advantage of SMJobBless
is that you can add a login item without copying the .plist
to ~/Library/LaunchAgents
.
fwiw i have implemented this feature as part of my briefcase app. while there are a few ways to do it and theoretically the preferred method these days is via Apple's
SMJobBless()
that approach requires the execution of Swift or objective C code, which could require compiling an entirely separate small app in the bundle.
Thanks for those details. There's obviously a couple of details in that literal plist and config script that would need to be templated/configured, but it seems like that should be reasonably straightforward to include in a bundled macOS pkg (once #1781 lands).
The question I have about this configuration is how it interacts with individual users. The current state of #1781 only supports global installs; AIUI, login items are specific to individual users. This script seems to be hard-coding the installation of the launch item use of UID 501, and the uzor
username. Would introducing this code also require the introduction of user-space app installers? How would a second user on the same machine get this tool installed as a launch item?
Also - I don't think we wouldn't necessarily need to ship an extra executable. You can invoke Objective C APIs with ctypes; and although a bundled Briefcase app isn't a "true" Python interpreter, you can control the Python entry point using the BRIEFCASE_MAIN_MODULE
environment variable. I think it should be possible to write a Python script that uses ctypes
to invoke SMJobBless
, and use the app binary itself to run that script.
login items are specific to individual users.
Sort of... but a system wide startup item is installed the same way. The only differences are that
.plist
file in /Library/LaunchDaemons
instead of ~/Library/LaunchAgents
launchctl bootstrap system
as opposed to launchctl bootstrap gui/501
launchctl
command with sudo
. (As an aside it seems like the standard installer output by pkgbuild
runs as root, which is mildly terrifying but it is what it is). There's also the user/
domain. gui/
domain theoretically waits until login to launch, user/
domain theoretically does not but I remember that not really working as intended (though I could be remembering incorrectly). Some more good details on the extremely poorly documented launchctl
world can be found here.
Also - I don't think we wouldn't necessarily need to ship an extra executable. You can invoke Objective C APIs with ctypes;
That sounds like it should work - in fact I may try to incorporate that in my app, and if so i will report back - but Apple's documentation is extremely lacking about all this stuff.
This script seems to be hard-coding the installation of the launch item use of UID 501, and the uzor username.
You are correct. FWIW the .plist
generation is templated and actually currently done by my briefcase app's python code at build time, but out in the real world that templating would need to happen at either install or initial launch time. postinstall
has access to the $USER
and $UID
variables so it's more annoying than difficult. launchctl
is extremely finicky about everything (and horrifically annoying to debug) and IIRC does not accept things like relative paths or the ~
variable. If you are on a mac take a look at the .plist
files in the various LaunchAgents
and LaunchDaemons
dirs and you will see everything is hard coded with full paths.
Would introducing this code also require the introduction of user-space app installers? How would a second user on the same machine get this tool installed as a launch item?
I think the answer is "yes" though fwiw Apple's entire approach tends to kind of casually assume single-tenancy machines.
EDIT 2024-06-05:
postinstall
has access to the$USER
and$UID
variables
i found that bizarrely postinstall
runs with $USER
as the installing user but $UID
as 0 (root
account) and in general does indeed run with root privileges other than this weird qurik
i just noticed this piece of the official apple docs:
The Service Management framework only supports the kSMDomainSystemLaunchd domain.
so apparently SMJobBless()
does not even support installing login items for individual users and only supports running your app from startup as root
, which is a bit scary.
and only supports running your app from startup as root
Apparently you can use the UserName
key in your .plist
file to run from boot as a specific (non-root) user.
i changed my situation so that the .plist
is generated by postinstall
calling out to the app it just installed so there's no templating required, just a bit of additional code in the briefcase app's python. edited my comment above with the updated code.
added a couple of lines to the comment above to check for and bootout
the service if it's already installed because otherwise postinstall
will fail which is a small glitch i just ran into but otherwise this setup has been serving me very well.
What is the problem or limitation you are having?
Some apps (in particular server and toolbar style apps) need to add themselves to the startup items list. Briefcase should be able to produce apps that can install themselves in the startup item list
Describe the solution you'd like
Apps should have a configuration option so that when an app is installed, it is added to the startup items for the platform - e.g.,
On macOS, this will require the use of a .pkg installer (see #1781) On Windows, it will require an MSI installer Linux packages will result in the app being added to systemd/initd.
It might be desirable to add this in a way that the user can opt-in, rather than it being a forced requirement on installation.
Describe alternatives you've considered
None.
Additional context
No response