diff --git a/docs/PRE-RELEASE-TASKS.md b/docs/PRE-RELEASE-TASKS.md index 6ad2f86..f986a2e 100644 --- a/docs/PRE-RELEASE-TASKS.md +++ b/docs/PRE-RELEASE-TASKS.md @@ -84,7 +84,7 @@ These land together because both edit `domain/job.go` and `storage/store.go`. - [x] P1.2 — `writeJSON` + JSON unmarshal - [x] P1.3 — `gosentry.json` / `jobs.json` paths; drop pysentry name - [x] P1.4 — One-time YAML import -- [ ] P1.5 — Remove `SuccessExitCodes` across code +- [x] P1.5 — Remove `SuccessExitCodes` across code - [ ] P1.6 — Update storage/runner/format tests + TESTS.md ### Phase 2 — PySentry legacy removal diff --git a/src/app/format.go b/src/app/format.go index b44989e..c49863d 100644 --- a/src/app/format.go +++ b/src/app/format.go @@ -50,15 +50,6 @@ func DisplayArguments(arguments string) string { return strings.TrimSpace(arguments) } -// DisplaySuccessExitCodes formats a job's success exit codes for display: -// "0" (the default) if empty, else the trimmed codes. -func DisplaySuccessExitCodes(codes string) string { - if strings.TrimSpace(codes) == "" { - return "0" - } - return strings.TrimSpace(codes) -} - // DisplayRunMode formats a job's execution mode: "Start only" or // "Wait for completion". func DisplayRunMode(job domain.Job) string { diff --git a/src/app/format_test.go b/src/app/format_test.go index 016bd7b..3fb3f54 100644 --- a/src/app/format_test.go +++ b/src/app/format_test.go @@ -68,15 +68,6 @@ func TestDisplayArguments(t *testing.T) { } } -func TestDisplaySuccessExitCodes(t *testing.T) { - if got := DisplaySuccessExitCodes(" "); got != "0" { - t.Errorf("empty codes = %q, want %q", got, "0") - } - if got := DisplaySuccessExitCodes(" 0,1 "); got != "0,1" { - t.Errorf("codes = %q, want %q", got, "0,1") - } -} - func TestDisplayRunMode(t *testing.T) { if got := DisplayRunMode(domain.Job{StartOnly: true}); got != "Start only" { t.Errorf("start-only = %q, want %q", got, "Start only") diff --git a/src/app/operations.go b/src/app/operations.go index a932e7e..bd66547 100644 --- a/src/app/operations.go +++ b/src/app/operations.go @@ -436,8 +436,6 @@ func runningOutput(job domain.Job, trigger string, started time.Time) string { builder.WriteString(job.Command + "\n\n") builder.WriteString("arguments:\n") builder.WriteString(runner.LogArguments(job.Arguments)) - builder.WriteString("\n\nsuccess_exit_codes:\n") - builder.WriteString(runner.SuccessExitCodesText(job)) builder.WriteString("\n\nstart_only:\n") builder.WriteString(fmt.Sprintf("%t", job.StartOnly)) return builder.String() @@ -451,10 +449,6 @@ func normalizeJob(job *domain.Job) { job.Schedule = strings.TrimSpace(job.Schedule) job.Command = strings.TrimSpace(job.Command) job.Arguments = strings.TrimSpace(job.Arguments) - job.SuccessExitCodes = strings.TrimSpace(job.SuccessExitCodes) - if job.SuccessExitCodes == "" { - job.SuccessExitCodes = "0" - } } // validateJob enforces the minimum executable definition: name, schedule, and diff --git a/src/domain/job.go b/src/domain/job.go index e58b72d..5a64492 100644 --- a/src/domain/job.go +++ b/src/domain/job.go @@ -12,7 +12,6 @@ type Job struct { Schedule string `json:"schedule"` Command string `json:"command"` Arguments string `json:"arguments,omitempty"` - SuccessExitCodes string `json:"success_exit_codes,omitempty"` StartOnly bool `json:"start_only,omitempty"` Enabled bool `json:"enabled"` } diff --git a/src/runner/exitcodes.go b/src/runner/exitcodes.go deleted file mode 100644 index 9d05433..0000000 --- a/src/runner/exitcodes.go +++ /dev/null @@ -1,52 +0,0 @@ -package runner - -import ( - "strconv" - "strings" - - "gitea.mixdep.ru/mix/gosentry/src/domain" -) - -func acceptedExitCode(exitCode int, successExitCodes string) bool { - for _, accepted := range parseExitCodes(successExitCodes) { - if exitCode == accepted { - return true - } - } - return false -} - -func parseExitCodes(value string) []int { - value = strings.TrimSpace(value) - if value == "" { - return []int{0} - } - fields := strings.FieldsFunc(value, func(r rune) bool { - return r == ',' || r == ';' || r == ' ' || r == '\t' || r == '\n' || r == '\r' - }) - result := make([]int, 0, len(fields)) - seen := map[int]bool{} - for _, field := range fields { - code, err := strconv.Atoi(strings.TrimSpace(field)) - if err != nil || seen[code] { - continue - } - seen[code] = true - result = append(result, code) - } - if len(result) == 0 { - return []int{0} - } - return result -} - -func SuccessExitCodesText(job domain.Job) string { - codes := parseExitCodes(job.SuccessExitCodes) - parts := make([]string, 0, len(codes)) - for _, code := range codes { - parts = append(parts, strconv.Itoa(code)) - } - return strings.Join(parts, ",") -} - -func successExitCodesText(job domain.Job) string { return SuccessExitCodesText(job) } diff --git a/src/runner/logfile.go b/src/runner/logfile.go index 641886b..59e9262 100644 --- a/src/runner/logfile.go +++ b/src/runner/logfile.go @@ -23,8 +23,8 @@ func writeRunLog(logsDir string, job domain.Job, trigger string, state string, d // avoid characters that are invalid on Windows or awkward on shells. fileName := started.Format("20060102-150405") + "_" + sanitizeFileName(job.Name) + ".log" path := filepath.Join(logsDir, fileName) - content := fmt.Sprintf("time: %s\njob_id: %d\njob_name: %s\ntrigger: %s\nstate: %s\ndetail: %s\ncommand: %s\narguments: %s\nsuccess_exit_codes: %s\nstart_only: %t\n\n%s\n", - started.Format("2006-01-02 15:04:05"), job.ID, job.Name, trigger, state, detail, job.Command, logArguments(job.Arguments), successExitCodesText(job), job.StartOnly, output) + content := fmt.Sprintf("time: %s\njob_id: %d\njob_name: %s\ntrigger: %s\nstate: %s\ndetail: %s\ncommand: %s\narguments: %s\nstart_only: %t\n\n%s\n", + started.Format("2006-01-02 15:04:05"), job.ID, job.Name, trigger, state, detail, job.Command, logArguments(job.Arguments), job.StartOnly, output) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { return "" } diff --git a/src/runner/runner.go b/src/runner/runner.go index 873d607..41d5ec9 100644 --- a/src/runner/runner.go +++ b/src/runner/runner.go @@ -45,7 +45,7 @@ func RunJob(ctx context.Context, job *domain.Job, trigger string, logsDir string err := command.Run() duration := time.Since(started).Round(time.Millisecond) output = formatOutput(stdoutBuf.String(), stderrBuf.String()) - state, detail = runStateDetail(err, runCtx.Err(), duration, *job) + state, detail = runStateDetail(err, runCtx.Err(), duration) } now := time.Now() @@ -100,7 +100,7 @@ func startOnlyOutput(job domain.Job, pid int) string { return builder.String() } -func runStateDetail(err error, runErr error, duration time.Duration, job domain.Job) (string, string) { +func runStateDetail(err error, runErr error, duration time.Duration) (string, string) { if err == nil { return "OK", fmt.Sprintf("Completed in %s (exit code 0)", duration) } @@ -110,14 +110,9 @@ func runStateDetail(err error, runErr error, duration time.Duration, job domain. if errors.Is(err, exec.ErrWaitDelay) { return "OK", fmt.Sprintf("Completed; output capture stopped after %s because a child process kept the stream open", commandWaitDelay) } - var exitError *exec.ExitError if errors.As(err, &exitError) { - exitCode := exitError.ExitCode() - if acceptedExitCode(exitCode, job.SuccessExitCodes) { - return "OK", fmt.Sprintf("Completed in %s with accepted exit code %d", duration, exitCode) - } - return "Failed", fmt.Sprintf("Exit code %d is not in success_exit_codes (%s)", exitCode, successExitCodesText(job)) + return "Failed", fmt.Sprintf("Failed with exit code %d", exitError.ExitCode()) } return "Failed", fmt.Sprintf("%T: %v", err, err) } diff --git a/src/runner/runner_test.go b/src/runner/runner_test.go index aadae4a..e578823 100644 --- a/src/runner/runner_test.go +++ b/src/runner/runner_test.go @@ -23,10 +23,9 @@ func echoCommand(message string) string { func TestRunJobLogFileAllHeaders(t *testing.T) { logsDir := t.TempDir() job := domain.Job{ - ID: 99, - Name: "Log Header Test", - Command: echoCommand("header test output"), - SuccessExitCodes: "0,1", + ID: 99, + Name: "Log Header Test", + Command: echoCommand("header test output"), } record := RunJob(context.Background(), &job, "Schedule", logsDir) @@ -48,7 +47,6 @@ func TestRunJobLogFileAllHeaders(t *testing.T) { "detail: ", "command: " + job.Command, "arguments: ", - "success_exit_codes: 0,1", "start_only: false", "stdout:", "stderr:", @@ -256,40 +254,15 @@ func TestRunJobRunsWindowsCommandWithSeparateArguments(t *testing.T) { } } -func TestRunJobAcceptsConfiguredExitCode(t *testing.T) { +func TestRunJobFailsOnNonZeroExitCode(t *testing.T) { command := `sh -c 'exit 1'` if runtime.GOOS == "windows" { command = `C:\Windows\System32\cmd.exe` } job := domain.Job{ - ID: 46, - Name: "Accepted Exit Code", - Command: command, - SuccessExitCodes: "0,1", - } - if runtime.GOOS == "windows" { - job.Arguments = "/C\nexit /b 1" - } - - record := RunJob(context.Background(), &job, "Manual", t.TempDir()) - if record.State != "OK" { - t.Fatalf("expected accepted exit code to be OK, got state %q detail %q", record.State, record.Detail) - } - if !strings.Contains(record.Detail, "accepted exit code 1") { - t.Fatalf("expected accepted exit code detail, got %q", record.Detail) - } -} - -func TestRunJobRejectsUnconfiguredExitCode(t *testing.T) { - command := `sh -c 'exit 1'` - if runtime.GOOS == "windows" { - command = `C:\Windows\System32\cmd.exe` - } - job := domain.Job{ - ID: 47, - Name: "Rejected Exit Code", - Command: command, - SuccessExitCodes: "0", + ID: 47, + Name: "Non-zero Exit Code", + Command: command, } if runtime.GOOS == "windows" { job.Arguments = "/C\nexit /b 1" @@ -297,9 +270,9 @@ func TestRunJobRejectsUnconfiguredExitCode(t *testing.T) { record := RunJob(context.Background(), &job, "Manual", t.TempDir()) if record.State != "Failed" { - t.Fatalf("expected rejected exit code to fail, got state %q detail %q", record.State, record.Detail) + t.Fatalf("expected non-zero exit code to fail, got state %q detail %q", record.State, record.Detail) } - if !strings.Contains(record.Detail, "Exit code 1") { + if !strings.Contains(record.Detail, "exit code 1") { t.Fatalf("expected exit code detail, got %q", record.Detail) } } @@ -349,19 +322,6 @@ func TestRunJobStartOnlyReportsStartFailure(t *testing.T) { } } -func TestParseExitCodes(t *testing.T) { - got := parseExitCodes("0, 1;2\n3") - want := []int{0, 1, 2, 3} - if len(got) != len(want) { - t.Fatalf("expected %v, got %v", want, got) - } - for index := range want { - if got[index] != want[index] { - t.Fatalf("expected %v, got %v", want, got) - } - } -} - func TestDirectCommandDoesNotHideWindow(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("Windows window visibility only") diff --git a/src/storage/store.go b/src/storage/store.go index b40e4a6..2eacb78 100644 --- a/src/storage/store.go +++ b/src/storage/store.go @@ -40,8 +40,7 @@ type yamlJob struct { Schedule string `yaml:"schedule"` Command string `yaml:"command"` Arguments string `yaml:"arguments,omitempty"` - SuccessExitCodes string `yaml:"success_exit_codes,omitempty"` - StartOnly bool `yaml:"start_only,omitempty"` + StartOnly bool `yaml:"start_only,omitempty"` Enabled bool `yaml:"enabled"` } @@ -246,10 +245,6 @@ func normalizeJobs(jobs []domain.Job) { job.Command = echoCommand("GoSentry job ran") } job.Arguments = strings.TrimSpace(job.Arguments) - job.SuccessExitCodes = strings.TrimSpace(job.SuccessExitCodes) - if job.SuccessExitCodes == "" { - job.SuccessExitCodes = "0" - } // Runtime state (last run, next run, status, output, activity) is no longer // part of Job. It is reconstructed each time the app starts via // domain.NewRuntime, so normalizeJobs only touches durable configuration. diff --git a/src/storage/store_test.go b/src/storage/store_test.go index 02c2fd1..635a2a9 100644 --- a/src/storage/store_test.go +++ b/src/storage/store_test.go @@ -10,25 +10,32 @@ import ( "go.yaml.in/yaml/v4" ) +func writeYAML(path string, value any) error { + data, err := yaml.Marshal(value) + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + func TestJobsRoundTrip(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "jobs.yaml") + path := filepath.Join(dir, "jobs.json") original := []domain.Job{ { - ID: 7, - Name: "Backup data", - Folder: "Maintenance", - Schedule: "0 2 * * *", - Command: "/usr/bin/backup", - Arguments: "--compress\n--verbose", - SuccessExitCodes: "0,1", - StartOnly: true, - Enabled: true, + ID: 7, + Name: "Backup data", + Folder: "Maintenance", + Schedule: "0 2 * * *", + Command: "/usr/bin/backup", + Arguments: "--compress\n--verbose", + StartOnly: true, + Enabled: true, }, } - if err := writeYAML(path, domain.JobsFile{Jobs: original}); err != nil { + if err := writeJSON(path, domain.JobsFile{Jobs: original}); err != nil { t.Fatal(err) } @@ -59,9 +66,6 @@ func TestJobsRoundTrip(t *testing.T) { if g.Arguments != w.Arguments { t.Errorf("Arguments: got %q, want %q", g.Arguments, w.Arguments) } - if g.SuccessExitCodes != w.SuccessExitCodes { - t.Errorf("SuccessExitCodes: got %q, want %q", g.SuccessExitCodes, w.SuccessExitCodes) - } if g.StartOnly != w.StartOnly { t.Errorf("StartOnly: got %v, want %v", g.StartOnly, w.StartOnly) } @@ -86,10 +90,10 @@ func TestConfigRoundTrip(t *testing.T) { MaxLogFiles: 50, MaxLogAgeDays: 14, StartOnLogin: true, - KeepRunningInTray: false, - NotifyOnFailure: false, + KeepRunningInTray: true, + NotifyOnFailure: true, } - if err := writeYAML(paths.ConfigPath, want); err != nil { + if err := writeJSON(paths.ConfigPath, want); err != nil { t.Fatal(err) } @@ -125,12 +129,12 @@ func TestNormalizeJobsFillsDefaults(t *testing.T) { jobs := []domain.Job{ {Enabled: true}, {Enabled: false}, - {ID: 5, Name: "Kept", Schedule: "*/10 * * * *", SuccessExitCodes: "0,1", Enabled: true}, + {ID: 5, Name: "Kept", Schedule: "*/10 * * * *", Enabled: true}, } normalizeJobs(jobs) - // Blank enabled job gets default name, schedule, command, and exit codes. + // Blank enabled job gets default name, schedule, and command. // normalizeJobs only fills durable configuration now; runtime status is built // separately by domain.NewRuntime. if jobs[0].ID != 1 { @@ -142,17 +146,11 @@ func TestNormalizeJobsFillsDefaults(t *testing.T) { if jobs[0].Schedule != "@every 1m" { t.Errorf("default schedule: got %q, want '@every 1m'", jobs[0].Schedule) } - if jobs[0].SuccessExitCodes != "0" { - t.Errorf("default exit codes: got %q, want '0'", jobs[0].SuccessExitCodes) - } // Pre-set fields survive normalization unchanged. if jobs[2].ID != 5 { t.Errorf("pre-set ID should be preserved: got %d, want 5", jobs[2].ID) } - if jobs[2].SuccessExitCodes != "0,1" { - t.Errorf("pre-set exit codes should be preserved: got %q, want '0,1'", jobs[2].SuccessExitCodes) - } } // TestLoadOrCreateConfigMigratesFromLegacy verifies that when gosentry.json is @@ -165,12 +163,12 @@ func TestLoadOrCreateConfigMigratesFromLegacy(t *testing.T) { ConfigPath: filepath.Join(dir, ConfigFileName), // gosentry.json — not created } - legacy := domain.Config{ - JobsDir: "/legacy/jobs", - LogsDir: "/legacy/logs", - MaxLogFiles: 77, + legacy := yamlConfig{ + JobsDir: "/legacy/jobs", + LogsDir: "/legacy/logs", + MaxLogFiles: 77, MaxLogAgeDays: 13, - StartOnLogin: true, + StartOnLogin: true, } if err := writeYAML(filepath.Join(dir, legacyYAMLConfigFileName), legacy); err != nil { t.Fatal(err) diff --git a/src/ui/job_dialog.go b/src/ui/job_dialog.go index 1e79767..e807af3 100644 --- a/src/ui/job_dialog.go +++ b/src/ui/job_dialog.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "gitea.mixdep.ru/mix/gosentry/src/app" "gitea.mixdep.ru/mix/gosentry/src/domain" "fyne.io/fyne/v2" @@ -31,9 +30,6 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) { argumentsEntry := widget.NewMultiLineEntry() argumentsEntry.SetPlaceHolder(`D:\Local\Jobs\Auto.ffs_batch`) argumentsEntry.SetText(current.Arguments) - successExitCodesEntry := widget.NewEntry() - successExitCodesEntry.SetPlaceHolder("0") - successExitCodesEntry.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes)) startOnly := widget.NewCheck("Start only, do not wait for exit", nil) startOnly.SetChecked(current.StartOnly) enabled := widget.NewCheck("Enabled", nil) @@ -49,7 +45,6 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) { widget.NewFormItem("Schedule", scheduleEntry), widget.NewFormItem("Command", commandEntry), widget.NewFormItem("Arguments", argumentsEntry), - widget.NewFormItem("Success exit codes", successExitCodesEntry), widget.NewFormItem("", startOnly), widget.NewFormItem("", enabled), }, @@ -72,10 +67,6 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) { current.Schedule = strings.TrimSpace(scheduleEntry.Text) current.Command = strings.TrimSpace(commandEntry.Text) current.Arguments = strings.TrimSpace(argumentsEntry.Text) - current.SuccessExitCodes = strings.TrimSpace(successExitCodesEntry.Text) - if current.SuccessExitCodes == "" { - current.SuccessExitCodes = "0" - } current.StartOnly = startOnly.Checked current.Enabled = enabled.Checked // The dialog only edits durable configuration. Runtime status is diff --git a/src/ui/jobs_view.go b/src/ui/jobs_view.go index 452604f..1b8bab3 100644 --- a/src/ui/jobs_view.go +++ b/src/ui/jobs_view.go @@ -60,7 +60,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) { scheduleLabel := newJobDetailLabel(jobs[selected].Schedule) commandLabel := newJobDetailLabel(jobs[selected].Command) argumentsLabel := newJobDetailLabel(jobs[selected].Arguments) - successExitCodesLabel := newJobDetailLabel(app.DisplaySuccessExitCodes(jobs[selected].SuccessExitCodes)) runModeLabel := newJobDetailLabel(app.DisplayRunMode(jobs[selected])) selectedRuntime := runtimeFor(selected) lastRunLabel := newJobDetailLabel(selectedRuntime.LastRun) @@ -93,7 +92,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) { scheduleLabel.SetText("") commandLabel.SetText("") argumentsLabel.SetText("") - successExitCodesLabel.SetText("") runModeLabel.SetText("") lastRunLabel.SetText("") nextRunLabel.SetText("") @@ -110,7 +108,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) { scheduleLabel.SetText(current.Schedule) commandLabel.SetText(current.Command) argumentsLabel.SetText(app.DisplayArguments(current.Arguments)) - successExitCodesLabel.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes)) runModeLabel.SetText(app.DisplayRunMode(current)) lastRunLabel.SetText(rt.LastRun) nextRunLabel.SetText(rt.NextRun) @@ -338,7 +335,6 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) { detailRow("Schedule", scheduleLabel), detailRow("Command", commandLabel), detailRow("Arguments", argumentsLabel), - detailRow("Success exit codes", successExitCodesLabel), detailRow("Run mode", runModeLabel), detailRow("Last run", lastRunLabel), detailRow("Next run", nextRunLabel),