Skip to content

Commit 9eac445

Browse files
committed
launchd: implement 'Stop()' function
Implement and test the 'Stop()' function for the 'launchd' daemon manager. 'Stop()' shuts down the specified service (if it is running) with 'launchctl kill SIGINT'. If the command succeeds *or* fails indicating the service is not bootstrapped, exit without an error; otherwise, report the appropriate error. Signed-off-by: Victoria Dye <[email protected]>
1 parent 9d267bc commit 9eac445

File tree

2 files changed

+73
-1
lines changed

2 files changed

+73
-1
lines changed

internal/daemon/launchd.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,22 @@ func (l *launchd) Start(label string) error {
184184
}
185185

186186
func (l *launchd) Stop(label string) error {
187-
return fmt.Errorf("not implemented")
187+
user, err := l.user.CurrentUser()
188+
if err != nil {
189+
return fmt.Errorf("could not get current user for launchd service: %w", err)
190+
}
191+
192+
domainTarget := fmt.Sprintf(domainFormat, user.Uid)
193+
serviceTarget := fmt.Sprintf("%s/%s", domainTarget, label)
194+
exitCode, err := l.cmdExec.Run("launchctl", "kill", "SIGINT", serviceTarget)
195+
if err != nil {
196+
return err
197+
}
198+
199+
// Don't throw an error if the service hasn't been bootstrapped
200+
if exitCode != 0 && exitCode != LaunchdServiceNotFoundErrorCode {
201+
return fmt.Errorf("'launchctl kill' exited with status %d", exitCode)
202+
}
203+
204+
return nil
188205
}

internal/daemon/launchd_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,58 @@ func TestLaunchd_Start(t *testing.T) {
258258
})
259259
}
260260

261+
func TestLaunchd_Stop(t *testing.T) {
262+
// Set up mocks
263+
testUser := &user.User{
264+
Uid: "123",
265+
Username: "testuser",
266+
}
267+
testUserProvider := &mockUserProvider{}
268+
testUserProvider.On("CurrentUser").Return(testUser, nil)
269+
270+
testCommandExecutor := &mockCommandExecutor{}
271+
272+
launchd := daemon.NewLaunchdProvider(testUserProvider, testCommandExecutor, nil)
273+
274+
// Test #1: launchctl succeeds
275+
t.Run("Calls correct launchctl command", func(t *testing.T) {
276+
testCommandExecutor.On("Run",
277+
"launchctl",
278+
[]string{"kill", "SIGINT", fmt.Sprintf("gui/123/%s", basicDaemonConfig.Label)},
279+
).Return(0, nil).Once()
280+
281+
err := launchd.Stop(basicDaemonConfig.Label)
282+
assert.Nil(t, err)
283+
mock.AssertExpectationsForObjects(t, testCommandExecutor)
284+
})
285+
286+
// Reset the mock structure between tests
287+
testCommandExecutor.Mock = mock.Mock{}
288+
289+
// Test #2: launchctl fails with uncaught error
290+
t.Run("Returns error when launchctl fails", func(t *testing.T) {
291+
testCommandExecutor.On("Run",
292+
mock.AnythingOfType("string"),
293+
mock.AnythingOfType("[]string"),
294+
).Return(1, nil).Once()
295+
296+
err := launchd.Stop(basicDaemonConfig.Label)
297+
assert.NotNil(t, err)
298+
mock.AssertExpectationsForObjects(t, testCommandExecutor)
299+
})
300+
301+
// Reset the mock structure between tests
302+
testCommandExecutor.Mock = mock.Mock{}
303+
304+
// Test #3: launchctl fails with uncaught error
305+
t.Run("Exits without error if service not found", func(t *testing.T) {
306+
testCommandExecutor.On("Run",
307+
mock.AnythingOfType("string"),
308+
mock.AnythingOfType("[]string"),
309+
).Return(daemon.LaunchdServiceNotFoundErrorCode, nil).Once()
310+
311+
err := launchd.Stop(basicDaemonConfig.Label)
312+
assert.Nil(t, err)
313+
mock.AssertExpectationsForObjects(t, testCommandExecutor)
314+
})
315+
}

0 commit comments

Comments
 (0)