Skip to content

Commit 9d267bc

Browse files
committed
launchd: implement 'Start()' function
Implement and test the 'Start()' function for the 'launchd' daemon process manager. The 'Start()' function assumes that the specified service has already been boostrapped (with 'Create()'), and simply runs 'launchctl kickstart' to start the service. If the service is already running, 'Start()' is effectively a no-op (as 'launchctl kickstart' does not restart the already-running process). Signed-off-by: Victoria Dye <[email protected]>
1 parent a4d43a6 commit 9d267bc

File tree

2 files changed

+59
-1
lines changed

2 files changed

+59
-1
lines changed

internal/daemon/launchd.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,23 @@ func (l *launchd) Create(config *DaemonConfig, force bool) error {
164164
}
165165

166166
func (l *launchd) Start(label string) error {
167-
return fmt.Errorf("not implemented")
167+
user, err := l.user.CurrentUser()
168+
if err != nil {
169+
return fmt.Errorf("could not get current user for launchd service: %w", err)
170+
}
171+
172+
domainTarget := fmt.Sprintf(domainFormat, user.Uid)
173+
serviceTarget := fmt.Sprintf("%s/%s", domainTarget, label)
174+
exitCode, err := l.cmdExec.Run("launchctl", "kickstart", serviceTarget)
175+
if err != nil {
176+
return err
177+
}
178+
179+
if exitCode != 0 {
180+
return fmt.Errorf("'launchctl kickstart' exited with status %d", exitCode)
181+
}
182+
183+
return nil
168184
}
169185

170186
func (l *launchd) Stop(label string) error {

internal/daemon/launchd_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,45 @@ func TestLaunchd_Create(t *testing.T) {
216216
assert.Contains(t, fileContents, fmt.Sprintf("<key>Program</key><string>%s</string>", basicDaemonConfig.Program))
217217
})
218218
}
219+
220+
func TestLaunchd_Start(t *testing.T) {
221+
// Set up mocks
222+
testUser := &user.User{
223+
Uid: "123",
224+
Username: "testuser",
225+
}
226+
testUserProvider := &mockUserProvider{}
227+
testUserProvider.On("CurrentUser").Return(testUser, nil)
228+
229+
testCommandExecutor := &mockCommandExecutor{}
230+
231+
launchd := daemon.NewLaunchdProvider(testUserProvider, testCommandExecutor, nil)
232+
233+
// Test #1: launchctl succeeds
234+
t.Run("Calls correct launchctl command", func(t *testing.T) {
235+
testCommandExecutor.On("Run",
236+
"launchctl",
237+
[]string{"kickstart", fmt.Sprintf("gui/123/%s", basicDaemonConfig.Label)},
238+
).Return(0, nil).Once()
239+
240+
err := launchd.Start(basicDaemonConfig.Label)
241+
assert.Nil(t, err)
242+
mock.AssertExpectationsForObjects(t, testCommandExecutor)
243+
})
244+
245+
// Reset the mock structure between tests
246+
testCommandExecutor.Mock = mock.Mock{}
247+
248+
// Test #2: launchctl fails
249+
t.Run("Returns error when launchctl fails", func(t *testing.T) {
250+
testCommandExecutor.On("Run",
251+
mock.AnythingOfType("string"),
252+
mock.AnythingOfType("[]string"),
253+
).Return(1, nil).Once()
254+
255+
err := launchd.Start(basicDaemonConfig.Label)
256+
assert.NotNil(t, err)
257+
mock.AssertExpectationsForObjects(t, testCommandExecutor)
258+
})
259+
}
260+

0 commit comments

Comments
 (0)