Documentation
¶
Overview ¶
Package servicemanagement registers a macOS application, agent or daemon to start at login, through SMAppService — from pure Go, with CGO_ENABLED=0. It reaches the ServiceManagement framework via github.com/go-macos/objc, which reaches it through purego: no cgo, no Objective-C source file, no launchctl.
svc := servicemanagement.MainApp()
if err := svc.Register(); err != nil {
return err
}
st, err := svc.Status()
if err != nil {
return err
}
if st == servicemanagement.RequiresApproval {
fmt.Println(st.Advice()) // tell the person; do not fail
}
Why this rather than a plist ¶
The legacy way to start something at login is to write a plist into ~/Library/LaunchAgents and hope launchd reads it — which is what github.com/go-macos/launchagent does, and still does. Since macOS 13 that is no longer the supported path for an application: SMAppService is. It keeps the registration with the bundle that owns it, it appears in System Settings under the application's own name instead of as an anonymous label, and — the part a plist can never have — the person can switch it off there, and the program can find out that they did.
requiresApproval is a NORMAL outcome, not a failure ¶
Service.Register can return nil and leave the service in RequiresApproval. That is macOS saying: it is registered, it will not run yet, a person has to allow it in System Settings > General > Login Items & Extensions. It happens routinely — most visibly when the person has switched this very item off before, in which case the switch stays off until they turn it back on, whatever the program does.
A program that treats that as success runs nothing and says nothing. A program that treats it as an error tells the person something is broken when nothing is. Both are wrong, so Status is returned as a value rather than flattened into an error, and Status.Advice carries the sentence to show.
It requires a bundle, and that is the first thing that goes wrong ¶
SMAppService is a bundle API: it identifies the caller by its bundle identifier, and a bare executable has none. Outside a bundle the class is still there, the factory methods still hand back objects, and -status still answers — with NotFound, which is indistinguishable from a service that is genuinely absent. The failure only surfaces at -register:, as "Codesigning failure loading plist", which names neither the cause nor the fix.
So every operation here checks first, with -[NSBundle bundleIdentifier], and reports ErrNotBundled: distinct from every other error, and answerable. Bundled exposes the same question directly. github.com/go-macos/appbundle builds the .app to answer it with, in pure Go, in the same build:
_, err := appbundle.Build(appbundle.Spec{
Dir: "dist", Name: "godl", Identifier: "io.github.go-downloader.godl",
Version: "0.1.0", Executable: "build/godl", Accessory: true,
})
Where an agent's plist lives ¶
Agent and Daemon take a plist FILE NAME, not a path and not a label. The file must be shipped inside the bundle — Contents/Library/LaunchAgents for an agent, Contents/Library/LaunchDaemons for a daemon — and the name is the leaf of it, extension included:
servicemanagement.Agent("io.github.go-downloader.godl.plist")
A name that does not resolve to a file in there is not rejected: the object exists, its status is NotFound, and -register: fails with "Unable to read plist". That is the one to look for when a registration that "worked" during development stops working once the plist stopped being copied into the bundle.
Threads ¶
Nothing here is AppKit, so nothing here is main-thread-only: SMAppService is backed by XPC and every call in this package is safe from any goroutine. Each operation runs inside its own autorelease pool on a pinned OS thread (see objc.AutoreleasePool), because the factory methods hand back autoreleased objects and a goroutine that migrates between creating one and messaging it drains the pool on a thread that never owned it.
Portability ¶
Every exported symbol exists on every platform, so a consumer cross-compiles without a build tag of its own; off darwin each operation reports ErrUnsupported and Bundled reports false. The portable half — service identity, name validation, status typing and advice — behaves identically everywhere and is tested to the last branch on runners with no macOS in sight.
Index ¶
Constants ¶
const Framework = "/System/Library/Frameworks/ServiceManagement.framework/ServiceManagement"
Framework is the ServiceManagement framework's path, opened by the darwin half on first use. It is exported because a caller that loads frameworks of its own has one list to keep, not two.
Variables ¶
var ( // ErrUnsupported is returned by every operation on non-darwin platforms. // SMAppService is macOS-only; the symbols exist everywhere so consumers // cross-compile. ErrUnsupported = errors.New("servicemanagement: unsupported on this platform (darwin only)") // ErrNotBundled reports that this process is not inside an application // bundle, so it has no bundle identifier for SMAppService to register. // // It is separate from every other error here because it is the one that // actually happens, and because macOS hides it: outside a bundle the // factory methods still return objects and -status still answers NotFound, // so the only native symptom is a register failure blaming code signing. // Build the bundle with github.com/go-macos/appbundle. ErrNotBundled = errors.New("servicemanagement: this process is not in an application bundle (SMAppService identifies its caller by bundle identifier)") // ErrTooOld reports that SMAppService does not exist in this process: // the class is absent, which means macOS older than 13 (Ventura). // A caller that must run on both keeps github.com/go-macos/launchagent as // its fallback. ErrTooOld = errors.New("servicemanagement: SMAppService is not available (macOS 13 or later is required)") // ErrNoService reports that the ServiceManagement factory yielded nil for // this service. The service cannot be spoken about at all, which is not // the same as it being absent — that is reported as the [NotFound] status. ErrNoService = errors.New("servicemanagement: SMAppService returned no object for this service") )
Sentinel errors. They are stable and may be compared with errors.Is.
Functions ¶
func Bundled ¶
Bundled reports this process's bundle identifier, and whether it has one.
It is the question SMAppService really asks, and the honest answer to "will any of this work" — a path that merely looks like a .app is not enough, since a bundle with no CFBundleIdentifier fails the same way a bare executable does. Off darwin it reports false, which is the truth there too.
Types ¶
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is one registrable service: the application itself, one of the agents or daemons its bundle ships, or a login item belonging to it.
The zero Service is MainApp. It is a value, so it costs nothing to keep and nothing to release; the Objective-C object is made and dropped inside each operation.
func Agent ¶
Agent is a per-user LaunchAgent the bundle ships, named by its plist's FILE NAME — "io.example.thing.plist", not a path and not a label. The file must be at Contents/Library/LaunchAgents inside the bundle; +[SMAppService agentServiceWithPlistName:] resolves it there and nowhere else.
func Daemon ¶
Daemon is a system-wide LaunchDaemon the bundle ships, named by its plist's file name at Contents/Library/LaunchDaemons.
Registering one asks for an administrator's password: a daemon runs as root, before anybody logs in. That prompt is macOS's, and it arrives whether or not the calling program expected it.
func LoginItem ¶
LoginItem is a helper application inside this bundle, named by ITS bundle identifier: +[SMAppService loginItemServiceWithIdentifier:]. It is the modern replacement for SMLoginItemSetEnabled.
func MainApp ¶
func MainApp() Service
MainApp is the application this process is in: +[SMAppService mainAppService]. Registering it is what "open at login" means for an application.
func (Service) Name ¶
Name is the plist file name or login-item identifier the service was made with, and "" for MainApp.
func (Service) Register ¶
Register asks macOS to start this service at login.
A nil error does NOT mean it will run. macOS may register the service and leave it switched off, waiting for the person to allow it; read Status and show Status.Advice when it comes back RequiresApproval. That is not a failure and must not be reported as one.
Registering an already-registered service is not an error: it is how a program re-asserts a registration after an update.
func (Service) Status ¶
Status reports what macOS currently thinks of the service.
It never invents a status from an error: a service that cannot be asked about yields an error and the zero Status, not NotFound. Outside a bundle macOS itself answers NotFound for everything, and passing that on would be this package telling a caller its plist is missing when the real answer is that nobody asked.
func (Service) Unregister ¶
Unregister takes the registration away.
macOS reports unregistering something that was never registered as an error (code 22, "Invalid argument") rather than as the no-op it looks like. That is left as macOS reports it rather than swallowed here: a caller for whom "gone is gone" is the wanted outcome reads Status first, and a caller that swallowed it blindly would also swallow a real refusal.
type Status ¶
type Status int
Status is SMAppServiceStatus: what macOS currently thinks of a service.
The numeric values are Apple's, not ours: they cross the ObjC boundary as an NSInteger and are pinned here so a mis-ordered constant cannot turn "enabled" into "needs approval" silently.
const ( // NotRegistered means the service is known and has never been registered, // or has been unregistered. Nothing will run. NotRegistered Status = 0 // Enabled means the service is registered and allowed to run. This is the // one outcome that needs nothing said to anybody. Enabled Status = 1 // RequiresApproval means the service is registered and will NOT run until // a person allows it in System Settings. It is a normal outcome of a // successful [Service.Register] — see [Status.Advice]. RequiresApproval Status = 2 // NotFound means macOS has no such service: no plist of that name in the // bundle, or a bundle it does not recognise. It is also what a process // outside a bundle sees for everything, which is why the operations here // refuse to reach this far without one. NotFound Status = 3 )
func (Status) Advice ¶
Advice is the sentence to show a person, or "" when there is nothing they need to do.
It exists so that RequiresApproval cannot be handled by silence. A program that registered successfully and starts nothing has to say why, and the only place the person can act is a Settings pane they will not find by guessing.
func (Status) Registered ¶
Registered reports whether macOS holds a registration for the service — Enabled or RequiresApproval. It is the question "did my Register take", which is not the same as "will it run": a service awaiting approval is registered and idle.
type SystemError ¶
type SystemError struct {
// Op is "register" or "unregister".
Op string
// Service is the service the operation was for.
Service string
// Domain is the NSError domain, normally "SMAppServiceErrorDomain".
Domain string
// Code is the NSError code.
Code int
// Message is -localizedDescription, verbatim.
Message string
}
SystemError is an NSError from SMAppServiceErrorDomain, with the operation that produced it.
The message is macOS's own -localizedDescription, carried through unaltered rather than translated into something friendlier: it is the only text that says which of the several ways a registration can fail actually happened, and a friendlier summary would lose exactly that.
Three that come up, from this package's own runs:
- code 3, "Codesigning failure loading plist" — the caller is not in a bundle. This package reports ErrNotBundled before reaching that.
- code 108, "Unable to read plist: NAME" — there is a bundle, but no plist of that name inside it.
- code 22, "Invalid argument" — unregistering something that was never registered. Read Service.Status first if that is not an error to you.
func (*SystemError) Error ¶
func (e *SystemError) Error() string
Error implements the error interface.