Skip to content

Commit 90c8ebd

Browse files
committed
implement stop program functionality
1 parent 71e7ec7 commit 90c8ebd

8 files changed

Lines changed: 180 additions & 40 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,16 @@ meeseeks logs web-server
221221
Stop running programs.
222222

223223
```bash
224-
meeseeks stop web-server # Stop specific program
225-
meeseeks stop # Stop all programs and daemon
224+
meeseeks stop web-server # Stop specific program
225+
meeseeks stop web-server -timeout 10s # Stop specifc program with custom timeout
226+
```
227+
228+
### `meeseeks exit`
229+
230+
Stop and programs and meeseks process. Useful to stop meeseeks when running in detached mode
231+
232+
```bash
233+
meeseeks exit
226234
```
227235

228236
### `meeseeks version`

cmd/meeseeks/stop.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"flag"
56
"fmt"
67
"os"
@@ -10,22 +11,30 @@ import (
1011

1112
func stopCommand(args []string) error {
1213
fs := flag.NewFlagSet("stop", flag.ExitOnError)
14+
timeout := fs.String(
15+
"timeout",
16+
"5s",
17+
"Timeout to wait for program to exit. If exceeded and error is trigerred and the program is force kill",
18+
)
1319
fs.Usage = func() {
14-
fmt.Fprintf(os.Stderr, "Usage: meeseeks stop [program_name]\n\n")
20+
fmt.Fprintf(os.Stderr, "Usage: meeseeks stop [options] [program_name]\n\n")
1521
fmt.Fprintf(os.Stderr, "Stop running programs\n")
22+
fs.PrintDefaults()
1623
}
1724

1825
if err := fs.Parse(args); err != nil {
1926
return err
2027
}
2128

22-
programName := ""
23-
if fs.NArg() > 0 {
24-
programName = fs.Arg(0)
29+
if fs.NArg() == 0 {
30+
fs.Usage()
31+
return errors.New("program name required")
2532
}
2633

34+
programName := fs.Arg(0)
35+
2736
client := server.NewClient(getSocketPath())
28-
resp, err := client.Stop(programName)
37+
resp, err := client.Stop(programName, *timeout)
2938
if err != nil {
3039
return err
3140
}
@@ -34,6 +43,6 @@ func stopCommand(args []string) error {
3443
return fmt.Errorf("%s", resp.Error)
3544
}
3645

37-
fmt.Fprintln(os.Stdout, "Stop command executed")
46+
fmt.Fprintln(os.Stdout, resp.Data)
3847
return nil
3948
}

cmd/meeseeks/stop_test.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,17 @@ func TestStopCommand_Validation(t *testing.T) {
99

1010
tests := []commandTestCase{
1111
{
12-
name: "stop with no daemon running",
12+
name: "stop without program",
1313
args: []string{"stop"},
1414
expectedExit: 1,
15-
shouldContain: "meeseeks server not running",
16-
},
17-
{
18-
name: "stop specific program with no daemon",
19-
args: []string{"stop", "test-program"},
20-
expectedExit: 1,
21-
shouldContain: "meeseeks server not running",
15+
shouldContain: "program name required",
2216
},
2317
}
2418

2519
runCommandTests(t, tests)
2620
}
2721

28-
func TestStopCommand(t *testing.T) {
22+
func TestStopCommand_MissingProgramName(t *testing.T) {
2923
configContent := `programs:
3024
- name: "test-stop-program1"
3125
command: "sleep"
@@ -39,22 +33,28 @@ func TestStopCommand(t *testing.T) {
3933

4034
tests := []commandTestCase{
4135
{
42-
name: "stop specific program - not implemented",
43-
args: []string{"stop", "test-stop-program1"},
36+
name: "stop with invalid timeout",
37+
args: []string{"stop", "-timeout", "invalid", "test-stop-program1"},
4438
expectedExit: 1,
45-
shouldContain: "stop command not yet implemented",
39+
shouldContain: "invalid duration",
4640
},
4741
{
48-
name: "stop non-existing program - not implemented",
42+
name: "stop non-existing program",
4943
args: []string{"stop", "fake-program"},
5044
expectedExit: 1,
51-
shouldContain: "stop command not yet implemented",
45+
shouldContain: "program fake-program not present",
5246
},
5347
{
54-
name: "stop all programs - not implemented",
55-
args: []string{"stop"},
56-
expectedExit: 1,
57-
shouldContain: "stop command not yet implemented",
48+
name: "stop existing program",
49+
args: []string{"stop", "test-stop-program1"},
50+
expectedExit: 0,
51+
shouldContain: "test-stop-program1 stopped",
52+
},
53+
{
54+
name: "stop with custom timeout",
55+
args: []string{"stop", "-timeout", "10s", "test-stop-program2"},
56+
expectedExit: 0,
57+
shouldContain: "test-stop-program2 stopped",
5858
},
5959
}
6060

@@ -63,7 +63,8 @@ func TestStopCommand(t *testing.T) {
6363

6464
func TestStopCommand_Help(t *testing.T) {
6565
testCommandHelp(t, "stop", []string{
66-
"Usage: meeseeks stop [program_name]",
66+
"Usage: meeseeks stop [options] [program_name]",
6767
"Stop running programs",
68+
"-timeout",
6869
})
6970
}

pkg/meeseeks/meeseek.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
type Meeseek interface {
1515
AddProgram(program.Program) error
1616
Start(ctx context.Context)
17+
Stop(programName string, timeout time.Duration) error
1718
Wait(ctx context.Context) error
1819
Statistic(program string) (program.Statistics, error)
1920
Statistics() []program.Statistics
@@ -102,6 +103,14 @@ func (m *meeseek) Wait(ctx context.Context) error {
102103
}
103104
}
104105

106+
func (m *meeseek) Stop(programName string, timeout time.Duration) error {
107+
if _, ok := m.programs[programName]; !ok {
108+
return fmt.Errorf("program %s not present", programName)
109+
}
110+
111+
return m.programs[programName].Shutdown(timeout)
112+
}
113+
105114
func (m *meeseek) Shutdown(timeout time.Duration) error {
106115
errs := []error{}
107116

pkg/meeseeks/meeseeks_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,101 @@ func BenchmarkMeeseek_Statistics(b *testing.B) {
432432
}
433433
}
434434

435+
func TestMeeseek_Stop(t *testing.T) {
436+
tests := []struct {
437+
name string
438+
programs []program.Program
439+
stopProgram string
440+
timeout time.Duration
441+
wantErr bool
442+
errMsg string
443+
}{
444+
{
445+
name: "stop existing program",
446+
programs: []program.Program{
447+
program.New("test-stop-1", "sleep", program.Args("10")),
448+
program.New("test-stop-2", "sleep", program.Args("10")),
449+
},
450+
stopProgram: "test-stop-1",
451+
timeout: 5 * time.Second,
452+
},
453+
{
454+
name: "stop non-existing program",
455+
programs: []program.Program{
456+
program.New("test-stop-1", "sleep", program.Args("10")),
457+
},
458+
stopProgram: "non-existent",
459+
timeout: 5 * time.Second,
460+
wantErr: true,
461+
errMsg: "program non-existent not present",
462+
},
463+
{
464+
name: "stop with short timeout",
465+
programs: []program.Program{
466+
program.New("test-stop-3", "sleep", program.Args("10")),
467+
},
468+
stopProgram: "test-stop-3",
469+
timeout: 1 * time.Millisecond,
470+
},
471+
}
472+
473+
for _, tt := range tests {
474+
t.Run(tt.name, func(t *testing.T) {
475+
m := New()
476+
477+
for _, prog := range tt.programs {
478+
err := m.AddProgram(prog)
479+
if err != nil {
480+
t.Fatalf("Failed to add program: %v", err)
481+
}
482+
}
483+
484+
// Start programs
485+
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
486+
defer cancel()
487+
m.Start(ctx)
488+
489+
// Give programs time to start
490+
time.Sleep(100 * time.Millisecond)
491+
492+
// Test Stop method
493+
err := m.Stop(tt.stopProgram, tt.timeout)
494+
495+
if tt.wantErr {
496+
if err == nil {
497+
t.Errorf("Stop() expected error but got none")
498+
return
499+
}
500+
if !strings.Contains(err.Error(), tt.errMsg) {
501+
t.Errorf("Stop() error = %q, want error containing %q", err.Error(), tt.errMsg)
502+
}
503+
return
504+
}
505+
506+
if err != nil {
507+
t.Errorf("Stop() unexpected error = %v", err)
508+
}
509+
510+
// Verify the program is stopped by checking its status
511+
if !tt.wantErr {
512+
stat, err := m.Statistic(tt.stopProgram)
513+
if err != nil {
514+
t.Errorf("Failed to get statistic after stop: %v", err)
515+
return
516+
}
517+
// The program should have terminated
518+
if stat.Running > 0 {
519+
t.Errorf(
520+
"Program %s should be stopped but %d instances are still running",
521+
tt.stopProgram,
522+
stat.Running,
523+
)
524+
}
525+
}
526+
})
527+
}
528+
}
529+
435530
func BenchmarkMeeseek_Statistic(b *testing.B) {
436531
m := New()
437532

pkg/server/client.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ func (c *Client) Logs(programName string) (*Response, error) {
9393
return c.sendRequest("/logs", params)
9494
}
9595

96-
func (c *Client) Stop(programName string) (*Response, error) {
97-
params := make(map[string]string)
98-
if programName != "" {
99-
params["program"] = programName
96+
func (c *Client) Stop(programName, timeout string) (*Response, error) {
97+
params := map[string]string{
98+
"program": programName,
99+
"timeout": timeout,
100100
}
101+
101102
return c.sendRequest("/stop", params)
102103
}

pkg/server/server.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,27 @@ func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
167167
handleResponse(w, resp)
168168
}
169169

170-
func (s *Server) handleStop(w http.ResponseWriter, _ *http.Request) {
170+
func (s *Server) handleStop(w http.ResponseWriter, r *http.Request) {
171171
w.Header().Set("Content-Type", "application/json")
172-
resp := Response{Success: false, Error: "stop command not yet implemented"}
172+
programName := r.URL.Query().Get("program")
173+
timeoutString := r.URL.Query().Get("timeout")
174+
if programName == "" {
175+
resp := Response{Success: false, Error: "program name required"}
176+
handleResponse(w, resp)
177+
}
178+
179+
duration, err := time.ParseDuration(timeoutString)
180+
if err != nil {
181+
resp := Response{Success: false, Error: fmt.Sprintf("error parsing timeout %s. %s", timeoutString, err.Error())}
182+
handleResponse(w, resp)
183+
}
184+
185+
err = s.meeseeks.Stop(programName, duration)
186+
if err != nil {
187+
resp := Response{Success: false, Error: err.Error()}
188+
handleResponse(w, resp)
189+
}
190+
resp := Response{Success: true, Data: fmt.Sprintf("%s stopped", programName)}
173191
handleResponse(w, resp)
174192
}
175193

pkg/server/server_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,16 @@ func TestServer_HTTPHandlers(t *testing.T) {
244244
{
245245
name: "stop command",
246246
testFn: func(t *testing.T, client *Client) {
247-
resp, err := client.Stop("")
247+
resp, err := client.Stop("", "5s")
248248
if err != nil {
249249
t.Errorf("Stop() unexpected error = %v", err)
250250
return
251251
}
252252
if resp.Success {
253-
t.Errorf("Expected success=false for unimplemented stop, got %v", resp.Success)
253+
t.Errorf("Expected success=false for empty program name, got %v", resp.Success)
254254
}
255255
if resp.Error == "" {
256-
t.Errorf("Expected error message for unimplemented command")
256+
t.Errorf("Expected error message for empty program name")
257257
}
258258
},
259259
},
@@ -334,14 +334,13 @@ func TestClient_SendRequest(t *testing.T) {
334334
{
335335
name: "stop request",
336336
testFn: func(t *testing.T, client *Client) {
337-
resp, err := client.Stop("")
337+
resp, err := client.Stop("test-program", "5s")
338338
if err != nil {
339339
t.Errorf("Stop() unexpected error = %v", err)
340340
return
341341
}
342-
// Stop is not implemented yet, so expect failure
343-
if resp.Success {
344-
t.Errorf("Stop() expected success=false (unimplemented), got %v", resp.Success)
342+
if !resp.Success {
343+
t.Errorf("Stop() expected success=true, got %v", resp.Success)
345344
}
346345
},
347346
},

0 commit comments

Comments
 (0)