Compare commits
No commits in common. "5bae155b3b3dd84a49d89960dec4376c145b5b15" and "a87c5683354a94e6d049e6d40f18a7678f133490" have entirely different histories.
5bae155b3b
...
a87c568335
55
.idea/remote-targets.xml
generated
55
.idea/remote-targets.xml
generated
@ -1,55 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RemoteTargetsManager">
|
|
||||||
<targets>
|
|
||||||
<target name="golang-dev:1.21-bookworm-user" type="docker" uuid="5b79636b-3db9-4e2a-9fd3-03277653ae58">
|
|
||||||
<config>
|
|
||||||
<option name="targetPlatform">
|
|
||||||
<TargetPlatform />
|
|
||||||
</option>
|
|
||||||
<option name="buildImageConfig">
|
|
||||||
<BuildImageConfig>
|
|
||||||
<option name="dockerFile" value="docker/bookworm/Dockerfile" />
|
|
||||||
</BuildImageConfig>
|
|
||||||
</option>
|
|
||||||
<option name="buildNotPull" value="false" />
|
|
||||||
<option name="containerConfig">
|
|
||||||
<ContainerConfig>
|
|
||||||
<option name="runCliOptions" value="-u "1000:1000" --rm" />
|
|
||||||
</ContainerConfig>
|
|
||||||
</option>
|
|
||||||
<option name="pullImageConfig">
|
|
||||||
<PullImageConfig>
|
|
||||||
<option name="tagToPull" value="golang-dev:1.21-bookworm-user" />
|
|
||||||
</PullImageConfig>
|
|
||||||
</option>
|
|
||||||
</config>
|
|
||||||
<ContributedStateBase type="GoLanguageRuntime">
|
|
||||||
<config>
|
|
||||||
<option name="compiledExecutablesVolume">
|
|
||||||
<VolumeState>
|
|
||||||
<option name="targetSpecificBits">
|
|
||||||
<map>
|
|
||||||
<entry key="mountAsVolume" value="false" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</VolumeState>
|
|
||||||
</option>
|
|
||||||
<option name="goPath" value="/go" />
|
|
||||||
<option name="goRoot" value="/usr/local/go/bin/go" />
|
|
||||||
<option name="goVersion" value="go1.21.5 linux/amd64" />
|
|
||||||
<option name="projectSourcesVolume">
|
|
||||||
<VolumeState>
|
|
||||||
<option name="targetSpecificBits">
|
|
||||||
<map>
|
|
||||||
<entry key="mountAsVolume" value="false" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</VolumeState>
|
|
||||||
</option>
|
|
||||||
</config>
|
|
||||||
</ContributedStateBase>
|
|
||||||
</target>
|
|
||||||
</targets>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
17
Makefile
17
Makefile
@ -1,13 +1,6 @@
|
|||||||
DESTDIR = /usr/local/bin
|
DESTDIR = /usr/local/bin
|
||||||
|
|
||||||
installs = install-dir
|
all: wingmate dummy oneshot spawner starter pidproxy exec
|
||||||
programs = wingmate pidproxy exec
|
|
||||||
ifdef TEST_BUILD
|
|
||||||
programs += oneshot spawner starter dummy
|
|
||||||
installs += install-test
|
|
||||||
endif
|
|
||||||
|
|
||||||
all: ${programs}
|
|
||||||
|
|
||||||
wingmate:
|
wingmate:
|
||||||
$(MAKE) -C cmd/wingmate all
|
$(MAKE) -C cmd/wingmate all
|
||||||
@ -39,16 +32,12 @@ clean:
|
|||||||
$(MAKE) -C cmd/experiment/spawner clean
|
$(MAKE) -C cmd/experiment/spawner clean
|
||||||
$(MAKE) -C cmd/experiment/starter clean
|
$(MAKE) -C cmd/experiment/starter clean
|
||||||
|
|
||||||
install: ${installs}
|
install:
|
||||||
|
install -d ${DESTDIR}
|
||||||
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
|
||||||
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
|
||||||
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
|
||||||
|
|
||||||
install-test:
|
|
||||||
$(MAKE) -C cmd/experiment/dummy DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/experiment/dummy DESTDIR=${DESTDIR} install
|
||||||
$(MAKE) -C cmd/experiment/oneshot DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/experiment/oneshot DESTDIR=${DESTDIR} install
|
||||||
$(MAKE) -C cmd/experiment/spawner DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/experiment/spawner DESTDIR=${DESTDIR} install
|
||||||
$(MAKE) -C cmd/experiment/starter DESTDIR=${DESTDIR} install
|
$(MAKE) -C cmd/experiment/starter DESTDIR=${DESTDIR} install
|
||||||
|
|
||||||
install-dir:
|
|
||||||
install -d ${DESTDIR}
|
|
||||||
|
|||||||
137
README.md
137
README.md
@ -1,139 +1,2 @@
|
|||||||
# wingmate
|
# wingmate
|
||||||
|
|
||||||
Wingmate is a process manager for services. It works like init. It starts and restarts services.
|
|
||||||
It also has cron feature. It is designed to run in a container/docker.
|
|
||||||
The Wingmate binary do not need any external dependency.
|
|
||||||
Just copy the binary, and exec from the entry point script.
|
|
||||||
|
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
## Binaries
|
|
||||||
|
|
||||||
There are three binaries in this project: `wingmate`, `wmpidproxy`, and `wmexec`.
|
|
||||||
|
|
||||||
`wingmate` is the core binary. It reads config, starts, restarts services. It also
|
|
||||||
runs cron. Read the [configuration](#configuration) section for files needed to run
|
|
||||||
`wingmate`.
|
|
||||||
|
|
||||||
`wmpidproxy` is a helper binary for monitoring _legacy style_ service (fork, exit
|
|
||||||
initial proces, and continue in background). Read [here](#wingmate-pid-proxy-binary)
|
|
||||||
for further details about `wmpidproxy`.
|
|
||||||
|
|
||||||
`wmexec` is a helper binary for running process in different `user` or `group`.
|
|
||||||
It also useful for setting the process as process group leader.
|
|
||||||
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
|
|
||||||
|
|
||||||
## Building a container image based on wingmate image in Docker Hub
|
|
||||||
|
|
||||||
Wingmate has no dependency other than `alpine` base image, so you just need to copy
|
|
||||||
the binaries directly. If you have built your application into an `alpine` based image,
|
|
||||||
all you need to do is copy whichever binary you need, crontab file (if you use cron)
|
|
||||||
and add some shell script to glue them together. Here is a Dockerfile example.
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
# Dockerfile
|
|
||||||
FROM suyono/wingmate:alpine as source
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
ADD --chmod=755 wingmate/ /etc/wingmate/
|
|
||||||
ADD --chmod=755 entry.sh /usr/local/bin/entry.sh
|
|
||||||
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/wingmate
|
|
||||||
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/wmpidproxy
|
|
||||||
COPY --from=source /usr/local/bin/wmexec /usr/local/bin/wmexec
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
|
||||||
CMD [ "/usr/local/bin/wingmate" ]
|
|
||||||
```
|
|
||||||
You can find some examples for shell script in [alpine docker](docker/alpine) and
|
|
||||||
[bookworm docker](docker/bookworm).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
When `wingmate` binary starts, it will look for some files. By default, it will
|
|
||||||
try to read the content of `/etc/wingmate` directory. You can change the directory
|
|
||||||
where it reads by setting `WINGMATE_CONFIG_PATH` environment variable. The structure
|
|
||||||
inside the config path should look like this.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
/etc
|
|
||||||
└── wingmate
|
|
||||||
├── crontab
|
|
||||||
├── crontab.d
|
|
||||||
│ ├── cron1.sh
|
|
||||||
│ ├── cron2.sh
|
|
||||||
│ └── cron3.sh
|
|
||||||
└── service
|
|
||||||
├── one.sh
|
|
||||||
└── spawner.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
First, `wingmate` will try to read the content of `service` directory. The content of
|
|
||||||
this directory should be executables (either shell scripts or binaries). The wingmate
|
|
||||||
will run every executable in `service` directory without going into any subdirectory.
|
|
||||||
|
|
||||||
Next, `wingmate` will read the `crontab` file. `wingmate` expects the `crontab` file using
|
|
||||||
common UNIX crontab file format. Something like this
|
|
||||||
|
|
||||||
```shell
|
|
||||||
┌───────────── minute (0–59)
|
|
||||||
│ ┌───────────── hour (0–23)
|
|
||||||
│ │ ┌───────────── day of the month (1–31)
|
|
||||||
│ │ │ ┌───────────── month (1–12)
|
|
||||||
│ │ │ │ ┌───────────── day of the week (0–6) (Sunday to Saturday)
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
* * * * * <commad or shell script or binary>
|
|
||||||
```
|
|
||||||
|
|
||||||
The command part only support simple command and arguments. Shell expression is not supported
|
|
||||||
yet. It is recommended to write a shell script and put the path to shell script in
|
|
||||||
the command part.
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
## Wingmate PID Proxy binary
|
|
||||||
|
|
||||||
`wingmate` works by monitoring its direct children process. When it sees one of its children
|
|
||||||
exited, it will start the child process again.
|
|
||||||
|
|
||||||
Sometimes you find some services work by running in the background. It means it forks a new
|
|
||||||
process, disconnect the new child from terminal, exit the parent process, and continue
|
|
||||||
running in the child process. This kind of service usually write its background process
|
|
||||||
PID in a pid file.
|
|
||||||
|
|
||||||
To monitor the background services, `wingmate` utilizes `wmpidproxy`. `wmpidproxy` runs
|
|
||||||
in foreground in-place of the background service. It also periodically check whether the
|
|
||||||
background service is still running, in current implementation it checks every second.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wmpidproxy --pid-file <path to pid file> -- <background service binary/start script>
|
|
||||||
```
|
|
||||||
#### Example
|
|
||||||
Running sshd background with `wingmate` and `wmpidproxy`: [here](example/ssh-docker)
|
|
||||||
|
|
||||||
#### Note
|
|
||||||
|
|
||||||
It is recommended to configure services to run in foreground if they support it. When services
|
|
||||||
running in foreground, they are running as direct children process of `wingmate`.
|
|
||||||
`wingmate` monitors children process effectively. Whenever a child process exited/terminated,
|
|
||||||
`wingmate` will start it again quickly. Running in foreground also removes the overhead of running
|
|
||||||
`wmpidproxy` together with the service.
|
|
||||||
|
|
||||||
## Wingmate Exec binary
|
|
||||||
|
|
||||||
`wingmate` runs all the services as its children using the same `uid`, `gid`, and in the
|
|
||||||
same process group. You can use `wmexec` to run service in different `uid`, `gid`, or make
|
|
||||||
the service process as its own process group leader.
|
|
||||||
|
|
||||||
#### Syntax
|
|
||||||
|
|
||||||
```shell
|
|
||||||
wmexec [--user <uid>[:<gid>]] [--setsid] -- <target executable>
|
|
||||||
```
|
|
||||||
| Option | Parameter | Description |
|
|
||||||
|----------|-----------|----------------------------------------------------------------------------------------------------------|
|
|
||||||
| --user | uid[:gid] | Set the real user ID and the real group id. Uid and Gid can be either in numeric form or in name form |
|
|
||||||
| --setsid | | Set the process become the leader of its own process group, effectively detaching from parent's terminal |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
You can find example for `wmexec` in [here](docker/alpine/etc/wingmate) and [here](docker/bookworm/etc/wingmate)
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -94,13 +92,13 @@ func pidProxy(cmd *cobra.Command, args []string) error {
|
|||||||
err error
|
err error
|
||||||
pid int
|
pid int
|
||||||
sc chan os.Signal
|
sc chan os.Signal
|
||||||
t *time.Ticker
|
t *time.Timer
|
||||||
)
|
)
|
||||||
|
|
||||||
sc = make(chan os.Signal, 1)
|
sc = make(chan os.Signal, 1)
|
||||||
signal.Notify(sc, unix.SIGTERM)
|
signal.Notify(sc, unix.SIGTERM)
|
||||||
|
|
||||||
t = time.NewTicker(time.Second)
|
t = time.NewTimer(time.Second)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
for {
|
for {
|
||||||
@ -132,6 +130,8 @@ func readPid(pidFile string) (int, error) {
|
|||||||
var (
|
var (
|
||||||
file *os.File
|
file *os.File
|
||||||
err error
|
err error
|
||||||
|
buf []byte
|
||||||
|
n int
|
||||||
pid64 int64
|
pid64 int64
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,15 +142,18 @@ func readPid(pidFile string) (int, error) {
|
|||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
buf = make([]byte, 1024)
|
||||||
if scanner.Scan() {
|
n, err = file.Read(buf)
|
||||||
if pid64, err = strconv.ParseInt(scanner.Text(), 10, 64); err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return int(pid64), nil
|
|
||||||
} else {
|
pid64, err = strconv.ParseInt(string(buf[:n]), 10, 64)
|
||||||
return 0, errors.New("invalid scanner")
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return int(pid64), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startProcess(arg0 string, args ...string) {
|
func startProcess(arg0 string, args ...string) {
|
||||||
|
|||||||
@ -1,53 +1,60 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.suyono.dev/suyono/wingmate/config"
|
"gitea.suyono.dev/suyono/wingmate/config"
|
||||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
"gitea.suyono.dev/suyono/wingmate/task"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type wPath struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p wPath) Path() string {
|
||||||
|
return p.path
|
||||||
|
}
|
||||||
|
|
||||||
type wConfig struct {
|
type wConfig struct {
|
||||||
tasks *task.Tasks
|
services []wminit.Path
|
||||||
|
cron []wminit.Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *wConfig) Tasks() wminit.Tasks {
|
func (c wConfig) Services() []wminit.Path {
|
||||||
return c.tasks
|
return c.services
|
||||||
}
|
}
|
||||||
|
|
||||||
func convert(cfg *config.Config) *wConfig {
|
func (c wConfig) Cron() []wminit.Cron {
|
||||||
retval := &wConfig{
|
return c.cron
|
||||||
tasks: task.NewTasks(),
|
}
|
||||||
|
|
||||||
|
type wCron struct {
|
||||||
|
iCron *config.Cron
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c wCron) TimeToRun(now time.Time) bool {
|
||||||
|
return c.iCron.TimeToRun(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c wCron) Command() wminit.Path {
|
||||||
|
return wPath{
|
||||||
|
path: c.iCron.Command(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert(cfg *config.Config) wConfig {
|
||||||
|
retval := wConfig{
|
||||||
|
services: make([]wminit.Path, 0, len(cfg.ServicePaths)),
|
||||||
|
cron: make([]wminit.Cron, 0, len(cfg.Cron)),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range cfg.ServicePaths {
|
for _, s := range cfg.ServicePaths {
|
||||||
retval.tasks.AddV0Service(s)
|
retval.services = append(retval.services, wPath{path: s})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var schedule task.CronSchedule
|
|
||||||
for _, c := range cfg.Cron {
|
for _, c := range cfg.Cron {
|
||||||
schedule.Minute = convertSchedule(c.Minute)
|
retval.cron = append(retval.cron, wCron{iCron: c})
|
||||||
schedule.Hour = convertSchedule(c.Hour)
|
|
||||||
schedule.DoM = convertSchedule(c.DoM)
|
|
||||||
schedule.Month = convertSchedule(c.Month)
|
|
||||||
schedule.DoW = convertSchedule(c.DoW)
|
|
||||||
|
|
||||||
retval.tasks.AddV0Cron(schedule, c.Command)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertSchedule(cfg config.CronTimeSpec) task.CronTimeSpec {
|
|
||||||
switch v := cfg.(type) {
|
|
||||||
case *config.SpecAny:
|
|
||||||
return task.NewCronAnySpec()
|
|
||||||
case *config.SpecExact:
|
|
||||||
return task.NewCronExactSpec(v.Value())
|
|
||||||
case *config.SpecMultiOccurrence:
|
|
||||||
return task.NewCronMultiOccurrenceSpec(v.Values()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("invalid conversion")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,28 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
serviceDir = "service"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
func setup(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
|
||||||
t.Fatal("setup", err)
|
|
||||||
}
|
|
||||||
viper.Set(EnvConfigPath, configDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tear(t *testing.T) {
|
|
||||||
if err := os.RemoveAll(configDir); err != nil {
|
|
||||||
t.Fatal("tear", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRead(t *testing.T) {
|
func TestRead(t *testing.T) {
|
||||||
|
|
||||||
type testEntry struct {
|
type testEntry struct {
|
||||||
@ -39,6 +17,26 @@ func TestRead(t *testing.T) {
|
|||||||
testFunc func(t *testing.T)
|
testFunc func(t *testing.T)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
configDir string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
const serviceDir = "service"
|
||||||
|
|
||||||
|
setup := func(t *testing.T) {
|
||||||
|
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
||||||
|
t.Fatal("setup", err)
|
||||||
|
}
|
||||||
|
viper.Set(EnvConfigPath, configDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
tear := func(t *testing.T) {
|
||||||
|
if err = os.RemoveAll(configDir); err != nil {
|
||||||
|
t.Fatal("tear", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mkSvcDir := func(t *testing.T) {
|
mkSvcDir := func(t *testing.T) {
|
||||||
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
|
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
|
||||||
t.Fatal("create dir", err)
|
t.Fatal("create dir", err)
|
||||||
|
|||||||
@ -4,34 +4,46 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.suyono.dev/suyono/wingmate"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CronExactSpec interface {
|
||||||
|
CronTimeSpec
|
||||||
|
Value() uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronMultipleOccurrenceSpec interface {
|
||||||
|
CronTimeSpec
|
||||||
|
Values() []uint8
|
||||||
|
}
|
||||||
|
|
||||||
type CronTimeSpec interface {
|
type CronTimeSpec interface {
|
||||||
//Type() wingmate.CronTimeType
|
Type() wingmate.CronTimeType
|
||||||
//Match(uint8) bool
|
Match(uint8) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cron struct {
|
type Cron struct {
|
||||||
Minute CronTimeSpec
|
minute CronTimeSpec
|
||||||
Hour CronTimeSpec
|
hour CronTimeSpec
|
||||||
DoM CronTimeSpec
|
dom CronTimeSpec
|
||||||
Month CronTimeSpec
|
month CronTimeSpec
|
||||||
DoW CronTimeSpec
|
dow CronTimeSpec
|
||||||
Command string
|
command string
|
||||||
|
lastRun time.Time
|
||||||
|
hasRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type cronField int
|
type cronField int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CrontabEntryRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
CrontabEntryRegex = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||||
CrontabCommentLineRegexPattern = `^\s*#.*$`
|
CrontabSubmatchLen = 7
|
||||||
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
|
|
||||||
CrontabSubMatchLen = 7
|
|
||||||
|
|
||||||
minute cronField = iota
|
minute cronField = iota
|
||||||
hour
|
hour
|
||||||
@ -40,22 +52,21 @@ const (
|
|||||||
dow
|
dow
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
|
|
||||||
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
|
|
||||||
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
|
|
||||||
)
|
|
||||||
|
|
||||||
func readCrontab(path string) ([]*Cron, error) {
|
func readCrontab(path string) ([]*Cron, error) {
|
||||||
var (
|
var (
|
||||||
file *os.File
|
file *os.File
|
||||||
err error
|
err error
|
||||||
scanner *bufio.Scanner
|
scanner *bufio.Scanner
|
||||||
line string
|
line string
|
||||||
|
re *regexp.Regexp
|
||||||
parts []string
|
parts []string
|
||||||
retval []*Cron
|
retval []*Cron
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if file, err = os.Open(path); err != nil {
|
if file, err = os.Open(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -68,48 +79,41 @@ func readCrontab(path string) ([]*Cron, error) {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line = scanner.Text()
|
line = scanner.Text()
|
||||||
|
|
||||||
if crontabCommentLineRegex.MatchString(line) {
|
parts = re.FindStringSubmatch(line)
|
||||||
continue
|
if len(parts) != CrontabSubmatchLen {
|
||||||
}
|
|
||||||
|
|
||||||
parts = crontabCommentSuffixRegex.FindStringSubmatch(line)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
line = parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = crontabEntryRegex.FindStringSubmatch(line)
|
|
||||||
if len(parts) != CrontabSubMatchLen {
|
|
||||||
wingmate.Log().Error().Msgf("invalid entry %s", line)
|
wingmate.Log().Error().Msgf("invalid entry %s", line)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &Cron{}
|
c := &Cron{
|
||||||
|
hasRun: false,
|
||||||
|
}
|
||||||
if err = c.setField(minute, parts[1]); err != nil {
|
if err = c.setField(minute, parts[1]); err != nil {
|
||||||
wingmate.Log().Error().Msgf("error parsing Minute field %+v", err)
|
wingmate.Log().Error().Msgf("error parsing minute field %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.setField(hour, parts[2]); err != nil {
|
if err = c.setField(hour, parts[2]); err != nil {
|
||||||
wingmate.Log().Error().Msgf("error parsing Hour field %+v", err)
|
wingmate.Log().Error().Msgf("error parsing hour field %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.setField(dom, parts[3]); err != nil {
|
if err = c.setField(dom, parts[3]); err != nil {
|
||||||
wingmate.Log().Error().Msgf("error parsing Day of Month field %+v", err)
|
wingmate.Log().Error().Msgf("error parsing day of month field %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.setField(month, parts[4]); err != nil {
|
if err = c.setField(month, parts[4]); err != nil {
|
||||||
wingmate.Log().Error().Msgf("error parsing Month field %+v", err)
|
wingmate.Log().Error().Msgf("error parsing month field %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.setField(dow, parts[5]); err != nil {
|
if err = c.setField(dow, parts[5]); err != nil {
|
||||||
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err)
|
wingmate.Log().Error().Msgf("error parsing day of week field %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Command = parts[6]
|
c.command = parts[6]
|
||||||
|
|
||||||
retval = append(retval, c)
|
retval = append(retval, c)
|
||||||
}
|
}
|
||||||
@ -117,6 +121,35 @@ func readCrontab(path string) ([]*Cron, error) {
|
|||||||
return retval, nil
|
return retval, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cron) Command() string {
|
||||||
|
return c.command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cron) TimeToRun(now time.Time) bool {
|
||||||
|
if c.minute.Match(uint8(now.Minute())) &&
|
||||||
|
c.hour.Match(uint8(now.Hour())) &&
|
||||||
|
c.dom.Match(uint8(now.Day())) &&
|
||||||
|
c.month.Match(uint8(now.Month())) &&
|
||||||
|
c.dow.Match(uint8(now.Weekday())) {
|
||||||
|
|
||||||
|
if c.hasRun {
|
||||||
|
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
c.lastRun = now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
c.lastRun = now
|
||||||
|
c.hasRun = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type fieldRange struct {
|
type fieldRange struct {
|
||||||
min int
|
min int
|
||||||
max int
|
max int
|
||||||
@ -149,25 +182,25 @@ func (c *Cron) setField(field cronField, input string) error {
|
|||||||
switch field {
|
switch field {
|
||||||
case minute:
|
case minute:
|
||||||
fr = newRange(0, 59)
|
fr = newRange(0, 59)
|
||||||
cField = &c.Minute
|
cField = &c.minute
|
||||||
case hour:
|
case hour:
|
||||||
fr = newRange(0, 23)
|
fr = newRange(0, 23)
|
||||||
cField = &c.Hour
|
cField = &c.hour
|
||||||
case dom:
|
case dom:
|
||||||
fr = newRange(1, 31)
|
fr = newRange(1, 31)
|
||||||
cField = &c.DoM
|
cField = &c.dom
|
||||||
case month:
|
case month:
|
||||||
fr = newRange(1, 12)
|
fr = newRange(1, 12)
|
||||||
cField = &c.Month
|
cField = &c.month
|
||||||
case dow:
|
case dow:
|
||||||
fr = newRange(0, 6)
|
fr = newRange(0, 6)
|
||||||
cField = &c.DoW
|
cField = &c.dow
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid cron field descriptor")
|
return errors.New("invalid cron field descriptor")
|
||||||
}
|
}
|
||||||
|
|
||||||
if input == "*" {
|
if input == "*" {
|
||||||
*cField = &SpecAny{}
|
*cField = &specAny{}
|
||||||
} else if strings.HasPrefix(input, "*/") {
|
} else if strings.HasPrefix(input, "*/") {
|
||||||
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
|
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
|
||||||
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
|
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
|
||||||
@ -184,7 +217,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
|||||||
current += parsed
|
current += parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
*cField = &SpecMultiOccurrence{
|
*cField = &specMultiOccurrence{
|
||||||
values: multi,
|
values: multi,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -204,7 +237,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
|||||||
multi = append(multi, parsed)
|
multi = append(multi, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
*cField = &SpecMultiOccurrence{
|
*cField = &specMultiOccurrence{
|
||||||
values: multi,
|
values: multi,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -217,7 +250,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
|||||||
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
*cField = &SpecExact{
|
*cField = &specExact{
|
||||||
value: parsed,
|
value: parsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,21 +259,51 @@ func (c *Cron) setField(field cronField, input string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpecAny struct{}
|
type specAny struct{}
|
||||||
|
|
||||||
type SpecExact struct {
|
func (a *specAny) Type() wingmate.CronTimeType {
|
||||||
|
return wingmate.Any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *specAny) Match(u uint8) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type specExact struct {
|
||||||
value uint8
|
value uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *SpecExact) Value() uint8 {
|
func (e *specExact) Type() wingmate.CronTimeType {
|
||||||
|
return wingmate.Exact
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *specExact) Match(u uint8) bool {
|
||||||
|
return u == e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *specExact) Value() uint8 {
|
||||||
return e.value
|
return e.value
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpecMultiOccurrence struct {
|
type specMultiOccurrence struct {
|
||||||
values []uint8
|
values []uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SpecMultiOccurrence) Values() []uint8 {
|
func (m *specMultiOccurrence) Type() wingmate.CronTimeType {
|
||||||
|
return wingmate.MultipleOccurrence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *specMultiOccurrence) Match(u uint8) bool {
|
||||||
|
for _, v := range m.values {
|
||||||
|
if v == u {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *specMultiOccurrence) Values() []uint8 {
|
||||||
out := make([]uint8, len(m.values))
|
out := make([]uint8, len(m.values))
|
||||||
copy(out, m.values)
|
copy(out, m.values)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.suyono.dev/suyono/wingmate"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
crontabFileName = "crontab"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCrontab(t *testing.T) {
|
|
||||||
type testEntry struct {
|
|
||||||
name string
|
|
||||||
crontab string
|
|
||||||
wantErr bool
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = wingmate.NewLog(os.Stderr)
|
|
||||||
tests := []testEntry{
|
|
||||||
{
|
|
||||||
name: "positive",
|
|
||||||
crontab: crontabTestCase0,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with comment",
|
|
||||||
crontab: crontabTestCase1,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "various values",
|
|
||||||
crontab: crontabTestCase2,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "failed to parse",
|
|
||||||
crontab: crontabTestCase3,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setup(t)
|
|
||||||
defer tear(t)
|
|
||||||
|
|
||||||
writeCrontab(t, tt.crontab)
|
|
||||||
|
|
||||||
cfg, err := Read()
|
|
||||||
if tt.wantErr != (err != nil) {
|
|
||||||
t.Fatalf("wantErr is %v but err is %+v", tt.wantErr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("cfg: %+v", cfg)
|
|
||||||
for _, c := range cfg.Cron {
|
|
||||||
t.Logf("%+v", c)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeCrontab(t *testing.T, content string) {
|
|
||||||
var (
|
|
||||||
f *os.File
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if f, err = os.Create(filepath.Join(configDir, crontabFileName)); err != nil {
|
|
||||||
t.Fatal("create crontab file", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = f.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if _, err = f.Write([]byte(content)); err != nil {
|
|
||||||
t.Fatal("writing crontab file", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const crontabTestCase0 = `* * * * * /path/to/executable`
|
|
||||||
const crontabTestCase1 = `# this is a comment
|
|
||||||
## comment with space
|
|
||||||
* * * * * /path/to/executable
|
|
||||||
* * * * * /path/to/executable # comment as a suffix
|
|
||||||
`
|
|
||||||
|
|
||||||
const crontabTestCase2 = `# first comment
|
|
||||||
*/5 13 3,5,7 * * /path/to/executable`
|
|
||||||
|
|
||||||
const crontabTestCase3 = `a 13 3,5,7 * * /path/to/executable
|
|
||||||
*/5 a 3,5,7 * * /path/to/executable
|
|
||||||
*/5 13 a * * /path/to/executable
|
|
||||||
*/5 13 3,5,7 a * /path/to/executable
|
|
||||||
*/5 13 3,5,7 * a /path/to/executable
|
|
||||||
*/x 13 3,5,7 * a /path/to/executable
|
|
||||||
76 13 3,5,7 * a /path/to/executable
|
|
||||||
*/75 13 3,5,7 * a /path/to/executable
|
|
||||||
*/5 13 3,x,7 * a /path/to/executable
|
|
||||||
*/5 13 3,5,67 * a /path/to/executable
|
|
||||||
*/5 13 * * /path/to/executable
|
|
||||||
*/5 13 3,5,7 * * /path/to/executable`
|
|
||||||
|
|
||||||
func TestSpecExact(t *testing.T) {
|
|
||||||
var val uint8 = 45
|
|
||||||
s := SpecExact{
|
|
||||||
value: val,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, val, s.Value())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSpecMulti(t *testing.T) {
|
|
||||||
val := []uint8{3, 5, 7, 15}
|
|
||||||
s := SpecMultiOccurrence{
|
|
||||||
values: val,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, val, s.Values())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidField(t *testing.T) {
|
|
||||||
c := &Cron{}
|
|
||||||
assert.NotNil(t, c.setField(cronField(99), "x"))
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ FROM golang:1.21-alpine as builder
|
|||||||
|
|
||||||
ADD . /root/wingmate
|
ADD . /root/wingmate
|
||||||
WORKDIR /root/wingmate/
|
WORKDIR /root/wingmate/
|
||||||
ARG TEST_BUILD
|
|
||||||
RUN apk add make build-base && CGO_ENABLED=1 make all && make DESTDIR=/usr/local/bin/wingmate install
|
RUN apk add make build-base && CGO_ENABLED=1 make all && make DESTDIR=/usr/local/bin/wingmate install
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ FROM golang:1.21-bookworm as builder
|
|||||||
|
|
||||||
ADD . /root/wingmate
|
ADD . /root/wingmate
|
||||||
WORKDIR /root/wingmate/
|
WORKDIR /root/wingmate/
|
||||||
ARG TEST_BUILD
|
|
||||||
RUN make all && make DESTDIR=/usr/local/bin/wingmate install
|
RUN make all && make DESTDIR=/usr/local/bin/wingmate install
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
FROM suyono/wingmate:alpine as source
|
|
||||||
|
|
||||||
FROM alpine:3.19
|
|
||||||
|
|
||||||
RUN apk update && apk add tzdata openssh-server && \
|
|
||||||
ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime && ssh-keygen -A
|
|
||||||
|
|
||||||
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/
|
|
||||||
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/
|
|
||||||
ADD --chmod=755 example/ssh-docker/entry.sh /usr/local/bin/entry.sh
|
|
||||||
ADD --chmod=755 example/ssh-docker/etc /etc
|
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
|
||||||
CMD [ "/usr/local/bin/wingmate" ]
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
|
||||||
exec "$@"
|
|
||||||
else
|
|
||||||
exec /usr/local/bin/wingmate
|
|
||||||
fi
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
exec /usr/local/bin/wmpidproxy --pid-file /var/run/sshd.pid -- /usr/sbin/sshd
|
|
||||||
23
init/cron.go
23
init/cron.go
@ -13,7 +13,7 @@ const (
|
|||||||
cronTag = "cron"
|
cronTag = "cron"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
|
func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -21,40 +21,35 @@ func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
|
|||||||
err error
|
err error
|
||||||
stdout io.ReadCloser
|
stdout io.ReadCloser
|
||||||
stderr io.ReadCloser
|
stderr io.ReadCloser
|
||||||
cmd *exec.Cmd
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Second * 30)
|
ticker := time.NewTicker(time.Second * 30)
|
||||||
cron:
|
cron:
|
||||||
for {
|
for {
|
||||||
if cron.TimeToRun(time.Now()) {
|
if cron.TimeToRun(time.Now()) {
|
||||||
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing")
|
wingmate.Log().Info().Str(cronTag, cron.Command().Path()).Msg("executing")
|
||||||
if len(cron.Command()) == 1 {
|
cmd := exec.Command(cron.Command().Path())
|
||||||
cmd = exec.Command(cron.Command()[0])
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command(cron.Command()[0], cron.Command()[1:]...)
|
|
||||||
}
|
|
||||||
iwg = &sync.WaitGroup{}
|
iwg = &sync.WaitGroup{}
|
||||||
|
|
||||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stdout pipe: %+v", err)
|
wingmate.Log().Error().Str(cronTag, cron.Command().Path()).Msgf("stdout pipe: %+v", err)
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stderr pipe: %+v", err)
|
wingmate.Log().Error().Str(cronTag, cron.Command().Path()).Msgf("stderr pipe: %+v", err)
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
iwg.Add(1)
|
iwg.Add(1)
|
||||||
go i.pipeReader(iwg, stdout, cronTag, cron.Name())
|
go i.pipeReader(iwg, stdout, cronTag, cron.Command().Path())
|
||||||
|
|
||||||
iwg.Add(1)
|
iwg.Add(1)
|
||||||
go i.pipeReader(iwg, stderr, cronTag, cron.Name())
|
go i.pipeReader(iwg, stderr, cronTag, cron.Command().Path())
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Name(), err)
|
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Command().Path(), err)
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
iwg.Wait()
|
iwg.Wait()
|
||||||
@ -64,7 +59,7 @@ cron:
|
|||||||
iwg.Wait()
|
iwg.Wait()
|
||||||
|
|
||||||
if err = cmd.Wait(); err != nil {
|
if err = cmd.Wait(); err != nil {
|
||||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("got error when waiting: %+v", err)
|
wingmate.Log().Error().Str(cronTag, cron.Command().Path()).Msgf("got error when waiting: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
init/init.go
35
init/init.go
@ -6,37 +6,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tasks interface {
|
type Path interface {
|
||||||
List() []Task
|
Path() string
|
||||||
Services() []Task
|
|
||||||
Crones() []CronTask
|
|
||||||
Get(string) (Task, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserGroup interface {
|
type Cron interface {
|
||||||
}
|
Command() Path
|
||||||
|
|
||||||
type TaskStatus interface {
|
|
||||||
}
|
|
||||||
|
|
||||||
type Task interface {
|
|
||||||
Name() string
|
|
||||||
Command() []string
|
|
||||||
Environ() []string
|
|
||||||
Setsid() bool
|
|
||||||
UserGroup() UserGroup
|
|
||||||
Background() bool
|
|
||||||
WorkingDir() string
|
|
||||||
Status() TaskStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronTask interface {
|
|
||||||
Task
|
|
||||||
TimeToRun(time.Time) bool
|
TimeToRun(time.Time) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
Tasks() Tasks
|
Services() []Path
|
||||||
|
Cron() []Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
type Init struct {
|
type Init struct {
|
||||||
@ -68,12 +49,12 @@ func (i *Init) Start() {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
|
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
|
||||||
|
|
||||||
for _, s := range i.config.Tasks().Services() {
|
for _, s := range i.config.Services() {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go i.service(wg, s, signalTrigger)
|
go i.service(wg, s, signalTrigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range i.config.Tasks().Crones() {
|
for _, c := range i.config.Cron() {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go i.cron(wg, c, signalTrigger)
|
go i.cron(wg, c, signalTrigger)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const (
|
|||||||
serviceTag = "service"
|
serviceTag = "service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i *Init) service(wg *sync.WaitGroup, task Task, exitFlag <-chan any) {
|
func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -23,42 +23,37 @@ func (i *Init) service(wg *sync.WaitGroup, task Task, exitFlag <-chan any) {
|
|||||||
stderr io.ReadCloser
|
stderr io.ReadCloser
|
||||||
stdout io.ReadCloser
|
stdout io.ReadCloser
|
||||||
failStatus bool
|
failStatus bool
|
||||||
cmd *exec.Cmd
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
|
wingmate.Log().Info().Str(serviceTag, path.Path()).Msg("stopped")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
service:
|
service:
|
||||||
for {
|
for {
|
||||||
failStatus = false
|
failStatus = false
|
||||||
if len(task.Command()) == 1 {
|
cmd := exec.Command(path.Path())
|
||||||
cmd = exec.Command(task.Command()[0])
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command(task.Command()[0], task.Command()[1:]...)
|
|
||||||
}
|
|
||||||
iwg = &sync.WaitGroup{}
|
iwg = &sync.WaitGroup{}
|
||||||
|
|
||||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stdout pipe: %#v", err)
|
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("stdout pipe: %#v", err)
|
||||||
failStatus = true
|
failStatus = true
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
iwg.Add(1)
|
iwg.Add(1)
|
||||||
go i.pipeReader(iwg, stdout, serviceTag, task.Name())
|
go i.pipeReader(iwg, stdout, serviceTag, path.Path())
|
||||||
|
|
||||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stderr pipe: %#v", err)
|
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("stderr pipe: %#v", err)
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
failStatus = true
|
failStatus = true
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
iwg.Add(1)
|
iwg.Add(1)
|
||||||
go i.pipeReader(iwg, stderr, serviceTag, task.Name())
|
go i.pipeReader(iwg, stderr, serviceTag, path.Path())
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = cmd.Start(); err != nil {
|
||||||
wingmate.Log().Error().Msgf("starting service %s error %#v", task.Name(), err)
|
wingmate.Log().Error().Msgf("starting service %s error %#v", path.Path(), err)
|
||||||
failStatus = true
|
failStatus = true
|
||||||
_ = stdout.Close()
|
_ = stdout.Close()
|
||||||
_ = stderr.Close()
|
_ = stderr.Close()
|
||||||
@ -69,7 +64,7 @@ service:
|
|||||||
iwg.Wait()
|
iwg.Wait()
|
||||||
|
|
||||||
if err = cmd.Wait(); err != nil {
|
if err = cmd.Wait(); err != nil {
|
||||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("got error when waiting: %+v", err)
|
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("got error when waiting: %+v", err)
|
||||||
}
|
}
|
||||||
fail:
|
fail:
|
||||||
if failStatus {
|
if failStatus {
|
||||||
|
|||||||
139
task/cron.go
139
task/cron.go
@ -1,139 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CronSchedule struct {
|
|
||||||
Minute CronTimeSpec
|
|
||||||
Hour CronTimeSpec
|
|
||||||
DoM CronTimeSpec
|
|
||||||
Month CronTimeSpec
|
|
||||||
DoW CronTimeSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronTimeSpec interface {
|
|
||||||
Match(uint8) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronAnySpec struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCronAnySpec() *CronAnySpec {
|
|
||||||
return &CronAnySpec{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cas *CronAnySpec) Match(u uint8) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronExactSpec struct {
|
|
||||||
value uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCronExactSpec(v uint8) *CronExactSpec {
|
|
||||||
return &CronExactSpec{
|
|
||||||
value: v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ces *CronExactSpec) Match(u uint8) bool {
|
|
||||||
return u == ces.value
|
|
||||||
}
|
|
||||||
|
|
||||||
type CronMultiOccurrenceSpec struct {
|
|
||||||
values []uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCronMultiOccurrenceSpec(v ...uint8) *CronMultiOccurrenceSpec {
|
|
||||||
retval := &CronMultiOccurrenceSpec{}
|
|
||||||
if len(v) > 0 {
|
|
||||||
retval.values = make([]uint8, len(v))
|
|
||||||
copy(retval.values, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cms *CronMultiOccurrenceSpec) Match(u uint8) bool {
|
|
||||||
for _, v := range cms.values {
|
|
||||||
if v == u {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cron struct {
|
|
||||||
CronSchedule
|
|
||||||
name string
|
|
||||||
command []string
|
|
||||||
lastRun time.Time
|
|
||||||
hasRun bool //NOTE: make sure initialised as false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Name() string {
|
|
||||||
return c.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Command() []string {
|
|
||||||
retval := make([]string, len(c.command))
|
|
||||||
copy(retval, c.command)
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Environ() []string {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Setsid() bool {
|
|
||||||
panic("not implemented")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) UserGroup() wminit.UserGroup {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Background() bool {
|
|
||||||
panic("not implemented")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) WorkingDir() string {
|
|
||||||
panic("not implemented")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) Status() wminit.TaskStatus {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cron) TimeToRun(now time.Time) bool {
|
|
||||||
if c.Minute.Match(uint8(now.Minute())) &&
|
|
||||||
c.Hour.Match(uint8(now.Hour())) &&
|
|
||||||
c.DoM.Match(uint8(now.Day())) &&
|
|
||||||
c.Month.Match(uint8(now.Month())) &&
|
|
||||||
c.DoW.Match(uint8(now.Weekday())) {
|
|
||||||
|
|
||||||
if c.hasRun {
|
|
||||||
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
c.lastRun = now
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.lastRun = now
|
|
||||||
c.hasRun = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
95
task/task.go
95
task/task.go
@ -1,95 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tasks struct {
|
|
||||||
services []wminit.Task
|
|
||||||
crones []wminit.CronTask
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTasks() *Tasks {
|
|
||||||
return &Tasks{
|
|
||||||
services: make([]wminit.Task, 0),
|
|
||||||
crones: make([]wminit.CronTask, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) AddV0Service(path string) {
|
|
||||||
ts.services = append(ts.services, &Task{
|
|
||||||
name: path,
|
|
||||||
command: []string{path},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) {
|
|
||||||
ts.crones = append(ts.crones, &Cron{
|
|
||||||
CronSchedule: schedule,
|
|
||||||
name: path,
|
|
||||||
command: []string{path},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) List() []wminit.Task {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) Services() []wminit.Task {
|
|
||||||
return ts.services
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) Crones() []wminit.CronTask {
|
|
||||||
return ts.crones
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *Tasks) Get(name string) (wminit.Task, error) {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Task struct {
|
|
||||||
name string
|
|
||||||
command []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Name() string {
|
|
||||||
return t.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Command() []string {
|
|
||||||
retval := make([]string, len(t.command))
|
|
||||||
copy(retval, t.command)
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Environ() []string {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Setsid() bool {
|
|
||||||
panic("not implemented")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) UserGroup() wminit.UserGroup {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Background() bool {
|
|
||||||
panic("not implemented")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) WorkingDir() string {
|
|
||||||
panic("not implemented")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Task) Status() wminit.TaskStatus {
|
|
||||||
panic("not implemented")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user