diff --git a/cmd/cli/commands/logs/logs.go b/cmd/cli/commands/logs/logs.go index d37571a..51b29f0 100644 --- a/cmd/cli/commands/logs/logs.go +++ b/cmd/cli/commands/logs/logs.go @@ -83,19 +83,5 @@ func showLogs( return fmt.Errorf("invalid sidecar run method: %s", cfg.RunMethod) } - // Check if service is running. - running, err := runner.IsRunning() - if err != nil { - log.Errorf("could not check sidecar status: %v", err) - - return err - } - - if !running { - fmt.Printf("%sContributoor is not running%s\n", tui.TerminalColorYellow, tui.TerminalColorReset) - - return nil - } - return runner.Logs(c.Int("tail"), c.Bool("follow")) } diff --git a/cmd/cli/commands/logs/logs_test.go b/cmd/cli/commands/logs/logs_test.go index 16f6513..9d3c8cc 100644 --- a/cmd/cli/commands/logs/logs_test.go +++ b/cmd/cli/commands/logs/logs_test.go @@ -36,22 +36,9 @@ func TestShowLogs(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_DOCKER, }).Times(1) - d.EXPECT().IsRunning().Return(true, nil) d.EXPECT().Logs(100, false).Return(nil) }, }, - { - name: "docker - service not running", - runMethod: config.RunMethod_RUN_METHOD_DOCKER, - tailLines: 100, - follow: false, - setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) { - cfg.EXPECT().Get().Return(&config.Config{ - RunMethod: config.RunMethod_RUN_METHOD_DOCKER, - }).Times(1) - d.EXPECT().IsRunning().Return(false, nil) - }, - }, { name: "docker - logs fail", runMethod: config.RunMethod_RUN_METHOD_DOCKER, @@ -61,7 +48,6 @@ func TestShowLogs(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_DOCKER, }).Times(1) - d.EXPECT().IsRunning().Return(true, nil) d.EXPECT().Logs(100, false).Return(errors.New("logs failed")) }, expectedError: "logs failed", @@ -75,7 +61,6 @@ func TestShowLogs(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_BINARY, }).Times(1) - b.EXPECT().IsRunning().Return(true, nil) b.EXPECT().Logs(50, true).Return(nil) }, }, @@ -88,7 +73,6 @@ func TestShowLogs(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_SYSTEMD, }).Times(1) - s.EXPECT().IsRunning().Return(true, nil) s.EXPECT().Logs(200, false).Return(nil) }, }, diff --git a/cmd/cli/commands/status/status.go b/cmd/cli/commands/status/status.go index 8015a73..3edc719 100644 --- a/cmd/cli/commands/status/status.go +++ b/cmd/cli/commands/status/status.go @@ -10,6 +10,8 @@ import ( "github.com/ethpandaops/contributoor/pkg/config/v1" "github.com/sirupsen/logrus" "github.com/urfave/cli" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) func RegisterCommands(app *cli.App, opts *options.CommandOpts) { @@ -91,6 +93,12 @@ func showStatus( return fmt.Errorf("failed to check status: %w", err) } + // Get the underlying status from the sidecar. + status, err := runner.Status() + if err != nil { + return fmt.Errorf("failed to get status: %w", err) + } + // Print status information. fmt.Printf("%sContributoor Status%s\n", tui.TerminalColorLightBlue, tui.TerminalColorReset) fmt.Printf("%-20s: %s\n", "Version", cfg.Version) @@ -103,13 +111,12 @@ func showStatus( fmt.Printf("%-20s: %s\n", "Output Server", cfg.OutputServer.Address) } - // Print running status with color + // Print running status with color. statusColor := tui.TerminalColorRed - statusText := "Stopped" + statusText := cases.Title(language.English).String(status) if running { statusColor = tui.TerminalColorGreen - statusText = "Running" } fmt.Printf("%-20s: %s%s%s\n", "Status", statusColor, statusText, tui.TerminalColorReset) diff --git a/cmd/cli/commands/status/status_test.go b/cmd/cli/commands/status/status_test.go index 71cbbbb..07f5878 100644 --- a/cmd/cli/commands/status/status_test.go +++ b/cmd/cli/commands/status/status_test.go @@ -34,6 +34,7 @@ func TestShowStatus(t *testing.T) { // Create mock docker sidecar that's running mockDocker := mock.NewMockDockerSidecar(ctrl) mockDocker.EXPECT().IsRunning().Return(true, nil) + mockDocker.EXPECT().Status().Return("running", nil) // Create mock binary sidecar (shouldn't be used) mockBinary := mock.NewMockBinarySidecar(ctrl) @@ -78,6 +79,7 @@ func TestShowStatus(t *testing.T) { // Create mock binary sidecar that's stopped mockBinary := mock.NewMockBinarySidecar(ctrl) mockBinary.EXPECT().IsRunning().Return(false, nil) + mockBinary.EXPECT().Status().Return("stopped", nil) // Create mock systemd sidecar (shouldn't be used) mockSystemd := mock.NewMockSystemdSidecar(ctrl) @@ -99,6 +101,43 @@ func TestShowStatus(t *testing.T) { assert.NoError(t, err) }) + t.Run("shows status for systemd service", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(&config.Config{ + Version: "1.0.0", + RunMethod: config.RunMethod_RUN_METHOD_SYSTEMD, + NetworkName: config.NetworkName_NETWORK_NAME_MAINNET, + BeaconNodeAddress: "http://localhost:5052", + }).AnyTimes() + mockConfig.EXPECT().GetConfigPath().Return("/path/to/config.yaml") + + mockDocker := mock.NewMockDockerSidecar(ctrl) + mockBinary := mock.NewMockBinarySidecar(ctrl) + + // Create mock systemd sidecar that's active + mockSystemd := mock.NewMockSystemdSidecar(ctrl) + mockSystemd.EXPECT().IsRunning().Return(true, nil) + mockSystemd.EXPECT().Status().Return("active", nil) + + mockGithub := servicemock.NewMockGitHubService(ctrl) + mockGithub.EXPECT().GetLatestVersion().Return("1.0.0", nil) + + err := showStatus( + cli.NewContext(nil, nil, nil), + logrus.New(), + mockConfig, + mockDocker, + mockSystemd, + mockBinary, + mockGithub, + ) + + assert.NoError(t, err) + }) + t.Run("handles github service error gracefully", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -113,6 +152,7 @@ func TestShowStatus(t *testing.T) { mockDocker := mock.NewMockDockerSidecar(ctrl) mockDocker.EXPECT().IsRunning().Return(true, nil) + mockDocker.EXPECT().Status().Return("running", nil) mockSystemd := mock.NewMockSystemdSidecar(ctrl) // Create mock GitHub service that returns an error diff --git a/cmd/cli/commands/stop/stop.go b/cmd/cli/commands/stop/stop.go index 8e0b4e8..3a70956 100644 --- a/cmd/cli/commands/stop/stop.go +++ b/cmd/cli/commands/stop/stop.go @@ -87,21 +87,6 @@ func stopContributoor( return fmt.Errorf("invalid sidecar run method: %s", cfg.RunMethod) } - // Check if running before attempting to stop. - running, err := runner.IsRunning() - if err != nil { - log.Errorf("could not check sidecar status: %v", err) - - return err - } - - // If the service is not running, we can just return. - if !running { - fmt.Printf("%sContributoor is not running. Use 'contributoor start' to start it%s\n", tui.TerminalColorYellow, tui.TerminalColorReset) - - return nil - } - if err := runner.Stop(); err != nil { return err } diff --git a/cmd/cli/commands/stop/stop_test.go b/cmd/cli/commands/stop/stop_test.go index 781f210..e61c251 100644 --- a/cmd/cli/commands/stop/stop_test.go +++ b/cmd/cli/commands/stop/stop_test.go @@ -27,29 +27,17 @@ func TestStopContributoor(t *testing.T) { expectedError string }{ { - name: "docker - stops running service successfully", + name: "docker - stops service successfully", runMethod: config.RunMethod_RUN_METHOD_DOCKER, setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerSidecar, b *mock.MockBinarySidecar, g *servicemock.MockGitHubService) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_DOCKER, Version: "latest", }).Times(1) - d.EXPECT().IsRunning().Return(true, nil) d.EXPECT().Stop().Return(nil) g.EXPECT().GetLatestVersion().Return("v1.0.0", nil) }, }, - { - name: "docker - service not running", - runMethod: config.RunMethod_RUN_METHOD_DOCKER, - setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerSidecar, b *mock.MockBinarySidecar, g *servicemock.MockGitHubService) { - cfg.EXPECT().Get().Return(&config.Config{ - RunMethod: config.RunMethod_RUN_METHOD_DOCKER, - }).Times(1) - d.EXPECT().IsRunning().Return(false, nil) - g.EXPECT().GetLatestVersion().Return("v1.0.0", nil) - }, - }, { name: "docker - stop fails", runMethod: config.RunMethod_RUN_METHOD_DOCKER, @@ -57,35 +45,22 @@ func TestStopContributoor(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_DOCKER, }).Times(1) - d.EXPECT().IsRunning().Return(true, nil) d.EXPECT().Stop().Return(errors.New("stop failed")) g.EXPECT().GetLatestVersion().Return("v1.0.0", nil) }, expectedError: "stop failed", }, { - name: "binary - stops running service successfully", + name: "binary - stops service successfully", runMethod: config.RunMethod_RUN_METHOD_BINARY, setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerSidecar, b *mock.MockBinarySidecar, g *servicemock.MockGitHubService) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_BINARY, }).Times(1) - b.EXPECT().IsRunning().Return(true, nil) b.EXPECT().Stop().Return(nil) g.EXPECT().GetLatestVersion().Return("v1.0.0", nil) }, }, - { - name: "binary - service not running", - runMethod: config.RunMethod_RUN_METHOD_BINARY, - setupMocks: func(cfg *mock.MockConfigManager, d *mock.MockDockerSidecar, b *mock.MockBinarySidecar, g *servicemock.MockGitHubService) { - cfg.EXPECT().Get().Return(&config.Config{ - RunMethod: config.RunMethod_RUN_METHOD_BINARY, - }).Times(1) - b.EXPECT().IsRunning().Return(false, nil) - g.EXPECT().GetLatestVersion().Return("v1.0.0", nil) - }, - }, { name: "invalid sidecar run method", runMethod: config.RunMethod_RUN_METHOD_UNSPECIFIED, @@ -104,7 +79,6 @@ func TestStopContributoor(t *testing.T) { cfg.EXPECT().Get().Return(&config.Config{ RunMethod: config.RunMethod_RUN_METHOD_DOCKER, }).Times(1) - d.EXPECT().IsRunning().Return(true, nil) d.EXPECT().Stop().Return(nil) g.EXPECT().GetLatestVersion().Return("", errors.New("github error")) }, diff --git a/internal/service/mock/github.mock.go b/internal/service/mock/github.mock.go index b1810f2..0db2e41 100644 --- a/internal/service/mock/github.mock.go +++ b/internal/service/mock/github.mock.go @@ -19,6 +19,7 @@ import ( type MockGitHubService struct { ctrl *gomock.Controller recorder *MockGitHubServiceMockRecorder + isgomock struct{} } // MockGitHubServiceMockRecorder is the mock recorder for MockGitHubService. @@ -54,16 +55,16 @@ func (mr *MockGitHubServiceMockRecorder) GetLatestVersion() *gomock.Call { } // VersionExists mocks base method. -func (m *MockGitHubService) VersionExists(arg0 string) (bool, error) { +func (m *MockGitHubService) VersionExists(version string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VersionExists", arg0) + ret := m.ctrl.Call(m, "VersionExists", version) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // VersionExists indicates an expected call of VersionExists. -func (mr *MockGitHubServiceMockRecorder) VersionExists(arg0 any) *gomock.Call { +func (mr *MockGitHubServiceMockRecorder) VersionExists(version any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VersionExists", reflect.TypeOf((*MockGitHubService)(nil).VersionExists), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VersionExists", reflect.TypeOf((*MockGitHubService)(nil).VersionExists), version) } diff --git a/internal/sidecar/binary.go b/internal/sidecar/binary.go index 6be4dd5..8adee0c 100644 --- a/internal/sidecar/binary.go +++ b/internal/sidecar/binary.go @@ -164,6 +164,39 @@ func (s *binarySidecar) Stop() error { return nil } +// Status returns the current state of the binary process. +func (s *binarySidecar) Status() (string, error) { + cfg := s.sidecarCfg.Get() + pidFile := filepath.Join(cfg.ContributoorDirectory, "contributoor.pid") + + // If no PID file, process is not running. + pidBytes, err := os.ReadFile(pidFile) + if err != nil { + if os.IsNotExist(err) { + return "stopped", nil + } + + return "", fmt.Errorf("failed to read pid file: %w", err) + } + + pidStr := string(pidBytes) + if !regexp.MustCompile(`^\d+$`).MatchString(pidStr) { + return "unknown", fmt.Errorf("invalid PID format") + } + + // kill -0 just checks if process exists. It doesn't actually send a + // signal that affects the process. + cmd := exec.Command("kill", "-0", pidStr) + if err := cmd.Run(); err != nil { + os.Remove(pidFile) + + //nolint:nilerr // We don't care about the error here. + return "stopped", nil + } + + return "running", nil +} + // IsRunning checks if the binary service is running. func (s *binarySidecar) IsRunning() (bool, error) { cfg := s.sidecarCfg.Get() @@ -230,9 +263,11 @@ func (s *binarySidecar) Logs(tailLines int, follow bool) error { return fmt.Errorf("failed to expand config path: %w", err) } - logFile := filepath.Join(expandedDir, "logs", "debug.log") - - args := []string{} + var ( + debugLog = filepath.Join(expandedDir, "logs", "debug.log") + serviceLog = filepath.Join(expandedDir, "logs", "service.log") + args = make([]string, 0) + ) if follow { args = append(args, "-f") @@ -242,7 +277,7 @@ func (s *binarySidecar) Logs(tailLines int, follow bool) error { args = append(args, "-n", fmt.Sprintf("%d", tailLines)) } - args = append(args, logFile) + args = append(args, debugLog, serviceLog) cmd := exec.Command("tail", args...) cmd.Stdout = os.Stdout diff --git a/internal/sidecar/docker.go b/internal/sidecar/docker.go index a21550d..311e5fc 100644 --- a/internal/sidecar/docker.go +++ b/internal/sidecar/docker.go @@ -79,9 +79,20 @@ func NewDockerSidecar(logger *logrus.Logger, sidecarCfg ConfigManager, installer // Start starts the docker container using docker-compose. func (s *dockerSidecar) Start() error { + // Check if container exists and remove it first, if it does. + cmd := exec.Command("docker", "ps", "-aq", "-f", "name=contributoor") + + output, err := cmd.Output() + if err == nil && len(strings.TrimSpace(string(output))) > 0 { + removeCmd := exec.Command("docker", "rm", "-f", "contributoor") + if output, err := removeCmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to remove existing container: %w\nOutput: %s", err, string(output)) + } + } + args := append(s.getComposeArgs(), "up", "-d", "--pull", "always") - cmd := exec.Command("docker", args...) + cmd = exec.Command("docker", args...) cmd.Env = s.GetComposeEnv() if output, err := cmd.CombinedOutput(); err != nil { @@ -123,33 +134,39 @@ func (s *dockerSidecar) Stop() error { return nil } -// IsRunning checks if the docker container is running. -func (s *dockerSidecar) IsRunning() (bool, error) { - // Check via compose first. If there has been any sort of configuration change between - // versions, then this will return a non running state. - args := append(s.getComposeArgs(), "ps", "--format", "{{.State}}") - cmd := exec.Command("docker", args...) - cmd.Env = s.GetComposeEnv() +// Status returns the current state of the docker container. +func (s *dockerSidecar) Status() (string, error) { + // First check if container exists. + cmd := exec.Command("docker", "ps", "-a", "--filter", "name=contributoor", "--format", "{{.ID}}") output, err := cmd.Output() - if err == nil { - states := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, state := range states { - if strings.Contains(strings.ToLower(state), "running") { - return true, nil - } - } + if err != nil || len(strings.TrimSpace(string(output))) == 0 { + //nolint:nilerr // We don't care about the error here. + return "not running", nil } - // In that case, we will fallback to checking for any container with the name 'contributoor'. - cmd = exec.Command("docker", "ps", "-q", "-f", "name=contributoor") + // Container exists, get its status. + cmd = exec.Command("docker", "inspect", "-f", "{{.State.Status}}", "contributoor") output, err = cmd.Output() if err != nil { - return false, fmt.Errorf("failed to check container status: %w", err) + return "", fmt.Errorf("failed to get container status: %w", err) + } + + return strings.TrimSpace(string(output)), nil +} + +// IsRunning checks if the docker container is running. +func (s *dockerSidecar) IsRunning() (bool, error) { + cmd := exec.Command("docker", "inspect", "-f", "{{.State.Status}}", "contributoor") + + output, err := cmd.Output() + if err != nil { + //nolint:nilerr // We don't care about the error here. + return false, nil } - return len(strings.TrimSpace(string(output))) > 0, nil + return strings.TrimSpace(string(output)) == "running", nil } // Update pulls the latest image and restarts the container. diff --git a/internal/sidecar/mock/binary.mock.go b/internal/sidecar/mock/binary.mock.go index 83a0ca6..84acfcd 100644 --- a/internal/sidecar/mock/binary.mock.go +++ b/internal/sidecar/mock/binary.mock.go @@ -19,6 +19,7 @@ import ( type MockBinarySidecar struct { ctrl *gomock.Controller recorder *MockBinarySidecarMockRecorder + isgomock struct{} } // MockBinarySidecarMockRecorder is the mock recorder for MockBinarySidecar. @@ -54,17 +55,17 @@ func (mr *MockBinarySidecarMockRecorder) IsRunning() *gomock.Call { } // Logs mocks base method. -func (m *MockBinarySidecar) Logs(arg0 int, arg1 bool) error { +func (m *MockBinarySidecar) Logs(tailLines int, follow bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Logs", arg0, arg1) + ret := m.ctrl.Call(m, "Logs", tailLines, follow) ret0, _ := ret[0].(error) return ret0 } // Logs indicates an expected call of Logs. -func (mr *MockBinarySidecarMockRecorder) Logs(arg0, arg1 any) *gomock.Call { +func (mr *MockBinarySidecarMockRecorder) Logs(tailLines, follow any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockBinarySidecar)(nil).Logs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockBinarySidecar)(nil).Logs), tailLines, follow) } // Start mocks base method. @@ -81,6 +82,21 @@ func (mr *MockBinarySidecarMockRecorder) Start() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockBinarySidecar)(nil).Start)) } +// Status mocks base method. +func (m *MockBinarySidecar) Status() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status. +func (mr *MockBinarySidecarMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockBinarySidecar)(nil).Status)) +} + // Stop mocks base method. func (m *MockBinarySidecar) Stop() error { m.ctrl.T.Helper() diff --git a/internal/sidecar/mock/config.mock.go b/internal/sidecar/mock/config.mock.go index 20cd912..a5ddfe2 100644 --- a/internal/sidecar/mock/config.mock.go +++ b/internal/sidecar/mock/config.mock.go @@ -20,6 +20,7 @@ import ( type MockConfigManager struct { ctrl *gomock.Controller recorder *MockConfigManagerMockRecorder + isgomock struct{} } // MockConfigManagerMockRecorder is the mock recorder for MockConfigManager. @@ -82,15 +83,15 @@ func (mr *MockConfigManagerMockRecorder) Save() *gomock.Call { } // Update mocks base method. -func (m *MockConfigManager) Update(arg0 func(*config.Config)) error { +func (m *MockConfigManager) Update(updates func(*config.Config)) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", arg0) + ret := m.ctrl.Call(m, "Update", updates) ret0, _ := ret[0].(error) return ret0 } // Update indicates an expected call of Update. -func (mr *MockConfigManagerMockRecorder) Update(arg0 any) *gomock.Call { +func (mr *MockConfigManagerMockRecorder) Update(updates any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockConfigManager)(nil).Update), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockConfigManager)(nil).Update), updates) } diff --git a/internal/sidecar/mock/docker.mock.go b/internal/sidecar/mock/docker.mock.go index 2c288fc..70f2638 100644 --- a/internal/sidecar/mock/docker.mock.go +++ b/internal/sidecar/mock/docker.mock.go @@ -19,6 +19,7 @@ import ( type MockDockerSidecar struct { ctrl *gomock.Controller recorder *MockDockerSidecarMockRecorder + isgomock struct{} } // MockDockerSidecarMockRecorder is the mock recorder for MockDockerSidecar. @@ -68,17 +69,17 @@ func (mr *MockDockerSidecarMockRecorder) IsRunning() *gomock.Call { } // Logs mocks base method. -func (m *MockDockerSidecar) Logs(arg0 int, arg1 bool) error { +func (m *MockDockerSidecar) Logs(tailLines int, follow bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Logs", arg0, arg1) + ret := m.ctrl.Call(m, "Logs", tailLines, follow) ret0, _ := ret[0].(error) return ret0 } // Logs indicates an expected call of Logs. -func (mr *MockDockerSidecarMockRecorder) Logs(arg0, arg1 any) *gomock.Call { +func (mr *MockDockerSidecarMockRecorder) Logs(tailLines, follow any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockDockerSidecar)(nil).Logs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockDockerSidecar)(nil).Logs), tailLines, follow) } // Start mocks base method. @@ -95,6 +96,21 @@ func (mr *MockDockerSidecarMockRecorder) Start() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDockerSidecar)(nil).Start)) } +// Status mocks base method. +func (m *MockDockerSidecar) Status() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status. +func (mr *MockDockerSidecarMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockDockerSidecar)(nil).Status)) +} + // Stop mocks base method. func (m *MockDockerSidecar) Stop() error { m.ctrl.T.Helper() diff --git a/internal/sidecar/mock/systemd.mock.go b/internal/sidecar/mock/systemd.mock.go index 956b161..1bc28e2 100644 --- a/internal/sidecar/mock/systemd.mock.go +++ b/internal/sidecar/mock/systemd.mock.go @@ -19,6 +19,7 @@ import ( type MockSystemdSidecar struct { ctrl *gomock.Controller recorder *MockSystemdSidecarMockRecorder + isgomock struct{} } // MockSystemdSidecarMockRecorder is the mock recorder for MockSystemdSidecar. @@ -54,17 +55,17 @@ func (mr *MockSystemdSidecarMockRecorder) IsRunning() *gomock.Call { } // Logs mocks base method. -func (m *MockSystemdSidecar) Logs(arg0 int, arg1 bool) error { +func (m *MockSystemdSidecar) Logs(tailLines int, follow bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Logs", arg0, arg1) + ret := m.ctrl.Call(m, "Logs", tailLines, follow) ret0, _ := ret[0].(error) return ret0 } // Logs indicates an expected call of Logs. -func (mr *MockSystemdSidecarMockRecorder) Logs(arg0, arg1 any) *gomock.Call { +func (mr *MockSystemdSidecarMockRecorder) Logs(tailLines, follow any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockSystemdSidecar)(nil).Logs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockSystemdSidecar)(nil).Logs), tailLines, follow) } // Start mocks base method. @@ -81,6 +82,21 @@ func (mr *MockSystemdSidecarMockRecorder) Start() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSystemdSidecar)(nil).Start)) } +// Status mocks base method. +func (m *MockSystemdSidecar) Status() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status. +func (mr *MockSystemdSidecarMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockSystemdSidecar)(nil).Status)) +} + // Stop mocks base method. func (m *MockSystemdSidecar) Stop() error { m.ctrl.T.Helper() diff --git a/internal/sidecar/sidecar.go b/internal/sidecar/sidecar.go index b0d691c..7d1a9e0 100644 --- a/internal/sidecar/sidecar.go +++ b/internal/sidecar/sidecar.go @@ -23,6 +23,9 @@ type SidecarRunner interface { // Update updates the service. Update() error + // Status returns the status of the service. + Status() (string, error) + // IsRunning checks if the service is running. IsRunning() (bool, error) diff --git a/internal/sidecar/systemd.go b/internal/sidecar/systemd.go index 4760482..e282a29 100644 --- a/internal/sidecar/systemd.go +++ b/internal/sidecar/systemd.go @@ -2,6 +2,7 @@ package sidecar import ( "fmt" + "os" "os/exec" "runtime" "strings" @@ -95,16 +96,35 @@ func (s *systemdSidecar) Update() error { return s.reloadSystemd() } -// Logs shows the logs from the log files (systemd/launchd is configured to punch out logs to -// the path as the binary sidecar). +// Logs shows the logs from the service. func (s *systemdSidecar) Logs(tailLines int, follow bool) error { - // Create a binary sidecar just for log viewing. - binarySidecar, err := NewBinarySidecar(s.logger, s.sidecarCfg, s.installerCfg) - if err != nil { - return fmt.Errorf("failed to create binary sidecar for logs: %w", err) + // For macOS, use binary logs. + if runtime.GOOS == ArchDarwin { + binarySidecar, err := NewBinarySidecar(s.logger, s.sidecarCfg, s.installerCfg) + if err != nil { + return fmt.Errorf("failed to create binary sidecar for logs: %w", err) + } + + return binarySidecar.Logs(tailLines, follow) + } + + // For Linux/systemd, use journalctl. + args := []string{"-u", "contributoor.service", "-e"} + + if follow { + args = append(args, "-f") } - return binarySidecar.Logs(tailLines, follow) + if tailLines > 0 { + args = append(args, "-n", fmt.Sprintf("%d", tailLines)) + } + + //nolint:gosec // controlled input. + cmd := exec.Command("sudo", append([]string{"journalctl"}, args...)...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() } func (s *systemdSidecar) startSystemd() error { @@ -287,3 +307,46 @@ func (s *systemdSidecar) checkBinaryExists() error { return nil } + +// Status returns the current state of the service. +func (s *systemdSidecar) Status() (string, error) { + if runtime.GOOS == ArchDarwin { + if err := s.checkDaemonExists(); err != nil { + return "", wrapNotInstalledError(err, "launchd") + } + + // For macOS, check launchd status. + cmd := exec.Command("sudo", "launchctl", "list", "io.ethpandaops.contributoor") + + output, err := cmd.Output() + if err != nil { + //nolint:nilerr // We don't care about the error here. + return "inactive", nil + } + + // If service is running, output will contain a PID. + lines := strings.Split(string(output), "\n") + if len(lines) > 0 { + fields := strings.Fields(lines[0]) + if len(fields) > 0 && fields[0] != "-" { + return "active", nil + } + } + + return "inactive", nil + } + + if err := s.checkDaemonExists(); err != nil { + return "", wrapNotInstalledError(err, "systemd") + } + + // For Linux/systemd, get service state. + cmd := exec.Command("sudo", "systemctl", "show", "-p", "ActiveState", "--value", "contributoor.service") + + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get service status: %w", err) + } + + return strings.TrimSpace(string(output)), nil +}