Compare commits
38 Commits
rework
...
6dd0a8007c
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dd0a8007c | |||
| 6a40403434 | |||
| 98d57cda84 | |||
| 5bae155b3b | |||
| 22fee125bc | |||
| b2668287ae | |||
| 08a040ac72 | |||
| 6df8ba2810 | |||
| 7785b3ec48 | |||
| 70a4d132c3 | |||
| b15066b513 | |||
| e2275ef05e | |||
| 541228bf68 | |||
| a87c568335 | |||
| 574d3deb56 | |||
| 5679faecdb | |||
| 2a4629c017 | |||
| c043c91f0e | |||
| 15a804aa7d | |||
| 653b4ff158 | |||
| 8cf92167df | |||
| 9c74296c27 | |||
| 8a85ad5107 | |||
| ad8499daa5 | |||
| 8704f80d4b | |||
| dbe9dbba9c | |||
| 4ec5750cd5 | |||
| 3dc69325c1 | |||
| 2eae19f64c | |||
| b589fb8f0c | |||
| dd66cb9f1e | |||
| d5eb872b13 | |||
| 9128503da1 | |||
| d9d1fe72d4 | |||
| 2971f5c709 | |||
| bd4ba67ad2 | |||
| eb7bde3cbe | |||
| a4ba011b36 |
@@ -1,10 +1,25 @@
|
||||
{
|
||||
"name": "Golang Dev",
|
||||
"image": "golang-dev:1.21-bookworm-user",
|
||||
"mounts": [
|
||||
{
|
||||
"source": "WingmateGoPath",
|
||||
"target": "/go",
|
||||
"type": "volume"
|
||||
},
|
||||
{
|
||||
"source": "WingmateGolangDevHome",
|
||||
"target": "/home/golang",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go"
|
||||
"golang.go",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-vscode.makefile-tools",
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,2 +1,6 @@
|
||||
/.idea
|
||||
/wingmate
|
||||
/cmd/wingmate/wingmate
|
||||
/cmd/wingmate/version.txt
|
||||
/cmd/pidproxy/pidproxy
|
||||
/cmd/pidproxy/version.txt
|
||||
/cmd/exec/exec
|
||||
/cmd/exec/version.txt
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/wingmate.iml" filepath="$PROJECT_DIR$/.idea/wingmate.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
55
.idea/remote-targets.xml
generated
Normal file
55
.idea/remote-targets.xml
generated
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/wingmate.iml
generated
Normal file
9
.idea/wingmate.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1 +1 @@
|
||||
golang 1.21.4
|
||||
golang 1.21.5
|
||||
|
||||
54
Makefile
Normal file
54
Makefile
Normal file
@@ -0,0 +1,54 @@
|
||||
DESTDIR = /usr/local/bin
|
||||
|
||||
installs = install-dir
|
||||
programs = wingmate pidproxy exec
|
||||
ifdef TEST_BUILD
|
||||
programs += oneshot spawner starter dummy
|
||||
installs += install-test
|
||||
endif
|
||||
|
||||
all: ${programs}
|
||||
|
||||
wingmate:
|
||||
$(MAKE) -C cmd/wingmate all
|
||||
|
||||
pidproxy:
|
||||
$(MAKE) -C cmd/pidproxy all
|
||||
|
||||
exec:
|
||||
$(MAKE) -C cmd/exec all
|
||||
|
||||
dummy:
|
||||
$(MAKE) -C cmd/experiment/dummy all
|
||||
|
||||
oneshot:
|
||||
$(MAKE) -C cmd/experiment/oneshot all
|
||||
|
||||
spawner:
|
||||
$(MAKE) -C cmd/experiment/spawner all
|
||||
|
||||
starter:
|
||||
$(MAKE) -C cmd/experiment/starter all
|
||||
|
||||
clean:
|
||||
$(MAKE) -C cmd/wingmate clean
|
||||
$(MAKE) -C cmd/pidproxy clean
|
||||
$(MAKE) -C cmd/exec clean
|
||||
$(MAKE) -C cmd/experiment/dummy clean
|
||||
$(MAKE) -C cmd/experiment/oneshot clean
|
||||
$(MAKE) -C cmd/experiment/spawner clean
|
||||
$(MAKE) -C cmd/experiment/starter clean
|
||||
|
||||
install: ${installs}
|
||||
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
|
||||
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
|
||||
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
|
||||
|
||||
install-test:
|
||||
$(MAKE) -C cmd/experiment/dummy DESTDIR=${DESTDIR} install
|
||||
$(MAKE) -C cmd/experiment/oneshot DESTDIR=${DESTDIR} install
|
||||
$(MAKE) -C cmd/experiment/spawner DESTDIR=${DESTDIR} install
|
||||
$(MAKE) -C cmd/experiment/starter DESTDIR=${DESTDIR} install
|
||||
|
||||
install-dir:
|
||||
install -d ${DESTDIR}
|
||||
137
README.md
137
README.md
@@ -1,2 +1,139 @@
|
||||
# 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)
|
||||
10
cmd/exec/Makefile
Normal file
10
cmd/exec/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install exec ${DESTDIR}/wmexec
|
||||
11
cmd/exec/ent.go
Normal file
11
cmd/exec/ent.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !(cgo && linux)
|
||||
|
||||
package main
|
||||
|
||||
func getUid(user string) (uint64, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func getGid(group string) (uint64, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
51
cmd/exec/ent_lin.go
Normal file
51
cmd/exec/ent_lin.go
Normal file
@@ -0,0 +1,51 @@
|
||||
//go:build cgo && linux
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include<errno.h>
|
||||
#include<string.h>
|
||||
#include<sys/types.h>
|
||||
#include<pwd.h>
|
||||
#include<grp.h>
|
||||
|
||||
static uid_t getuid(const char* username) {
|
||||
struct passwd local, *rv;
|
||||
errno = 0;
|
||||
rv = getpwnam(username);
|
||||
if (errno != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(&local, rv, sizeof(struct passwd));
|
||||
return local.pw_uid;
|
||||
}
|
||||
static gid_t getgid(const char* groupname) {
|
||||
struct group local, *rv;
|
||||
errno = 0;
|
||||
rv = getgrnam(groupname);
|
||||
if (errno != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(&local, rv, sizeof(struct group));
|
||||
return local.gr_gid;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func getUid(user string) (uint64, error) {
|
||||
u, err := C.getuid(C.CString(user))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(u), nil
|
||||
}
|
||||
|
||||
func getGid(group string) (uint64, error) {
|
||||
g, err := C.getgid(C.CString(group))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(g), nil
|
||||
}
|
||||
133
cmd/exec/exec.go
Normal file
133
cmd/exec/exec.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
setsidFlag = "setsid"
|
||||
EnvSetsid = "SETSID"
|
||||
userFlag = "user"
|
||||
EnvUser = "USER"
|
||||
)
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "wmexec",
|
||||
RunE: execCmd,
|
||||
}
|
||||
|
||||
childArgs []string
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
found bool
|
||||
i int
|
||||
arg string
|
||||
selfArgs []string
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolP(setsidFlag, "s", false, "set to true to run setsid() before exec")
|
||||
viper.BindPFlag(EnvSetsid, rootCmd.PersistentFlags().Lookup(setsidFlag))
|
||||
|
||||
rootCmd.PersistentFlags().StringP(userFlag, "u", "", "\"user:[group]\"")
|
||||
viper.BindPFlag(EnvUser, rootCmd.PersistentFlags().Lookup(userFlag))
|
||||
|
||||
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||
viper.BindEnv(EnvUser)
|
||||
viper.BindEnv(EnvSetsid)
|
||||
viper.SetDefault(EnvSetsid, false)
|
||||
viper.SetDefault(EnvUser, "")
|
||||
|
||||
found = false
|
||||
for i, arg = range os.Args {
|
||||
if arg == "--" {
|
||||
found = true
|
||||
if len(os.Args) <= i+1 {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
selfArgs = os.Args[1:i]
|
||||
childArgs = os.Args[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(childArgs) == 0 {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(selfArgs)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func execCmd(cmd *cobra.Command, args []string) error {
|
||||
if viper.GetBool(EnvSetsid) {
|
||||
_, _ = unix.Setsid()
|
||||
}
|
||||
|
||||
var (
|
||||
uid uint64
|
||||
gid uint64
|
||||
err error
|
||||
path string
|
||||
)
|
||||
|
||||
ug := viper.GetString(EnvUser)
|
||||
if len(ug) > 0 {
|
||||
user, group, ok := strings.Cut(ug, ":")
|
||||
if ok {
|
||||
if gid, err = strconv.ParseUint(group, 10, 32); err != nil {
|
||||
if gid, err = getGid(group); err != nil {
|
||||
return fmt.Errorf("cgo getgid: %w", err)
|
||||
}
|
||||
}
|
||||
if err = unix.Setgid(int(gid)); err != nil {
|
||||
return fmt.Errorf("setgid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
uid, err = strconv.ParseUint(user, 10, 32)
|
||||
if err != nil {
|
||||
if uid, err = getUid(user); err != nil {
|
||||
return fmt.Errorf("cgo getuid: %w", err)
|
||||
}
|
||||
}
|
||||
if err = unix.Setuid(int(uid)); err != nil {
|
||||
return fmt.Errorf("setuid: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if path, err = exec.LookPath(childArgs[0]); err != nil {
|
||||
if !errors.Is(err, exec.ErrDot) {
|
||||
return fmt.Errorf("lookpath: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = unix.Exec(path, childArgs, os.Environ()); err != nil {
|
||||
return fmt.Errorf("exec: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
cmd/experiment/.gitignore
vendored
Normal file
8
cmd/experiment/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/dummy/dummy
|
||||
/dummy/version.txt
|
||||
/starter/starter
|
||||
/starter/version.txt
|
||||
/oneshot/oneshot
|
||||
/oneshot/version.txt
|
||||
/spawner/spawner
|
||||
/spawner/version.txt
|
||||
10
cmd/experiment/dummy/Makefile
Normal file
10
cmd/experiment/dummy/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install dummy ${DESTDIR}/wmdummy
|
||||
17
cmd/experiment/dummy/dummy.go
Normal file
17
cmd/experiment/dummy/dummy.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("using log.Println")
|
||||
fmt.Println("using fmt.Println")
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
log.Println("log: finishing up")
|
||||
fmt.Println("fmt: finishing up")
|
||||
}
|
||||
10
cmd/experiment/oneshot/Makefile
Normal file
10
cmd/experiment/oneshot/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install oneshot ${DESTDIR}/wmoneshot
|
||||
78
cmd/experiment/oneshot/oneshot.go
Normal file
78
cmd/experiment/oneshot/oneshot.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
// DummyPath = "/workspaces/wingmate/cmd/experiment/dummy/dummy"
|
||||
DummyPath = "/usr/local/bin/wmdummy"
|
||||
EnvDummyPath = "DUMMY_PATH"
|
||||
EnvLog = "LOG"
|
||||
EnvLogMessage = "LOG_MESSAGE"
|
||||
EnvDefaultLogMessage = "oneshot executed"
|
||||
EnvInstanceNum = "INSTANCE_NUM"
|
||||
EnvDefaultInstances = -1
|
||||
)
|
||||
|
||||
func main() {
|
||||
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||
viper.BindEnv(EnvDummyPath)
|
||||
viper.BindEnv(EnvLog)
|
||||
viper.BindEnv(EnvLogMessage)
|
||||
viper.BindEnv(EnvInstanceNum)
|
||||
viper.SetDefault(EnvDummyPath, DummyPath)
|
||||
viper.SetDefault(EnvLogMessage, EnvDefaultLogMessage)
|
||||
viper.SetDefault(EnvInstanceNum, EnvDefaultInstances)
|
||||
|
||||
exePath := viper.GetString(EnvDummyPath)
|
||||
|
||||
logPath := viper.GetString(EnvLog)
|
||||
logMessage := viper.GetString(EnvLogMessage)
|
||||
log.Println("log path:", logPath)
|
||||
if logPath != "" {
|
||||
var (
|
||||
err error
|
||||
file *os.File
|
||||
)
|
||||
|
||||
if file, err = os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666); err == nil {
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if err = wingmate.NewLog(file); err == nil {
|
||||
wingmate.Log().Info().Msg(logMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StartInstances(exePath)
|
||||
}
|
||||
|
||||
func StartInstances(exePath string) {
|
||||
num := (rand.Uint32() % 16) + 16
|
||||
|
||||
iNum := viper.GetInt(EnvInstanceNum)
|
||||
if iNum > 0 {
|
||||
num = uint32(iNum)
|
||||
}
|
||||
|
||||
var (
|
||||
ctr uint32
|
||||
cmd *exec.Cmd
|
||||
err error
|
||||
)
|
||||
for ctr = 0; ctr < num; ctr++ {
|
||||
cmd = exec.Command(exePath)
|
||||
if err = cmd.Start(); err != nil {
|
||||
log.Printf("failed to run %s: %+v\n", exePath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
cmd/experiment/spawner/Makefile
Normal file
10
cmd/experiment/spawner/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install spawner ${DESTDIR}/wmspawner
|
||||
40
cmd/experiment/spawner/spawner.go
Normal file
40
cmd/experiment/spawner/spawner.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvOneShotPath = "ONESHOT_PATH"
|
||||
OneShotPath = "/usr/local/bin/wmoneshot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
err error
|
||||
t *time.Ticker
|
||||
)
|
||||
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||
viper.BindEnv(EnvOneShotPath)
|
||||
viper.SetDefault(EnvOneShotPath, OneShotPath)
|
||||
|
||||
exePath := viper.GetString(EnvOneShotPath)
|
||||
|
||||
t = time.NewTicker(time.Second * 5)
|
||||
for {
|
||||
cmd = exec.Command(exePath)
|
||||
if err = cmd.Run(); err != nil {
|
||||
log.Printf("failed to run %s: %+v\n", exePath, err)
|
||||
} else {
|
||||
log.Printf("%s executed\n", exePath)
|
||||
}
|
||||
|
||||
<-t.C
|
||||
}
|
||||
}
|
||||
10
cmd/experiment/starter/Makefile
Normal file
10
cmd/experiment/starter/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install starter ${DESTDIR}/wmstarter
|
||||
73
cmd/experiment/starter/starter.go
Normal file
73
cmd/experiment/starter/starter.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
// DummyPath = "/workspaces/wingmate/cmd/experiment/dummy/dummy"
|
||||
DummyPath = "/usr/local/bin/wmdummy"
|
||||
EnvDummyPath = "DUMMY_PATH"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
wg *sync.WaitGroup
|
||||
err error
|
||||
exePath string
|
||||
)
|
||||
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||
viper.BindEnv(EnvDummyPath)
|
||||
viper.SetDefault(EnvDummyPath, DummyPath)
|
||||
|
||||
exePath = viper.GetString(EnvDummyPath)
|
||||
|
||||
cmd := exec.Command(exePath)
|
||||
|
||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
wg = &sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go pulley(wg, stdout, "stdout")
|
||||
go pulley(wg, stderr, "stderr")
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
log.Printf("got error when Waiting for child process: %#v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func pulley(wg *sync.WaitGroup, src io.ReadCloser, srcName string) {
|
||||
defer wg.Done()
|
||||
|
||||
scanner := bufio.NewScanner(src)
|
||||
for scanner.Scan() {
|
||||
log.Printf("coming out from %s: %s\n", srcName, scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("got error whean reading from %s: %#v\n", srcName, err)
|
||||
}
|
||||
|
||||
log.Printf("closing %s...\n", srcName)
|
||||
_ = src.Close()
|
||||
}
|
||||
10
cmd/pidproxy/Makefile
Normal file
10
cmd/pidproxy/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install pidproxy ${DESTDIR}/wmpidproxy
|
||||
161
cmd/pidproxy/pidproxy.go
Normal file
161
cmd/pidproxy/pidproxy.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
pidFileFlag = "pid-file"
|
||||
EnvStartSecs = "STARTSECS"
|
||||
EnvDefaultStartSecs = 1
|
||||
)
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "wmpidproxy",
|
||||
RunE: pidProxy,
|
||||
}
|
||||
|
||||
childArgs []string
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
i int
|
||||
arg string
|
||||
selfArgs []string
|
||||
found bool
|
||||
)
|
||||
|
||||
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||
viper.BindEnv(EnvStartSecs)
|
||||
viper.SetDefault(EnvStartSecs, EnvDefaultStartSecs)
|
||||
|
||||
rootCmd.PersistentFlags().StringP(pidFileFlag, "p", "", "location of pid file")
|
||||
rootCmd.MarkFlagRequired(pidFileFlag)
|
||||
viper.BindPFlag(pidFileFlag, rootCmd.PersistentFlags().Lookup(pidFileFlag))
|
||||
|
||||
found = false
|
||||
for i, arg = range os.Args {
|
||||
if arg == "--" {
|
||||
found = true
|
||||
if len(os.Args) <= i+1 {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
selfArgs = os.Args[1:i]
|
||||
childArgs = os.Args[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(childArgs) == 0 {
|
||||
log.Println("invalid argument")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(selfArgs)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func pidProxy(cmd *cobra.Command, args []string) error {
|
||||
pidfile := viper.GetString(pidFileFlag)
|
||||
log.Printf("%s %v", pidfile, childArgs)
|
||||
if len(childArgs) > 1 {
|
||||
go startProcess(childArgs[0], childArgs[1:]...)
|
||||
} else {
|
||||
go startProcess(childArgs[0])
|
||||
}
|
||||
initialWait := viper.GetInt(EnvStartSecs)
|
||||
time.Sleep(time.Second * time.Duration(initialWait))
|
||||
|
||||
var (
|
||||
err error
|
||||
pid int
|
||||
sc chan os.Signal
|
||||
t *time.Ticker
|
||||
)
|
||||
|
||||
sc = make(chan os.Signal, 1)
|
||||
signal.Notify(sc, unix.SIGTERM)
|
||||
|
||||
t = time.NewTicker(time.Second)
|
||||
|
||||
check:
|
||||
for {
|
||||
if pid, err = readPid(pidfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = unix.Kill(pid, syscall.Signal(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-sc:
|
||||
if pid, err = readPid(pidfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = unix.Kill(pid, unix.SIGTERM); err != nil {
|
||||
return err
|
||||
}
|
||||
break check
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPid(pidFile string) (int, error) {
|
||||
var (
|
||||
file *os.File
|
||||
err error
|
||||
pid64 int64
|
||||
)
|
||||
|
||||
if file, err = os.Open(pidFile); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
if scanner.Scan() {
|
||||
if pid64, err = strconv.ParseInt(scanner.Text(), 10, 64); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(pid64), nil
|
||||
} else {
|
||||
return 0, errors.New("invalid scanner")
|
||||
}
|
||||
}
|
||||
|
||||
func startProcess(arg0 string, args ...string) {
|
||||
if err := exec.Command(arg0, args...).Run(); err != nil {
|
||||
log.Println("exec:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
11
cmd/wingmate/Makefile
Normal file
11
cmd/wingmate/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
all:
|
||||
git describe > version.txt
|
||||
go build -v
|
||||
|
||||
clean:
|
||||
rm version.txt
|
||||
go clean -i -cache -testcache
|
||||
|
||||
install:
|
||||
install wingmate ${DESTDIR}/wingmate
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
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 {
|
||||
services []wminit.Path
|
||||
cron []wminit.Cron
|
||||
tasks *task.Tasks
|
||||
}
|
||||
|
||||
func (c wConfig) Services() []wminit.Path {
|
||||
return c.services
|
||||
func (c *wConfig) Tasks() wminit.Tasks {
|
||||
return c.tasks
|
||||
}
|
||||
|
||||
func (c wConfig) Cron() []wminit.Cron {
|
||||
return c.cron
|
||||
}
|
||||
|
||||
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)),
|
||||
func convert(cfg *config.Config) *wConfig {
|
||||
retval := &wConfig{
|
||||
tasks: task.NewTasks(),
|
||||
}
|
||||
|
||||
for _, s := range cfg.ServicePaths {
|
||||
retval.services = append(retval.services, wPath{path: s})
|
||||
for _, s := range cfg.ServiceV0 {
|
||||
retval.tasks.AddV0Service(s)
|
||||
|
||||
}
|
||||
|
||||
for _, c := range cfg.Cron {
|
||||
retval.cron = append(retval.cron, wCron{iCron: c})
|
||||
var schedule task.CronSchedule
|
||||
for _, c := range cfg.CronV0 {
|
||||
schedule.Minute = convertSchedule(c.Minute)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,11 +16,47 @@ const (
|
||||
DefaultConfigPath = "/etc/wingmate"
|
||||
ServiceDirName = "service"
|
||||
CrontabFileName = "crontab"
|
||||
WingmateConfigFileName = "wingmate"
|
||||
WingmateConfigFileFormat = "yaml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServicePaths []string
|
||||
Cron []*Cron
|
||||
ServiceV0 []string
|
||||
CronV0 []*Cron
|
||||
Service []ServiceTask
|
||||
Cron []CronTask
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Command []string `mapstructure:"command"`
|
||||
Environ []string `mapstructure:"environ"`
|
||||
Setsid bool `mapstructure:"setsid"`
|
||||
User string `mapstructure:"user"`
|
||||
Group string `mapstructure:"group"`
|
||||
Background bool `mapstructure:"background"`
|
||||
WorkingDir string `mapstructure:"working_dir"`
|
||||
}
|
||||
|
||||
type ServiceTask struct {
|
||||
Name string `mapstructure:"-"`
|
||||
Task `mapstructure:",squash"`
|
||||
AutoStart bool `mapstructure:"autostart"`
|
||||
AutoRestart bool `mapstructure:"autorestart"`
|
||||
}
|
||||
|
||||
type CronTask struct {
|
||||
Name string `mapstructure:"-"`
|
||||
CronSchedule `mapstructure:"-"`
|
||||
Task `mapstructure:",squash"`
|
||||
Schedule string `mapstructure:"schedule"`
|
||||
}
|
||||
|
||||
type CronSchedule struct {
|
||||
Minute CronTimeSpec
|
||||
Hour CronTimeSpec
|
||||
DoM CronTimeSpec
|
||||
Month CronTimeSpec
|
||||
DoW CronTimeSpec
|
||||
}
|
||||
|
||||
func Read() (*Config, error) {
|
||||
@@ -33,14 +70,17 @@ func Read() (*Config, error) {
|
||||
svcdir string
|
||||
serviceAvailable bool
|
||||
cronAvailable bool
|
||||
wingmateConfigAvailable bool
|
||||
cron []*Cron
|
||||
crontabfile string
|
||||
services []ServiceTask
|
||||
crones []CronTask
|
||||
)
|
||||
|
||||
serviceAvailable = false
|
||||
cronAvailable = false
|
||||
outConfig := &Config{
|
||||
ServicePaths: make([]string, 0),
|
||||
ServiceV0: make([]string, 0),
|
||||
}
|
||||
configPath := viper.GetString(EnvConfigPath)
|
||||
svcdir = filepath.Join(configPath, ServiceDirName)
|
||||
@@ -48,26 +88,42 @@ func Read() (*Config, error) {
|
||||
if len(dirent) > 0 {
|
||||
for _, d := range dirent {
|
||||
if d.Type().IsRegular() {
|
||||
svcPath := filepath.Join(svcdir, d.Name())
|
||||
if err = unix.Access(svcPath, unix.X_OK); err == nil {
|
||||
serviceAvailable = true
|
||||
outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
|
||||
outConfig.ServiceV0 = append(outConfig.ServiceV0, svcPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
wingmate.Log().Error().Msgf("encounter error when reading service directory %s: %#v", svcdir, err)
|
||||
wingmate.Log().Error().Msgf("encounter error when reading service directory %s: %+v", svcdir, err)
|
||||
}
|
||||
|
||||
crontabfile = filepath.Join(configPath, CrontabFileName)
|
||||
cron, err = readCrontab(crontabfile)
|
||||
if len(cron) > 0 {
|
||||
outConfig.Cron = cron
|
||||
outConfig.CronV0 = cron
|
||||
cronAvailable = true
|
||||
}
|
||||
if err != nil {
|
||||
wingmate.Log().Error().Msgf("encounter error when reading crontab %s: %#v", crontabfile, err)
|
||||
wingmate.Log().Error().Msgf("encounter error when reading crontab %s: %+v", crontabfile, err)
|
||||
}
|
||||
|
||||
if !serviceAvailable && !cronAvailable {
|
||||
wingmateConfigAvailable = false
|
||||
if services, crones, err = readConfigYaml(configPath, WingmateConfigFileName, WingmateConfigFileFormat); err != nil {
|
||||
wingmate.Log().Error().Msgf("encounter error when reading wingmate config file in %s/%s: %+v", configPath, WingmateConfigFileName, err)
|
||||
}
|
||||
if len(services) > 0 {
|
||||
outConfig.Service = services
|
||||
wingmateConfigAvailable = true
|
||||
}
|
||||
if len(crones) > 0 {
|
||||
outConfig.Cron = crones
|
||||
wingmateConfigAvailable = true
|
||||
}
|
||||
|
||||
if !serviceAvailable && !cronAvailable && !wingmateConfigAvailable {
|
||||
return nil, errors.New("no config found")
|
||||
}
|
||||
|
||||
|
||||
132
config/config_test.go
Normal file
132
config/config_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
"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) {
|
||||
|
||||
type testEntry struct {
|
||||
name string
|
||||
testFunc func(t *testing.T)
|
||||
}
|
||||
|
||||
mkSvcDir := func(t *testing.T) {
|
||||
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
|
||||
t.Fatal("create dir", err)
|
||||
}
|
||||
}
|
||||
|
||||
touchFile := func(t *testing.T, name string, perm os.FileMode) {
|
||||
f, err := os.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
t.Fatal("create file", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
_ = wingmate.NewLog(os.Stderr)
|
||||
tests := []testEntry{
|
||||
{
|
||||
name: "positive",
|
||||
testFunc: func(t *testing.T) {
|
||||
mkSvcDir(t)
|
||||
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
|
||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
|
||||
|
||||
cfg, err := Read()
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(
|
||||
t,
|
||||
cfg.ServiceV0,
|
||||
[]string{
|
||||
path.Join(configDir, serviceDir, "one.sh"),
|
||||
path.Join(configDir, serviceDir, "two.sh"),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with directory",
|
||||
testFunc: func(t *testing.T) {
|
||||
const subdir1 = "subdir1"
|
||||
mkSvcDir(t)
|
||||
assert.Nil(t, os.Mkdir(path.Join(configDir, serviceDir, subdir1), 0755))
|
||||
touchFile(t, path.Join(configDir, serviceDir, subdir1, "one.sh"), 0755)
|
||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
|
||||
cfg, err := Read()
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(
|
||||
t,
|
||||
cfg.ServiceV0,
|
||||
[]string{
|
||||
path.Join(configDir, serviceDir, "two.sh"),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong mode",
|
||||
testFunc: func(t *testing.T) {
|
||||
mkSvcDir(t)
|
||||
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
|
||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0644)
|
||||
|
||||
cfg, err := Read()
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(
|
||||
t,
|
||||
cfg.ServiceV0,
|
||||
[]string{
|
||||
path.Join(configDir, serviceDir, "one.sh"),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
testFunc: func(t *testing.T) {
|
||||
mkSvcDir(t)
|
||||
|
||||
_, err := Read()
|
||||
assert.NotNil(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup(t)
|
||||
tt.testFunc(t)
|
||||
tear(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,54 +4,34 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
)
|
||||
|
||||
type CronExactSpec interface {
|
||||
CronTimeSpec
|
||||
Value() uint8
|
||||
}
|
||||
|
||||
type CronMultipleOccurrenceSpec interface {
|
||||
CronTimeSpec
|
||||
Values() []uint8
|
||||
}
|
||||
|
||||
type CronTimeSpec interface {
|
||||
Type() wingmate.CronTimeType
|
||||
Match(uint8) bool
|
||||
//Type() wingmate.CronTimeType
|
||||
//Match(uint8) bool
|
||||
}
|
||||
|
||||
// type Cron interface {
|
||||
// Minute() CronTimeSpec
|
||||
// Hour() CronTimeSpec
|
||||
// DayOfMonth() CronTimeSpec
|
||||
// Month() CronTimeSpec
|
||||
// DayOfWeek() CronTimeSpec
|
||||
// }
|
||||
|
||||
type Cron struct {
|
||||
minute CronTimeSpec
|
||||
hour CronTimeSpec
|
||||
dom CronTimeSpec
|
||||
month CronTimeSpec
|
||||
dow CronTimeSpec
|
||||
command string
|
||||
lastRun time.Time
|
||||
hasRun bool
|
||||
Minute CronTimeSpec
|
||||
Hour CronTimeSpec
|
||||
DoM CronTimeSpec
|
||||
Month CronTimeSpec
|
||||
DoW CronTimeSpec
|
||||
Command string
|
||||
}
|
||||
|
||||
type cronField int
|
||||
|
||||
const (
|
||||
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*$`
|
||||
CrontabSubmatchLen = 7
|
||||
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*$`
|
||||
CrontabCommentLineRegexPattern = `^\s*#.*$`
|
||||
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
|
||||
CrontabSubMatchLen = 7
|
||||
|
||||
minute cronField = iota
|
||||
hour
|
||||
@@ -60,21 +40,22 @@ const (
|
||||
dow
|
||||
)
|
||||
|
||||
var (
|
||||
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
|
||||
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
|
||||
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
|
||||
)
|
||||
|
||||
func readCrontab(path string) ([]*Cron, error) {
|
||||
var (
|
||||
file *os.File
|
||||
err error
|
||||
scanner *bufio.Scanner
|
||||
line string
|
||||
re *regexp.Regexp
|
||||
parts []string
|
||||
retval []*Cron
|
||||
)
|
||||
|
||||
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file, err = os.Open(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,41 +68,48 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
for scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
|
||||
parts = re.FindStringSubmatch(line)
|
||||
if len(parts) != CrontabSubmatchLen {
|
||||
if crontabCommentLineRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
c := &Cron{
|
||||
hasRun: false,
|
||||
}
|
||||
c := &Cron{}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.command = parts[6]
|
||||
c.Command = parts[6]
|
||||
|
||||
retval = append(retval, c)
|
||||
}
|
||||
@@ -129,32 +117,6 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
func (c *Cron) Command() string {
|
||||
return c.command
|
||||
}
|
||||
|
||||
func (c *Cron) TimeToRun(now time.Time) bool {
|
||||
if !c.hasRun {
|
||||
c.lastRun = now
|
||||
c.hasRun = true
|
||||
return true
|
||||
}
|
||||
|
||||
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
|
||||
return false
|
||||
}
|
||||
|
||||
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())) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type fieldRange struct {
|
||||
min int
|
||||
max int
|
||||
@@ -187,33 +149,33 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
switch field {
|
||||
case minute:
|
||||
fr = newRange(0, 59)
|
||||
cField = &c.minute
|
||||
cField = &c.Minute
|
||||
case hour:
|
||||
fr = newRange(0, 23)
|
||||
cField = &c.hour
|
||||
cField = &c.Hour
|
||||
case dom:
|
||||
fr = newRange(1, 31)
|
||||
cField = &c.dom
|
||||
cField = &c.DoM
|
||||
case month:
|
||||
fr = newRange(1, 12)
|
||||
cField = &c.month
|
||||
cField = &c.Month
|
||||
case dow:
|
||||
fr = newRange(0, 6)
|
||||
cField = &c.dow
|
||||
cField = &c.DoW
|
||||
default:
|
||||
return errors.New("invalid cron field descriptor")
|
||||
}
|
||||
|
||||
if input == "*" {
|
||||
*cField = &specAny{}
|
||||
*cField = &SpecAny{}
|
||||
} else if strings.HasPrefix(input, "*/") {
|
||||
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)
|
||||
}
|
||||
|
||||
parsed = uint8(parsed64)
|
||||
if fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
|
||||
if !fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %+v with input %s parsed to %d: invalid value", field, input, parsed)
|
||||
}
|
||||
multi = make([]uint8, 0)
|
||||
current = parsed
|
||||
@@ -222,7 +184,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
current += parsed
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
@@ -231,31 +193,31 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
multi = make([]uint8, 0)
|
||||
for _, s := range multiStr {
|
||||
if parsed64, err = strconv.ParseUint(s, 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)
|
||||
}
|
||||
|
||||
parsed = uint8(parsed64)
|
||||
if fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
|
||||
if !fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||
}
|
||||
|
||||
multi = append(multi, parsed)
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
if parsed64, err = strconv.ParseUint(input, 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)
|
||||
}
|
||||
|
||||
parsed = uint8(parsed64)
|
||||
if fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
|
||||
if !fr.valid(parsed) {
|
||||
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||
}
|
||||
|
||||
*cField = &specExact{
|
||||
*cField = &SpecExact{
|
||||
value: parsed,
|
||||
}
|
||||
}
|
||||
@@ -264,51 +226,21 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type specAny struct{}
|
||||
type SpecAny struct{}
|
||||
|
||||
func (a *specAny) Type() wingmate.CronTimeType {
|
||||
return wingmate.Any
|
||||
}
|
||||
|
||||
func (a *specAny) Match(u uint8) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type specExact struct {
|
||||
type SpecExact struct {
|
||||
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 {
|
||||
func (e *SpecExact) Value() uint8 {
|
||||
return e.value
|
||||
}
|
||||
|
||||
type specMultiOccurrence struct {
|
||||
type SpecMultiOccurrence struct {
|
||||
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 {
|
||||
func (m *SpecMultiOccurrence) Values() []uint8 {
|
||||
out := make([]uint8, len(m.values))
|
||||
copy(out, m.values)
|
||||
return out
|
||||
|
||||
128
config/crontab_test.go
Normal file
128
config/crontab_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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.CronV0 {
|
||||
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"))
|
||||
}
|
||||
61
config/yaml.go
Normal file
61
config/yaml.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceConfigGroup = "service"
|
||||
CronConfigGroup = "cron"
|
||||
ServiceKeyFormat = "service.%s"
|
||||
CronKeyFormat = "cron.%s"
|
||||
)
|
||||
|
||||
func readConfigYaml(path, name, format string) ([]ServiceTask, []CronTask, error) {
|
||||
var (
|
||||
err error
|
||||
nameMap map[string]any
|
||||
itemName string
|
||||
serviceTask ServiceTask
|
||||
cronTask CronTask
|
||||
item any
|
||||
services []ServiceTask
|
||||
crones []CronTask
|
||||
)
|
||||
|
||||
viper.AddConfigPath(path)
|
||||
viper.SetConfigType(format)
|
||||
viper.SetConfigName(name)
|
||||
|
||||
if err = viper.ReadInConfig(); err != nil {
|
||||
return nil, nil, fmt.Errorf("reading config in dir %s, file %s, format %s: %w", path, name, format, err)
|
||||
}
|
||||
|
||||
services = make([]ServiceTask, 0)
|
||||
nameMap = viper.GetStringMap(ServiceConfigGroup)
|
||||
for itemName, item = range nameMap {
|
||||
serviceTask = ServiceTask{}
|
||||
if err = viper.UnmarshalKey(fmt.Sprintf(ServiceKeyFormat, itemName), &serviceTask); err != nil {
|
||||
wingmate.Log().Error().Msgf("failed to parse service %s: %+v | %+v", itemName, err, item)
|
||||
continue
|
||||
}
|
||||
serviceTask.Name = itemName
|
||||
services = append(services, serviceTask)
|
||||
}
|
||||
|
||||
crones = make([]CronTask, 0)
|
||||
nameMap = viper.GetStringMap(CronConfigGroup)
|
||||
for itemName, item = range nameMap {
|
||||
cronTask = CronTask{}
|
||||
if err = viper.UnmarshalKey(fmt.Sprintf(CronKeyFormat, itemName), &cronTask); err != nil {
|
||||
wingmate.Log().Error().Msgf("failed to parse cron %s: %v | %v", itemName, err, item)
|
||||
continue
|
||||
}
|
||||
cronTask.Name = itemName
|
||||
crones = append(crones, cronTask)
|
||||
}
|
||||
|
||||
return services, crones, nil
|
||||
}
|
||||
68
config/yaml_test.go
Normal file
68
config/yaml_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const configName = "wingmate.yaml"
|
||||
|
||||
func TestYaml(t *testing.T) {
|
||||
type testEntry struct {
|
||||
name string
|
||||
config string
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
_ = wingmate.NewLog(os.Stderr)
|
||||
tests := []testEntry{
|
||||
{
|
||||
name: "positive",
|
||||
config: yamlTestCase0,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup(t)
|
||||
defer tear(t)
|
||||
|
||||
writeYaml(t, path.Join(configDir, configName), tt.config)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeYaml(t *testing.T, path, content string) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
if f, err = os.Create(path); err != nil {
|
||||
t.Fatal("create yaml file", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
if _, err = f.Write([]byte(content)); err != nil {
|
||||
t.Fatal("write yaml file", err)
|
||||
}
|
||||
}
|
||||
|
||||
const yamlTestCase0 = `version: "1"
|
||||
service:
|
||||
one:
|
||||
command: ["command", "arg0", "arg1"]
|
||||
environ: ["ENV1=value1", "ENV2=valueX"]
|
||||
user: "user1"
|
||||
group: "999"
|
||||
working_dir: "/path/to/working"`
|
||||
22
docker/alpine/Dockerfile
Normal file
22
docker/alpine/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM golang:1.21-alpine as builder
|
||||
|
||||
ADD . /root/wingmate
|
||||
WORKDIR /root/wingmate/
|
||||
ARG TEST_BUILD
|
||||
RUN apk update && apk add git make build-base && \
|
||||
CGO_ENABLED=1 make all && \
|
||||
make DESTDIR=/usr/local/bin/wingmate install
|
||||
|
||||
|
||||
|
||||
FROM alpine:3.18
|
||||
|
||||
RUN apk add tzdata && ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime && \
|
||||
adduser -h /home/user1 -D -s /bin/sh user1 && \
|
||||
adduser -h /home/user2 -D -s /bin/sh user2
|
||||
COPY --from=builder /usr/local/bin/wingmate/ /usr/local/bin/
|
||||
ADD --chmod=755 docker/alpine/entry.sh /usr/local/bin/entry.sh
|
||||
ADD --chmod=755 docker/alpine/etc /etc
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||
CMD [ "/usr/local/bin/wingmate" ]
|
||||
7
docker/alpine/entry.sh
Normal file
7
docker/alpine/entry.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec /usr/local/bin/wingmate
|
||||
fi
|
||||
3
docker/alpine/etc/wingmate/crontab
Normal file
3
docker/alpine/etc/wingmate/crontab
Normal file
@@ -0,0 +1,3 @@
|
||||
*/5 * * * * /etc/wingmate/crontab.d/cron1.sh
|
||||
17,42 */2 * * * /etc/wingmate/crontab.d/cron2.sh
|
||||
7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh
|
||||
9
docker/alpine/etc/wingmate/crontab.d/cron1.sh
Normal file
9
docker/alpine/etc/wingmate/crontab.d/cron1.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron1.log
|
||||
export WINGMATE_LOG_MESSAGE="cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
|
||||
|
||||
echo "I'm runnig with dummy=$WINGMATE_DUMMY_PATH, log=$WINGMATE_LOG and mesage=$WINGMATE_LOG_MESSAGE" >> /var/log/debug-cron.log
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
7
docker/alpine/etc/wingmate/crontab.d/cron2.sh
Normal file
7
docker/alpine/etc/wingmate/crontab.d/cron2.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron2.log
|
||||
export WINGMATE_LOG_MESSAGE="cron scheduled using 17,42 */2 * * *"
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
7
docker/alpine/etc/wingmate/crontab.d/cron3.sh
Normal file
7
docker/alpine/etc/wingmate/crontab.d/cron3.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron3.log
|
||||
export WINGMATE_LOG_MESSAGE="cron entry: 7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh"
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
4
docker/alpine/etc/wingmate/service/one.sh
Normal file
4
docker/alpine/etc/wingmate/service/one.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
export DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
exec /usr/local/bin/wmexec --setsid --user user1:user1 -- /usr/local/bin/wmstarter
|
||||
5
docker/alpine/etc/wingmate/service/spawner.sh
Normal file
5
docker/alpine/etc/wingmate/service/spawner.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
exec /usr/local/bin/wmexec --user 1001 -- /usr/local/bin/wmspawner
|
||||
20
docker/bookworm/Dockerfile
Normal file
20
docker/bookworm/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM golang:1.21-bookworm as builder
|
||||
|
||||
ADD . /root/wingmate
|
||||
WORKDIR /root/wingmate/
|
||||
ARG TEST_BUILD
|
||||
RUN make all && make DESTDIR=/usr/local/bin/wingmate install
|
||||
|
||||
|
||||
|
||||
FROM debian:bookworm
|
||||
|
||||
RUN ln -sf /usr/share/zoneinfo/Australia/Sydney /etc/localtime && \
|
||||
apt update && apt install -y procps && \
|
||||
useradd -m -s /bin/bash user1
|
||||
COPY --from=builder /usr/local/bin/wingmate/ /usr/local/bin/
|
||||
ADD --chmod=755 docker/bookworm/entry.sh /usr/local/bin/entry.sh
|
||||
ADD --chmod=755 docker/bookworm/etc /etc
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||
CMD [ "/usr/local/bin/wingmate" ]
|
||||
7
docker/bookworm/entry.sh
Normal file
7
docker/bookworm/entry.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec /usr/local/bin/wingmate
|
||||
fi
|
||||
3
docker/bookworm/etc/wingmate/crontab
Normal file
3
docker/bookworm/etc/wingmate/crontab
Normal file
@@ -0,0 +1,3 @@
|
||||
*/5 * * * * /etc/wingmate/crontab.d/cron1.sh
|
||||
17,42 */2 * * * /etc/wingmate/crontab.d/cron2.sh
|
||||
7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh
|
||||
9
docker/bookworm/etc/wingmate/crontab.d/cron1.sh
Normal file
9
docker/bookworm/etc/wingmate/crontab.d/cron1.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron1.log
|
||||
export WINGMATE_LOG_MESSAGE="cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
|
||||
|
||||
echo "I'm runnig with dummy=$WINGMATE_DUMMY_PATH, log=$WINGMATE_LOG and mesage=$WINGMATE_LOG_MESSAGE" >> /var/log/debug-cron.log
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
7
docker/bookworm/etc/wingmate/crontab.d/cron2.sh
Normal file
7
docker/bookworm/etc/wingmate/crontab.d/cron2.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron2.log
|
||||
export WINGMATE_LOG_MESSAGE="cron scheduled using 17,42 */2 * * *"
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
7
docker/bookworm/etc/wingmate/crontab.d/cron3.sh
Normal file
7
docker/bookworm/etc/wingmate/crontab.d/cron3.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
export WINGMATE_LOG=/var/log/cron3.log
|
||||
export WINGMATE_LOG_MESSAGE="cron entry: 7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh"
|
||||
|
||||
exec /usr/local/bin/wmoneshot
|
||||
4
docker/bookworm/etc/wingmate/service/one.sh
Normal file
4
docker/bookworm/etc/wingmate/service/one.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
export DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
exec /usr/local/bin/wmexec --setsid --user user1:user1 -- /usr/local/bin/wmstarter
|
||||
5
docker/bookworm/etc/wingmate/service/spawner.sh
Normal file
5
docker/bookworm/etc/wingmate/service/spawner.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
|
||||
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
|
||||
exec /usr/local/bin/wmexec --user 1200 -- /usr/local/bin/wmspawner
|
||||
14
example/ssh-docker/Dockerfile
Normal file
14
example/ssh-docker/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
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" ]
|
||||
7
example/ssh-docker/entry.sh
Normal file
7
example/ssh-docker/entry.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec /usr/local/bin/wingmate
|
||||
fi
|
||||
3
example/ssh-docker/etc/wingmate/service/sshd.sh
Normal file
3
example/ssh-docker/etc/wingmate/service/sshd.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec /usr/local/bin/wmpidproxy --pid-file /var/run/sshd.pid -- /usr/sbin/sshd
|
||||
7
go.mod
7
go.mod
@@ -3,8 +3,8 @@ module gitea.suyono.dev/suyono/wingmate
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/rs/zerolog v1.30.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/sys v0.15.0
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -48,6 +48,7 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -150,6 +151,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
@@ -166,6 +169,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
@@ -183,6 +188,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -194,12 +201,15 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
|
||||
58
init/cron.go
58
init/cron.go
@@ -1,6 +1,7 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -8,19 +9,66 @@ import (
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
)
|
||||
|
||||
func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
|
||||
const (
|
||||
cronTag = "cron"
|
||||
)
|
||||
|
||||
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(time.Second * 20)
|
||||
var (
|
||||
iwg *sync.WaitGroup
|
||||
err error
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
cmd *exec.Cmd
|
||||
)
|
||||
|
||||
ticker := time.NewTicker(time.Second * 30)
|
||||
cron:
|
||||
for {
|
||||
if cron.TimeToRun(time.Now()) {
|
||||
cmd := exec.Command(cron.Command().Path())
|
||||
if err := cmd.Run(); err != nil {
|
||||
wingmate.Log().Error().Msgf("running cron %s error %#v", cron.Command().Path(), err)
|
||||
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing")
|
||||
if len(cron.Command()) == 1 {
|
||||
cmd = exec.Command(cron.Command()[0])
|
||||
} else {
|
||||
cmd = exec.Command(cron.Command()[0], cron.Command()[1:]...)
|
||||
}
|
||||
iwg = &sync.WaitGroup{}
|
||||
|
||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stdout pipe: %+v", err)
|
||||
goto fail
|
||||
}
|
||||
|
||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stderr pipe: %+v", err)
|
||||
_ = stdout.Close()
|
||||
goto fail
|
||||
}
|
||||
|
||||
iwg.Add(1)
|
||||
go i.pipeReader(iwg, stdout, cronTag, cron.Name())
|
||||
|
||||
iwg.Add(1)
|
||||
go i.pipeReader(iwg, stderr, cronTag, cron.Name())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Name(), err)
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
iwg.Wait()
|
||||
goto fail
|
||||
}
|
||||
|
||||
iwg.Wait()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("got error when waiting: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fail:
|
||||
select {
|
||||
case <-exitFlag:
|
||||
ticker.Stop()
|
||||
|
||||
48
init/init.go
48
init/init.go
@@ -1,22 +1,48 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Path interface {
|
||||
Path() string
|
||||
type Tasks interface {
|
||||
List() []Task
|
||||
Services() []ServiceTask
|
||||
Crones() []CronTask
|
||||
Get(string) (Task, error)
|
||||
}
|
||||
|
||||
type Cron interface {
|
||||
Command() Path
|
||||
type UserGroup interface {
|
||||
}
|
||||
|
||||
type TaskStatus interface {
|
||||
}
|
||||
|
||||
type Task interface {
|
||||
Name() string
|
||||
Command() []string
|
||||
Environ() []string
|
||||
Setsid() bool
|
||||
UserGroup() UserGroup
|
||||
Background() bool //NOTE: implies using wmpidproxy
|
||||
WorkingDir() string
|
||||
Status() TaskStatus
|
||||
}
|
||||
|
||||
type CronTask interface {
|
||||
Task
|
||||
TimeToRun(time.Time) bool
|
||||
}
|
||||
|
||||
type ServiceTask interface {
|
||||
Task
|
||||
AutoStart() bool
|
||||
AutoRestart() bool
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
Services() []Path
|
||||
Cron() []Cron
|
||||
Tasks() Tasks
|
||||
}
|
||||
|
||||
type Init struct {
|
||||
@@ -34,24 +60,26 @@ func (i *Init) Start() {
|
||||
wg *sync.WaitGroup
|
||||
signalTrigger chan any
|
||||
sighandlerExit chan any
|
||||
sigchld chan os.Signal
|
||||
)
|
||||
|
||||
signalTrigger = make(chan any)
|
||||
sighandlerExit = make(chan any)
|
||||
sigchld = make(chan os.Signal, 1)
|
||||
|
||||
wg = &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go i.waiter(wg, signalTrigger, sighandlerExit)
|
||||
go i.waiter(wg, signalTrigger, sighandlerExit, sigchld)
|
||||
|
||||
wg.Add(1)
|
||||
go i.sighandler(wg, signalTrigger, sighandlerExit)
|
||||
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
|
||||
|
||||
for _, s := range i.config.Services() {
|
||||
for _, s := range i.config.Tasks().Services() {
|
||||
wg.Add(1)
|
||||
go i.service(wg, s, signalTrigger)
|
||||
}
|
||||
|
||||
for _, c := range i.config.Cron() {
|
||||
for _, c := range i.config.Tasks().Crones() {
|
||||
wg.Add(1)
|
||||
go i.cron(wg, c, signalTrigger)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,81 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
)
|
||||
|
||||
func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
|
||||
const (
|
||||
serviceTag = "service"
|
||||
)
|
||||
|
||||
func (i *Init) service(wg *sync.WaitGroup, task Task, exitFlag <-chan any) {
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
err error
|
||||
iwg *sync.WaitGroup
|
||||
stderr io.ReadCloser
|
||||
stdout io.ReadCloser
|
||||
failStatus bool
|
||||
cmd *exec.Cmd
|
||||
)
|
||||
|
||||
defer func() {
|
||||
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
|
||||
}()
|
||||
|
||||
service:
|
||||
for {
|
||||
cmd := exec.Command(path.Path())
|
||||
if err = cmd.Run(); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting service %s error %#v", path.Path(), err)
|
||||
failStatus = false
|
||||
if len(task.Command()) == 1 {
|
||||
cmd = exec.Command(task.Command()[0])
|
||||
} else {
|
||||
cmd = exec.Command(task.Command()[0], task.Command()[1:]...)
|
||||
}
|
||||
iwg = &sync.WaitGroup{}
|
||||
|
||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stdout pipe: %#v", err)
|
||||
failStatus = true
|
||||
goto fail
|
||||
}
|
||||
iwg.Add(1)
|
||||
go i.pipeReader(iwg, stdout, serviceTag, task.Name())
|
||||
|
||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stderr pipe: %#v", err)
|
||||
_ = stdout.Close()
|
||||
failStatus = true
|
||||
goto fail
|
||||
}
|
||||
iwg.Add(1)
|
||||
go i.pipeReader(iwg, stderr, serviceTag, task.Name())
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting service %s error %#v", task.Name(), err)
|
||||
failStatus = true
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
iwg.Wait()
|
||||
goto fail
|
||||
}
|
||||
|
||||
iwg.Wait()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("got error when waiting: %+v", err)
|
||||
}
|
||||
fail:
|
||||
if failStatus {
|
||||
time.Sleep(time.Second)
|
||||
failStatus = false
|
||||
}
|
||||
select {
|
||||
case <-exitFlag:
|
||||
break service
|
||||
@@ -29,3 +84,18 @@ service:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (i *Init) pipeReader(wg *sync.WaitGroup, pipe io.ReadCloser, tag, serviceName string) {
|
||||
defer wg.Done()
|
||||
|
||||
scanner := bufio.NewScanner(pipe)
|
||||
for scanner.Scan() {
|
||||
wingmate.Log().Info().Str(tag, serviceName).Msg(scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
wingmate.Log().Error().Str(tag, serviceName).Msgf("got error when reading pipe: %#v", err)
|
||||
}
|
||||
|
||||
wingmate.Log().Info().Str(tag, serviceName).Msg("closing pipe")
|
||||
}
|
||||
|
||||
@@ -5,11 +5,16 @@ import (
|
||||
"os/signal"
|
||||
"sync"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
|
||||
defer wg.Wait()
|
||||
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any, sigchld chan<- os.Signal) {
|
||||
defer wg.Done()
|
||||
|
||||
defer func() {
|
||||
wingmate.Log().Warn().Msg("signal handler: exiting")
|
||||
}()
|
||||
|
||||
isOpen := true
|
||||
|
||||
@@ -23,14 +28,21 @@ signal:
|
||||
switch s {
|
||||
case unix.SIGTERM, unix.SIGINT:
|
||||
if isOpen {
|
||||
wingmate.Log().Info().Msg("initiating shutdown...")
|
||||
close(trigger)
|
||||
wg.Add(1)
|
||||
go i.signalPump(wg, selfExit)
|
||||
isOpen = false
|
||||
}
|
||||
case unix.SIGCHLD:
|
||||
// do nothing
|
||||
select {
|
||||
case sigchld <- s:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
case <-selfExit:
|
||||
wingmate.Log().Warn().Msg("signal handler received completion flag")
|
||||
break signal
|
||||
}
|
||||
}
|
||||
|
||||
71
init/signal-pump.go
Normal file
71
init/signal-pump.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type status int
|
||||
|
||||
const (
|
||||
triggered status = iota
|
||||
expired
|
||||
)
|
||||
|
||||
func (i *Init) signalPump(wg *sync.WaitGroup, selfExit <-chan any) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
wingmate.Log().Info().Msg("signal pump completed")
|
||||
}()
|
||||
|
||||
if seStatus := i.sigTermPump(time.Now(), selfExit); seStatus == triggered {
|
||||
return
|
||||
}
|
||||
|
||||
i.sigKillPump(time.Now(), selfExit)
|
||||
}
|
||||
|
||||
func (i *Init) sigKillPump(startTime time.Time, selfExit <-chan any) {
|
||||
t := time.NewTicker(time.Millisecond * 200)
|
||||
defer t.Stop()
|
||||
|
||||
wingmate.Log().Info().Msg("start pumping SIGKILL signal")
|
||||
defer func() {
|
||||
wingmate.Log().Info().Msg("stop pumping SIGKILL signal")
|
||||
}()
|
||||
|
||||
for time.Since(startTime) < time.Second {
|
||||
_ = unix.Kill(-1, unix.SIGKILL)
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-selfExit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Init) sigTermPump(startTime time.Time, selfExit <-chan any) status {
|
||||
t := time.NewTicker(time.Millisecond * 100)
|
||||
defer t.Stop()
|
||||
|
||||
wingmate.Log().Info().Msg("start pumping SIGTERM signal")
|
||||
defer func() {
|
||||
wingmate.Log().Info().Msg("stop pumping SIGTERM signal")
|
||||
}()
|
||||
|
||||
for time.Since(startTime) < time.Duration(time.Second*4) {
|
||||
_ = unix.Kill(-1, unix.SIGTERM)
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-selfExit:
|
||||
return triggered
|
||||
}
|
||||
}
|
||||
|
||||
return expired
|
||||
}
|
||||
@@ -2,35 +2,68 @@ package init
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any) {
|
||||
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any, sigchld <-chan os.Signal) {
|
||||
var (
|
||||
ws unix.WaitStatus
|
||||
// pid int
|
||||
err error
|
||||
running bool
|
||||
flagged bool
|
||||
waitingForSignal bool
|
||||
)
|
||||
defer wg.Done()
|
||||
|
||||
defer func() {
|
||||
wingmate.Log().Info().Msg("waiter exiting...")
|
||||
}()
|
||||
|
||||
running = true
|
||||
flagged = true
|
||||
waitingForSignal = true
|
||||
wait:
|
||||
for {
|
||||
if running {
|
||||
if waitingForSignal {
|
||||
select {
|
||||
case <-runningFlag:
|
||||
wingmate.Log().Info().Msg("waiter received shutdown signal...")
|
||||
running = false
|
||||
case <-sigchld:
|
||||
waitingForSignal = false
|
||||
}
|
||||
|
||||
} else {
|
||||
select {
|
||||
case <-runningFlag:
|
||||
wingmate.Log().Info().Msg("waiter received shutdown signal...")
|
||||
running = false
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = unix.Wait4(-1, &ws, 0, nil); err != nil {
|
||||
if errors.Is(err, unix.ECHILD) {
|
||||
if !running {
|
||||
if flagged {
|
||||
close(sigHandlerFlag)
|
||||
flagged = false
|
||||
wingmate.Log().Warn().Msg("waiter: inner flag")
|
||||
}
|
||||
wingmate.Log().Warn().Msg("waiter: no child left")
|
||||
break wait
|
||||
}
|
||||
}
|
||||
|
||||
wingmate.Log().Warn().Msgf("Wait4 returns error: %+v", err)
|
||||
waitingForSignal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
logger.go
29
logger.go
@@ -1,9 +1,15 @@
|
||||
package wingmate
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
timeTag = "time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,13 +35,28 @@ func Log() logger.Log {
|
||||
}
|
||||
|
||||
func (w *wrapper) Info() logger.Content {
|
||||
return w.log.Info()
|
||||
return (*eventWrapper)(w.log.Info().Time(timeTag, time.Now()))
|
||||
}
|
||||
|
||||
func (w *wrapper) Warn() logger.Content {
|
||||
return w.log.Warn()
|
||||
return (*eventWrapper)(w.log.Warn().Time(timeTag, time.Now()))
|
||||
}
|
||||
|
||||
func (w *wrapper) Error() logger.Content {
|
||||
return w.log.Error()
|
||||
return (*eventWrapper)(w.log.Error().Time(timeTag, time.Now()))
|
||||
}
|
||||
|
||||
type eventWrapper zerolog.Event
|
||||
|
||||
func (w *eventWrapper) Msg(msg string) {
|
||||
(*zerolog.Event)(w).Msg(msg)
|
||||
}
|
||||
|
||||
func (w *eventWrapper) Msgf(format string, data ...any) {
|
||||
(*zerolog.Event)(w).Msgf(format, data...)
|
||||
}
|
||||
|
||||
func (w *eventWrapper) Str(key, value string) logger.Content {
|
||||
rv := (*zerolog.Event)(w).Str(key, value)
|
||||
return (*eventWrapper)(rv)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package logger
|
||||
type Content interface {
|
||||
Msg(string)
|
||||
Msgf(string, ...any)
|
||||
Str(string, string) Content
|
||||
}
|
||||
|
||||
type Level interface {
|
||||
|
||||
139
task/cron.go
Normal file
139
task/cron.go
Normal file
@@ -0,0 +1,139 @@
|
||||
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
|
||||
}
|
||||
112
task/task.go
Normal file
112
task/task.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||
)
|
||||
|
||||
type Tasks struct {
|
||||
services []wminit.ServiceTask
|
||||
crones []wminit.CronTask
|
||||
}
|
||||
|
||||
func NewTasks() *Tasks {
|
||||
return &Tasks{
|
||||
services: make([]wminit.ServiceTask, 0),
|
||||
crones: make([]wminit.CronTask, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *Tasks) AddV0Service(path string) {
|
||||
ts.services = append(ts.services, &ServiceTask{
|
||||
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},
|
||||
hasRun: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *Tasks) List() []wminit.Task {
|
||||
retval := make([]wminit.Task, 0, len(ts.services)+len(ts.crones))
|
||||
for _, s := range ts.services {
|
||||
retval = append(retval, s.(wminit.Task))
|
||||
}
|
||||
for _, c := range ts.crones {
|
||||
retval = append(retval, c.(wminit.Task))
|
||||
}
|
||||
return retval
|
||||
}
|
||||
|
||||
func (ts *Tasks) Services() []wminit.ServiceTask {
|
||||
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 ServiceTask struct {
|
||||
name string
|
||||
command []string
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Command() []string {
|
||||
retval := make([]string, len(t.command))
|
||||
copy(retval, t.command)
|
||||
return retval
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Environ() []string {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Setsid() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *ServiceTask) UserGroup() wminit.UserGroup {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Background() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *ServiceTask) WorkingDir() string {
|
||||
panic("not implemented")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *ServiceTask) Status() wminit.TaskStatus {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ServiceTask) AutoStart() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *ServiceTask) AutoRestart() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
79
task/task_test.go
Normal file
79
task/task_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServicesV0(t *testing.T) {
|
||||
service := "/path/to/executable"
|
||||
tasks := NewTasks()
|
||||
tasks.AddV0Service(service)
|
||||
|
||||
assert.Equal(t, tasks.Services()[0].Name(), service)
|
||||
assert.ElementsMatch(t, tasks.Services()[0].Command(), []string{service})
|
||||
}
|
||||
|
||||
func TestCronV0(t *testing.T) {
|
||||
cron := "/path/to/executable"
|
||||
tasks := NewTasks()
|
||||
tasks.AddV0Cron(CronSchedule{
|
||||
Minute: NewCronAnySpec(),
|
||||
Hour: NewCronAnySpec(),
|
||||
DoM: NewCronAnySpec(),
|
||||
Month: NewCronAnySpec(),
|
||||
DoW: NewCronAnySpec(),
|
||||
}, cron)
|
||||
|
||||
assert.Equal(t, tasks.Crones()[0].Name(), cron)
|
||||
assert.ElementsMatch(t, tasks.Crones()[0].Command(), []string{cron})
|
||||
}
|
||||
|
||||
func TestTasks_List(t *testing.T) {
|
||||
tasks := NewTasks()
|
||||
tasks.services = []wminit.ServiceTask{
|
||||
&ServiceTask{
|
||||
name: "one",
|
||||
command: []string{"/path/to/executable"},
|
||||
},
|
||||
&ServiceTask{
|
||||
name: "two",
|
||||
command: []string{"/path/to/executable"},
|
||||
},
|
||||
}
|
||||
tasks.crones = []wminit.CronTask{
|
||||
&Cron{
|
||||
CronSchedule: CronSchedule{
|
||||
Minute: NewCronAnySpec(),
|
||||
Hour: NewCronAnySpec(),
|
||||
DoM: NewCronAnySpec(),
|
||||
Month: NewCronAnySpec(),
|
||||
DoW: NewCronAnySpec(),
|
||||
},
|
||||
name: "cron-one",
|
||||
command: []string{"/path/to/executable"},
|
||||
},
|
||||
&Cron{
|
||||
CronSchedule: CronSchedule{
|
||||
Minute: NewCronAnySpec(),
|
||||
Hour: NewCronAnySpec(),
|
||||
DoM: NewCronAnySpec(),
|
||||
Month: NewCronAnySpec(),
|
||||
DoW: NewCronAnySpec(),
|
||||
},
|
||||
name: "cron-two",
|
||||
command: []string{"/path/to/executable"},
|
||||
},
|
||||
}
|
||||
|
||||
tl := tasks.List()
|
||||
tnames := make([]string, 0)
|
||||
testNames := []string{"one", "two", "cron-one", "cron-two"}
|
||||
|
||||
for _, ti := range tl {
|
||||
tnames = append(tnames, ti.Name())
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, testNames, tnames)
|
||||
}
|
||||
Reference in New Issue
Block a user