Skip to content

Commit 8c6a468

Browse files
committed
launchd: unload by service target rather than filename
Change 'bootoutFile()' helper to 'bootout()' to unload a launchd service based on its service target identifier (e.g. 'gui/502/com.example.myservice') rather than the path to its plist file. This use of 'bootout' is both less fragile (since it does not require a plist file to exist to succeed) and easier to use when performing a "gentle" removal (i.e., suppressing "service not found" errors). Update 'Create()' and the 'launchd' tests with the updated bootout behavior. Signed-off-by: Victoria Dye <[email protected]>
1 parent 02282c4 commit 8c6a468

File tree

2 files changed

+27
-26
lines changed

2 files changed

+27
-26
lines changed

internal/daemon/launchd.go

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func (p *plist) addKeyValue(key string, value any) {
5353

5454
const domainFormat string = "user/%s"
5555

56+
const LaunchdNoSuchProcessErrorCode int = 3
5657
const LaunchdServiceNotFoundErrorCode int = 113
5758

5859
type launchdConfig struct {
@@ -137,18 +138,20 @@ func (l *launchd) bootstrapFile(domain string, filename string) error {
137138
return nil
138139
}
139140

140-
func (l *launchd) bootoutFile(domain string, filename string) error {
141-
// run 'launchctl bootout' on given domain & file
142-
exitCode, err := l.cmdExec.Run("launchctl", "bootout", domain, filename)
141+
func (l *launchd) bootout(serviceTarget string) (bool, error) {
142+
// run 'launchctl bootout' on given service target
143+
exitCode, err := l.cmdExec.Run("launchctl", "bootout", serviceTarget)
143144
if err != nil {
144-
return err
145+
return false, err
145146
}
146147

147-
if exitCode != 0 {
148-
return fmt.Errorf("'launchctl bootout' exited with status %d", exitCode)
148+
if exitCode == 0 {
149+
return true, nil
150+
} else if exitCode == LaunchdNoSuchProcessErrorCode {
151+
return false, nil
152+
} else {
153+
return false, fmt.Errorf("'launchctl bootout' failed with status %d", exitCode)
149154
}
150-
151-
return nil
152155
}
153156

154157
func (l *launchd) Create(config *DaemonConfig, force bool) error {
@@ -186,33 +189,31 @@ func (l *launchd) Create(config *DaemonConfig, force bool) error {
186189
return err
187190
}
188191

189-
// First, verify whether the file exists
190-
// TODO: only overwrite file if file contents have changed
191192
fileExists, err := l.fileSystem.FileExists(filename)
192193
if err != nil {
193194
return fmt.Errorf("could not determine whether plist '%s' exists: %w", filename, err)
194195
}
195196

196-
if alreadyLoaded && !fileExists {
197-
// Abort on corrupted configuration
198-
return fmt.Errorf("service target '%s' is bootstrapped, but its plist doesn't exist", serviceTarget)
199-
}
200-
201-
if !force && alreadyLoaded {
202-
// Not forcing a refresh of the file, so we do nothing
197+
// If not forcing re-configuration & the service configuration is valid,
198+
// do nothing
199+
if !force && alreadyLoaded && fileExists {
203200
return nil
204201
}
205202

206-
// Otherwise, write & bootstrap the file
203+
// Unload the service so we can reconfigure & reload
207204
if alreadyLoaded {
208-
// Unload the old file, if necessary
209-
l.bootoutFile(domainTarget, filename)
205+
_, err = l.bootout(serviceTarget)
206+
if err != nil {
207+
return fmt.Errorf("could not bootout daemon process '%s': %w", config.Label, err)
208+
}
210209
}
211210

211+
// Rewrite the plist, if needed
212212
if !fileExists || force {
213+
// TODO: only overwrite file if file contents have changed
213214
err = l.fileSystem.WriteFile(filename, newPlist.Bytes())
214215
if err != nil {
215-
return fmt.Errorf("unable to overwrite plist file: %w", err)
216+
return fmt.Errorf("unable to write plist file: %w", err)
216217
}
217218
}
218219

internal/daemon/launchd_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ var launchdCreateBehaviorTests = []struct {
8888
false,
8989
},
9090
{
91-
"Config missing & already bootstrapped throws error",
91+
"Plist missing & already bootstrapped unloads, writes new file, and bootstraps",
9292
&basicDaemonConfig,
9393
Any,
9494
[]Pair[bool, error]{NewPair[bool, error](false, nil)}, // file exists
95-
[]error{}, // write file
95+
[]error{nil}, // write file
9696
[]Pair[int, error]{NewPair[int, error](0, nil)}, // launchctl print (isBootstrapped)
97-
[]Pair[int, error]{}, // launchctl bootstrap
98-
[]Pair[int, error]{}, // launchctl bootout
99-
true,
97+
[]Pair[int, error]{NewPair[int, error](0, nil)}, // launchctl bootstrap
98+
[]Pair[int, error]{NewPair[int, error](0, nil)}, // launchctl bootout
99+
false,
100100
},
101101
}
102102

0 commit comments

Comments
 (0)