cron.go
169 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package internal
import (
"fmt"
"os/exec"
"strings"
)
// crontab backup path inside code-server (on persistent volume)
const crontabBackup = "/home/coder/data/.crontab"
// CronJob represents a parsed crontab entry.
type CronJob struct {
Schedule string // e.g. "*/5 * * * *"
Command string // e.g. "cd /home/coder/repos/myapp && go test ./..."
Comment string // from preceding # comment line
}
// Describe returns a human-readable schedule description for common patterns.
func (j CronJob) Describe() string {
switch j.Schedule {
case "* * * * *":
return "Every minute"
case "*/2 * * * *":
return "Every 2 min"
case "*/5 * * * *":
return "Every 5 min"
case "*/10 * * * *":
return "Every 10 min"
case "*/15 * * * *":
return "Every 15 min"
case "*/30 * * * *":
return "Every 30 min"
case "0 * * * *":
return "Hourly"
case "0 */2 * * *":
return "Every 2 hours"
case "0 */6 * * *":
return "Every 6 hours"
case "0 */12 * * *":
return "Every 12 hours"
case "0 0 * * *":
return "Daily"
case "0 0 * * 0":
return "Weekly"
case "0 0 1 * *":
return "Monthly"
}
return j.Schedule
}
// ListCronJobs reads cron entries from the code-server container.
func ListCronJobs() []CronJob {
output, err := CoderExec("crontab -l 2>/dev/null")
if err != nil || strings.TrimSpace(output) == "" || strings.Contains(output, "no crontab") {
return nil
}
var jobs []CronJob
lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
var comment string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "#") {
comment = strings.TrimSpace(strings.TrimPrefix(line, "#"))
continue
}
parts := strings.Fields(line)
if len(parts) < 6 {
comment = ""
continue
}
jobs = append(jobs, CronJob{
Schedule: strings.Join(parts[:5], " "),
Command: strings.Join(parts[5:], " "),
Comment: comment,
})
comment = ""
}
return jobs
}
// AddCronJob appends a new entry to the code-server crontab.
func AddCronJob(schedule, command, comment string) error {
existing, _ := CoderExec("crontab -l 2>/dev/null")
if strings.Contains(existing, "no crontab") {
existing = ""
}
existing = strings.TrimRight(existing, "\n")
var entry string
if comment != "" {
entry = "# " + comment + "\n" + schedule + " " + command
} else {
entry = schedule + " " + command
}
var full string
if existing != "" {
full = existing + "\n" + entry + "\n"
} else {
full = entry + "\n"
}
_, err := CoderExec(fmt.Sprintf("cat <<'CRONTAB_EOF' | crontab -\n%sCRONTAB_EOF", full))
if err != nil {
return err
}
persistCrontab()
return nil
}
// RemoveCronJob removes a cron entry matching the given schedule and command.
func RemoveCronJob(schedule, command string) error {
output, _ := CoderExec("crontab -l 2>/dev/null")
if output == "" || strings.Contains(output, "no crontab") {
return nil
}
target := schedule + " " + command
lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
var keep []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == target {
// Also remove preceding comment line
if len(keep) > 0 && strings.HasPrefix(strings.TrimSpace(keep[len(keep)-1]), "#") {
keep = keep[:len(keep)-1]
}
continue
}
keep = append(keep, line)
}
result := strings.TrimRight(strings.Join(keep, "\n"), "\n")
if result == "" {
CoderExec("crontab -r 2>/dev/null")
persistCrontab()
return nil
}
_, err := CoderExec(fmt.Sprintf("cat <<'CRONTAB_EOF' | crontab -\n%s\nCRONTAB_EOF", result))
if err == nil {
persistCrontab()
}
return err
}
// EnsureCronDaemon installs cron if needed, starts the daemon, and restores
// persisted crontab from the data volume (survives container restarts).
func EnsureCronDaemon() {
exec.Command("docker", "exec", "-u", "root", coderContainer,
"sh", "-c",
"which cron >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq cron >/dev/null 2>&1); "+
"pgrep -x cron >/dev/null 2>&1 || cron 2>/dev/null; true").Run()
// Restore crontab from persistent backup if current crontab is empty
CoderExec(fmt.Sprintf(
"crontab -l >/dev/null 2>&1 || (test -f %s && crontab %s); true",
crontabBackup, crontabBackup))
}
// persistCrontab saves the current crontab to the data volume for persistence.
func persistCrontab() {
CoderExec(fmt.Sprintf("crontab -l > %s 2>/dev/null || rm -f %s", crontabBackup, crontabBackup))
}