Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0546e259 | |||
| 438e48c6fe | |||
| 1827cf2e3e | |||
| 2474d3cddf | |||
| 61d735bbad | |||
| 6e84adbf16 | |||
| 630e4c2248 | |||
| bcb6435b4a | |||
| 3c0816f5f3 | |||
| b83c3acc30 | |||
| fe31698724 | |||
| dc6d575fae | |||
| a0dad29950 | |||
| 97d637ef2c | |||
| 6092629cb4 | |||
| 3bdca8c540 | |||
| f2bfd6e60b | |||
| a0134fa400 | |||
| 7db6f6f8f3 | |||
| a63646aab2 | |||
| 8f68c4ace9 | |||
| 6032b6c0c1 | |||
| a1d0360d46 | |||
| 2c9bc8b56d | |||
| 1926598c0f | |||
| cdc66a2c22 | |||
| 6a68209629 | |||
| fe465ad031 | |||
| 3dbac84f36 | |||
| db251da5f6 | |||
| 99436e54cd | |||
| a2f7dbca82 | |||
| 006f8278d7 | |||
| 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 |
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "Golang Dev",
|
||||||
|
"image": "golang-dev:1.22-bookworm-user",
|
||||||
|
"mounts": [
|
||||||
|
{
|
||||||
|
"source": "WingmateGoPath",
|
||||||
|
"target": "/go",
|
||||||
|
"type": "volume"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "WingmateGolangDevHome",
|
||||||
|
"target": "/home/golang",
|
||||||
|
"type": "volume"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"golang.go",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"ms-vscode.makefile-tools",
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/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>
|
||||||
59
Makefile
Normal file
59
Makefile
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
DESTDIR = /usr/local/bin
|
||||||
|
|
||||||
|
installs = install-dir
|
||||||
|
programs = wingmate pidproxy exec
|
||||||
|
ifdef TEST_BUILD
|
||||||
|
programs += oneshot spawner starter dummy bg
|
||||||
|
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
|
||||||
|
|
||||||
|
bg:
|
||||||
|
$(MAKE) -C cmd/experiment/bg 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
|
||||||
|
$(MAKE) -C cmd/experiment/bg 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
|
||||||
|
$(MAKE) -C cmd/experiment/bg DESTDIR=${DESTDIR} install
|
||||||
|
|
||||||
|
install-dir:
|
||||||
|
install -d ${DESTDIR}
|
||||||
155
README.md
155
README.md
@@ -1,2 +1,157 @@
|
|||||||
# wingmate
|
# wingmate
|
||||||
|
|
||||||
|
Wingmate is a process manager for services. It works like init. It starts and restarts services.
|
||||||
|
It also has cron feature. It is designed to run in a container/docker.
|
||||||
|
The Wingmate binary do not need any external dependency.
|
||||||
|
Just copy the binary, and exec from the entry point script.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Binaries
|
||||||
|
|
||||||
|
There are three binaries in this project: `wingmate`, `wmpidproxy`, and `wmexec`.
|
||||||
|
|
||||||
|
`wingmate` is the core binary. It reads config, starts, restarts services. It also
|
||||||
|
runs cron. Read the [configuration](#configuration) section for files needed to run
|
||||||
|
`wingmate`.
|
||||||
|
|
||||||
|
`wmpidproxy` is a helper binary for monitoring _legacy style_ service (fork, exit
|
||||||
|
initial proces, and continue in background). Read [here](#wingmate-pid-proxy-binary)
|
||||||
|
for further details about `wmpidproxy`.
|
||||||
|
|
||||||
|
`wmexec` is a helper binary for running process in different `user` or `group`.
|
||||||
|
It also useful for setting the process as process group leader.
|
||||||
|
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
|
||||||
|
|
||||||
|
## Building a container image based on wingmate image in Docker Hub
|
||||||
|
|
||||||
|
Wingmate has no dependency other than `alpine` base image, so you just need to copy
|
||||||
|
the binaries directly. If you have built your application into an `alpine` based image,
|
||||||
|
all you need to do is copy whichever binary you need, crontab file (if you use cron)
|
||||||
|
and add some shell script to glue them together. Here is a Dockerfile example.
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM suyono/wingmate:alpine as source
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
ADD --chmod=755 wingmate/ /etc/wingmate/
|
||||||
|
ADD --chmod=755 entry.sh /usr/local/bin/entry.sh
|
||||||
|
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/wingmate
|
||||||
|
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/wmpidproxy
|
||||||
|
COPY --from=source /usr/local/bin/wmexec /usr/local/bin/wmexec
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||||
|
CMD [ "/usr/local/bin/wingmate" ]
|
||||||
|
```
|
||||||
|
You can find some examples for shell script in [alpine docker](docker/alpine) and
|
||||||
|
[bookworm docker](docker/bookworm).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
When `wingmate` binary starts, it will look for some files. By default, it will
|
||||||
|
try to read the content of `/etc/wingmate` directory. You can change the directory
|
||||||
|
where it reads by setting `WINGMATE_CONFIG_PATH` environment variable. Wingmate supports
|
||||||
|
two format of configurations: yaml and shell script.
|
||||||
|
|
||||||
|
### YAML configuration
|
||||||
|
|
||||||
|
File structure:
|
||||||
|
```shell
|
||||||
|
/etc
|
||||||
|
└── wingmate
|
||||||
|
└── wingmate.yaml
|
||||||
|
```
|
||||||
|
Wingmate will parse the `wingmate.yaml` file and start services and crones based on the content
|
||||||
|
of the yaml file. Please read [wingmate.yaml.md](wingmate.yaml.md) for details on
|
||||||
|
the structure of yaml configuration file and some examples.
|
||||||
|
|
||||||
|
### Shell script configuration
|
||||||
|
|
||||||
|
Files and directories structure:
|
||||||
|
```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.
|
||||||
|
It is recommended to write a shell script and put the path to shell script in
|
||||||
|
the command part.
|
||||||
|
|
||||||
|
**Note: It is recommended to use the yaml format instead of shell script. In order to avoid less
|
||||||
|
obvious mistake when writing shell script.**
|
||||||
|
|
||||||
|
# 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)
|
||||||
37
cmd/cli/splitargs.go
Normal file
37
cmd/cli/splitargs.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SplitArgs(args []string) ([]string, []string, error) {
|
||||||
|
var (
|
||||||
|
i int
|
||||||
|
arg string
|
||||||
|
selfArgs []string
|
||||||
|
childArgs []string
|
||||||
|
)
|
||||||
|
found := false
|
||||||
|
for i, arg = range args {
|
||||||
|
if arg == "--" {
|
||||||
|
found = true
|
||||||
|
if i+1 == len(args) {
|
||||||
|
return nil, nil, errors.New("invalid argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args[i+1:]) == 0 {
|
||||||
|
return nil, nil, errors.New("invalid argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
selfArgs = args[:i]
|
||||||
|
childArgs = args[i+1:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, nil, errors.New("invalid argument")
|
||||||
|
}
|
||||||
|
return selfArgs, childArgs, nil
|
||||||
|
|
||||||
|
}
|
||||||
7
cmd/cli/splitargs_test.go
Normal file
7
cmd/cli/splitargs_test.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSplitArgs(t *testing.T) {
|
||||||
|
SplitArgs([]string{"wmexec", "--user", "1200", "--", "wmspawner"})
|
||||||
|
}
|
||||||
45
cmd/cli/version.go
Normal file
45
cmd/cli/version.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Version string
|
||||||
|
|
||||||
|
const versionFlag = "version"
|
||||||
|
|
||||||
|
func (v Version) Print() {
|
||||||
|
fmt.Print(v)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) Cmd(cmd *cobra.Command) {
|
||||||
|
cmd.AddCommand(&cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
v.Print()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) Flag(cmd *cobra.Command) {
|
||||||
|
v.FlagSet(cmd.PersistentFlags())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) FlagSet(fs *pflag.FlagSet) {
|
||||||
|
viper.SetDefault(versionFlag, false)
|
||||||
|
fs.Bool(versionFlag, false, "print version")
|
||||||
|
_ = viper.BindPFlag(versionFlag, fs.Lookup(versionFlag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) FlagHook() {
|
||||||
|
if viper.GetBool(versionFlag) {
|
||||||
|
v.Print()
|
||||||
|
}
|
||||||
|
}
|
||||||
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:
|
||||||
|
echo "dev" > 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
|
||||||
|
}
|
||||||
143
cmd/exec/exec.go
Normal file
143
cmd/exec/exec.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
|
||||||
|
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execApp struct {
|
||||||
|
childArgs []string
|
||||||
|
err error
|
||||||
|
version cli.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
setsidFlag = "setsid"
|
||||||
|
EnvSetsid = "SETSID"
|
||||||
|
userFlag = "user"
|
||||||
|
EnvUser = "USER"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
//go:embed version.txt
|
||||||
|
version string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
selfArgs []string
|
||||||
|
childArgs []string
|
||||||
|
app *execApp
|
||||||
|
rootCmd *cobra.Command
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
app = &execApp{
|
||||||
|
version: cli.Version(version),
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "wmexec",
|
||||||
|
SilenceUsage: true,
|
||||||
|
RunE: app.execCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
app.version.Flag(rootCmd)
|
||||||
|
|
||||||
|
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||||
|
_ = viper.BindEnv(EnvUser)
|
||||||
|
_ = viper.BindEnv(EnvSetsid)
|
||||||
|
viper.SetDefault(EnvSetsid, false)
|
||||||
|
viper.SetDefault(EnvUser, "")
|
||||||
|
|
||||||
|
app.version.Cmd(rootCmd)
|
||||||
|
|
||||||
|
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err != nil {
|
||||||
|
selfArgs = os.Args
|
||||||
|
}
|
||||||
|
app.childArgs = wmenv.ExpandEnv(os.Environ(), childArgs)
|
||||||
|
app.err = err
|
||||||
|
|
||||||
|
rootCmd.SetArgs(selfArgs[1:])
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *execApp) execCmd(_ *cobra.Command, _ []string) error {
|
||||||
|
e.version.FlagHook()
|
||||||
|
|
||||||
|
if e.err != nil {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
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(e.childArgs[0]); err != nil {
|
||||||
|
if !errors.Is(err, exec.ErrDot) {
|
||||||
|
return fmt.Errorf("lookpath: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = unix.Exec(path, e.childArgs, os.Environ()); err != nil {
|
||||||
|
return fmt.Errorf("exec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
10
cmd/experiment/.gitignore
vendored
Normal file
10
cmd/experiment/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/dummy/dummy
|
||||||
|
/dummy/version.txt
|
||||||
|
/starter/starter
|
||||||
|
/starter/version.txt
|
||||||
|
/oneshot/oneshot
|
||||||
|
/oneshot/version.txt
|
||||||
|
/spawner/spawner
|
||||||
|
/spawner/version.txt
|
||||||
|
/bg/bg
|
||||||
|
/bg/version.txt
|
||||||
10
cmd/experiment/bg/Makefile
Normal file
10
cmd/experiment/bg/Makefile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
all:
|
||||||
|
git describe > version.txt
|
||||||
|
go build -v
|
||||||
|
|
||||||
|
clean:
|
||||||
|
echo "dev" > version.txt
|
||||||
|
go clean -i -cache -testcache
|
||||||
|
|
||||||
|
install:
|
||||||
|
install bg ${DESTDIR}/wmbg
|
||||||
67
cmd/experiment/bg/background.go
Normal file
67
cmd/experiment/bg/background.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logPathFlag = "log-path"
|
||||||
|
pidFileFlag = "pid-file"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
logPath string
|
||||||
|
pidFilePath string
|
||||||
|
name string
|
||||||
|
pause uint
|
||||||
|
lf *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
pflag.StringVarP(&logPath, logPathFlag, "l", "/var/log/wmbg.log", "log file path")
|
||||||
|
pflag.StringVarP(&pidFilePath, pidFileFlag, "p", "/var/run/wmbg.pid", "pid file path")
|
||||||
|
pflag.StringVar(&name, "name", "no-name", "process name")
|
||||||
|
pflag.UintVar(&pause, "pause", 5, "pause interval")
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
if lf, err = os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err != nil {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = lf.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.SetOutput(lf)
|
||||||
|
log.Printf("starting process %s with pause interval %d", name, pause)
|
||||||
|
if err = writePid(pidFilePath); err != nil {
|
||||||
|
log.Printf("failed to write pid file: %+v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(pause) * time.Second)
|
||||||
|
log.Printf("process %s finished", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePid(path string) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
pf *os.File
|
||||||
|
)
|
||||||
|
|
||||||
|
if pf, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644); err != nil {
|
||||||
|
return fmt.Errorf("opening pid file for write: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = pf.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err = fmt.Fprintf(pf, "%d", unix.Getpid()); err != nil {
|
||||||
|
return fmt.Errorf("writing pid to the pid file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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:
|
||||||
|
echo "dev" > 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:
|
||||||
|
echo "dev" > version.txt
|
||||||
|
go clean -i -cache -testcache
|
||||||
|
|
||||||
|
install:
|
||||||
|
install oneshot ${DESTDIR}/wmoneshot
|
||||||
79
cmd/experiment/oneshot/oneshot.go
Normal file
79
cmd/experiment/oneshot/oneshot.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvLog = "LOG"
|
||||||
|
EnvLogMessage = "LOG_MESSAGE"
|
||||||
|
EnvDefaultLogMessage = "oneshot executed"
|
||||||
|
EnvInstanceNum = "INSTANCE_NUM"
|
||||||
|
EnvDefaultInstances = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||||
|
viper.BindEnv(EnvLog)
|
||||||
|
viper.BindEnv(EnvLogMessage)
|
||||||
|
viper.BindEnv(EnvInstanceNum)
|
||||||
|
viper.SetDefault(EnvLogMessage, EnvDefaultLogMessage)
|
||||||
|
viper.SetDefault(EnvInstanceNum, EnvDefaultInstances)
|
||||||
|
|
||||||
|
_, childArgs, err := cli.SplitArgs(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("splitargs: %+v", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
logPath := viper.GetString(EnvLog)
|
||||||
|
logMessage := viper.GetString(EnvLogMessage)
|
||||||
|
log.Println("log path:", logPath)
|
||||||
|
if logPath != "" {
|
||||||
|
var (
|
||||||
|
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, wingmate.Time|wingmate.Caller); err == nil {
|
||||||
|
wingmate.Log().Info().Msg(logMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(childArgs) > 0 {
|
||||||
|
StartInstances(childArgs[0], childArgs[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartInstances(exePath string, args ...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, args...)
|
||||||
|
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:
|
||||||
|
echo "dev" > 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, "--", "wmdummy")
|
||||||
|
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:
|
||||||
|
echo "dev" > version.txt
|
||||||
|
go clean -i -cache -testcache
|
||||||
|
|
||||||
|
install:
|
||||||
|
install starter ${DESTDIR}/wmstarter
|
||||||
110
cmd/experiment/starter/starter.go
Normal file
110
cmd/experiment/starter/starter.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DummyPath = "/usr/local/bin/wmdummy"
|
||||||
|
EnvDummyPath = "DUMMY_PATH"
|
||||||
|
NoWaitFlag = "no-wait"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
stdout io.ReadCloser
|
||||||
|
stderr io.ReadCloser
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
err error
|
||||||
|
exePath string
|
||||||
|
selfArgs []string
|
||||||
|
childArgs []string
|
||||||
|
flagSet *pflag.FlagSet
|
||||||
|
noWait bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
)
|
||||||
|
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err == nil {
|
||||||
|
flagSet = pflag.NewFlagSet(selfArgs[0], pflag.ExitOnError)
|
||||||
|
flagSet.Count(NoWaitFlag, "do not wait for the child process")
|
||||||
|
if err = flagSet.Parse(selfArgs[1:]); err != nil {
|
||||||
|
log.Printf("invalid argument: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flagSet = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
|
||||||
|
flagSet.Count(NoWaitFlag, "do not wait for the child process")
|
||||||
|
if err = flagSet.Parse(os.Args[1:]); err != nil {
|
||||||
|
log.Printf("invalid argument: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = viper.BindPFlag(NoWaitFlag, flagSet.Lookup(NoWaitFlag))
|
||||||
|
if viper.GetInt(NoWaitFlag) > 0 {
|
||||||
|
noWait = true
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetEnvPrefix(wingmate.EnvPrefix)
|
||||||
|
_ = viper.BindEnv(EnvDummyPath)
|
||||||
|
viper.SetDefault(EnvDummyPath, DummyPath)
|
||||||
|
|
||||||
|
exePath = viper.GetString(EnvDummyPath)
|
||||||
|
|
||||||
|
if len(childArgs) > 0 {
|
||||||
|
cmd = exec.Command(childArgs[0], childArgs[1:]...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(exePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !noWait {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !noWait {
|
||||||
|
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:
|
||||||
|
echo "dev" > version.txt
|
||||||
|
go clean -i -cache -testcache
|
||||||
|
|
||||||
|
install:
|
||||||
|
install pidproxy ${DESTDIR}/wmpidproxy
|
||||||
168
cmd/pidproxy/pidproxy.go
Normal file
168
cmd/pidproxy/pidproxy.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
|
||||||
|
"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"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pidProxyApp struct {
|
||||||
|
childArgs []string
|
||||||
|
err error
|
||||||
|
version cli.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pidFileFlag = "pid-file"
|
||||||
|
EnvStartSecs = "STARTSECS"
|
||||||
|
EnvDefaultStartSecs = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
//go:embed version.txt
|
||||||
|
version string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
selfArgs []string
|
||||||
|
childArgs []string
|
||||||
|
err error
|
||||||
|
app *pidProxyApp
|
||||||
|
rootCmd *cobra.Command
|
||||||
|
)
|
||||||
|
|
||||||
|
app = &pidProxyApp{
|
||||||
|
version: cli.Version(version),
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "wmpidproxy",
|
||||||
|
SilenceUsage: true,
|
||||||
|
RunE: app.pidProxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
app.version.Flag(rootCmd)
|
||||||
|
app.version.Cmd(rootCmd)
|
||||||
|
|
||||||
|
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err != nil {
|
||||||
|
selfArgs = os.Args
|
||||||
|
}
|
||||||
|
app.childArgs = childArgs
|
||||||
|
app.err = err
|
||||||
|
|
||||||
|
rootCmd.SetArgs(selfArgs[1:])
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pidProxyApp) pidProxy(cmd *cobra.Command, args []string) error {
|
||||||
|
p.version.FlagHook()
|
||||||
|
|
||||||
|
pidfile := viper.GetString(pidFileFlag)
|
||||||
|
log.Printf("%s %v", pidfile, p.childArgs)
|
||||||
|
if len(p.childArgs) > 1 {
|
||||||
|
go p.startProcess(p.childArgs[0], p.childArgs[1:]...)
|
||||||
|
} else {
|
||||||
|
go p.startProcess(p.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 = p.readPid(pidfile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = unix.Kill(pid, syscall.Signal(0)); err != nil {
|
||||||
|
if !errors.Is(err, unix.ESRCH) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break check
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
case <-sc:
|
||||||
|
if pid, err = p.readPid(pidfile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = unix.Kill(pid, unix.SIGTERM); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pidProxyApp) 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 (p *pidProxyApp) 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:
|
||||||
|
echo "dev" > version.txt
|
||||||
|
go clean -i -cache -testcache
|
||||||
|
|
||||||
|
install:
|
||||||
|
install wingmate ${DESTDIR}/wingmate
|
||||||
|
|
||||||
87
cmd/wingmate/bridge.go
Normal file
87
cmd/wingmate/bridge.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/config"
|
||||||
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wConfig struct {
|
||||||
|
tasks *task.Tasks
|
||||||
|
config *config.Config
|
||||||
|
viperMtx *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wConfig) Tasks() wminit.Tasks {
|
||||||
|
return c.tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wConfig) Reload() error {
|
||||||
|
//NOTE: for future use when reloading is possible
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert(cfg *config.Config) *wConfig {
|
||||||
|
retval := &wConfig{
|
||||||
|
tasks: task.NewTasks(),
|
||||||
|
config: cfg,
|
||||||
|
viperMtx: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range cfg.Service {
|
||||||
|
st := task.NewServiceTask(s.Name).SetCommand(s.Command...).SetEnv(s.Environ...)
|
||||||
|
st.SetFallbackEnv(s.FallbackEnv...).SetFlagSetsid(s.Setsid).SetWorkingDir(s.WorkingDir)
|
||||||
|
st.SetUser(s.User).SetGroup(s.Group).SetStartSecs(s.StartSecs).SetPidFile(s.PidFile)
|
||||||
|
st.SetConfig(cfg)
|
||||||
|
retval.tasks.AddService(st)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range cfg.ServiceV0 {
|
||||||
|
retval.tasks.AddV0Service(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var schedule task.CronSchedule
|
||||||
|
for _, c := range cfg.CronV0 {
|
||||||
|
schedule = configToTaskCronSchedule(c.CronSchedule)
|
||||||
|
retval.tasks.AddV0Cron(schedule, c.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cfg.Cron {
|
||||||
|
schedule = configToTaskCronSchedule(c.CronSchedule)
|
||||||
|
|
||||||
|
ct := task.NewCronTask(c.Name).SetCommand(c.Command...).SetEnv(c.Environ...)
|
||||||
|
ct.SetFallbackEnv(c.FallbackEnv...).SetFlagSetsid(c.Setsid).SetWorkingDir(c.WorkingDir)
|
||||||
|
ct.SetUser(c.User).SetGroup(c.Group)
|
||||||
|
ct.SetSchedule(c.Schedule, schedule)
|
||||||
|
ct.SetConfig(cfg)
|
||||||
|
|
||||||
|
retval.tasks.AddCron(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
func configToTaskCronSchedule(cfgSchedule config.CronSchedule) (taskSchedule task.CronSchedule) {
|
||||||
|
taskSchedule.Minute = configToTaskCronTimeSpec(cfgSchedule.Minute)
|
||||||
|
taskSchedule.Hour = configToTaskCronTimeSpec(cfgSchedule.Hour)
|
||||||
|
taskSchedule.DoM = configToTaskCronTimeSpec(cfgSchedule.DoM)
|
||||||
|
taskSchedule.Month = configToTaskCronTimeSpec(cfgSchedule.Month)
|
||||||
|
taskSchedule.DoW = configToTaskCronTimeSpec(cfgSchedule.DoW)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func configToTaskCronTimeSpec(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")
|
||||||
|
}
|
||||||
37
cmd/wingmate/wingmate.go
Normal file
37
cmd/wingmate/wingmate.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/config"
|
||||||
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed version.txt
|
||||||
|
version string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
cfg *config.Config
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = wingmate.NewLog(os.Stderr, wingmate.Time|wingmate.Caller)
|
||||||
|
wingmate.SetGlobalLevel(wingmate.InfoLevel)
|
||||||
|
config.SetVersion(version)
|
||||||
|
config.ParseFlags()
|
||||||
|
|
||||||
|
wingmate.Log().Info().Msgf("starting wingmate version %s", viper.GetString(config.WingmateVersion))
|
||||||
|
|
||||||
|
if cfg, err = config.Read(); err != nil {
|
||||||
|
wingmate.Log().Fatal().Err(err).Msg("failed to read config")
|
||||||
|
}
|
||||||
|
|
||||||
|
initCfg := convert(cfg)
|
||||||
|
wminit.NewInit(initCfg).Start()
|
||||||
|
}
|
||||||
23
cmd/wingmate/wingmate_test.go
Normal file
23
cmd/wingmate/wingmate_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntry_configPathEnv(t *testing.T) {
|
||||||
|
_ = os.Setenv("WINGMATE_CONFIG_PATH", "/Volumes/Source/go/src/gitea.suyono.dev/suyono/wingmate/docker/bookworm/etc/wingmate")
|
||||||
|
defer func() {
|
||||||
|
_ = os.Unsetenv("WINGMATE_CONFIG_PATH")
|
||||||
|
}()
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntry_configPathPFlag(t *testing.T) {
|
||||||
|
os.Args = []string{"wingmate", "--config", "/workspaces/wingmate/docker/bookworm-newconfig/etc/wingmate"}
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntry(t *testing.T) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
221
config/config.go
Normal file
221
config/config.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvPrefix = "WINGMATE"
|
||||||
|
PathConfig = "config_path"
|
||||||
|
DefaultConfigPath = "/etc/wingmate"
|
||||||
|
ServiceDirName = "service"
|
||||||
|
CrontabFileName = "crontab"
|
||||||
|
WingmateConfigFileName = "wingmate"
|
||||||
|
WingmateConfigFileFormat = "yaml"
|
||||||
|
WingmateVersion = "APP_VERSION"
|
||||||
|
PidProxyPathConfig = "pidproxy_path"
|
||||||
|
PidProxyPathDefault = "wmpidproxy"
|
||||||
|
ExecPathConfig = "exec_path"
|
||||||
|
ExecPathDefault = "wmexec"
|
||||||
|
versionTrimRightCutSet = "\r\n "
|
||||||
|
WMPidProxyPathFlag = "pid-proxy"
|
||||||
|
WMExecPathFlag = "exec"
|
||||||
|
PathConfigFlag = "config"
|
||||||
|
GlobalLogConfig = "log_level"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ServiceV0 []string
|
||||||
|
CronV0 []*Cron
|
||||||
|
Service []ServiceTask
|
||||||
|
Cron []CronTask
|
||||||
|
viperMtx *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Command []string `mapstructure:"command"`
|
||||||
|
Environ []string `mapstructure:"environ"`
|
||||||
|
FallbackEnv []string `mapstructure:"fallback_env"`
|
||||||
|
Setsid bool `mapstructure:"setsid"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Group string `mapstructure:"group"`
|
||||||
|
WorkingDir string `mapstructure:"working_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceTask struct {
|
||||||
|
Task `mapstructure:",squash"`
|
||||||
|
Name string `mapstructure:"-"`
|
||||||
|
Background bool `mapstructure:"background"`
|
||||||
|
PidFile string `mapstructure:"pidfile"`
|
||||||
|
StartSecs uint `mapstructure:"startsecs"`
|
||||||
|
AutoStart bool `mapstructure:"autostart"`
|
||||||
|
AutoRestart bool `mapstructure:"autorestart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronTask struct {
|
||||||
|
CronSchedule `mapstructure:"-"`
|
||||||
|
Task `mapstructure:",squash"`
|
||||||
|
Name string `mapstructure:"-"`
|
||||||
|
Schedule string `mapstructure:"schedule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronSchedule struct {
|
||||||
|
Minute CronTimeSpec
|
||||||
|
Hour CronTimeSpec
|
||||||
|
DoM CronTimeSpec
|
||||||
|
Month CronTimeSpec
|
||||||
|
DoW CronTimeSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVersion(version string) {
|
||||||
|
version = strings.TrimRight(version, versionTrimRightCutSet)
|
||||||
|
viper.Set(WingmateVersion, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read() (*Config, error) {
|
||||||
|
viper.SetEnvPrefix(EnvPrefix)
|
||||||
|
_ = viper.BindEnv(PathConfig)
|
||||||
|
_ = viper.BindEnv(PidProxyPathConfig)
|
||||||
|
_ = viper.BindEnv(ExecPathConfig)
|
||||||
|
_ = viper.BindEnv(GlobalLogConfig)
|
||||||
|
viper.SetDefault(PathConfig, DefaultConfigPath)
|
||||||
|
viper.SetDefault(PidProxyPathConfig, PidProxyPathDefault)
|
||||||
|
viper.SetDefault(ExecPathConfig, ExecPathDefault)
|
||||||
|
viper.SetDefault(GlobalLogConfig, wingmate.InfoLevelStr)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dirent []os.DirEntry
|
||||||
|
err error
|
||||||
|
svcdir string
|
||||||
|
serviceAvailable bool
|
||||||
|
cronAvailable bool
|
||||||
|
wingmateConfigAvailable bool
|
||||||
|
cron []*Cron
|
||||||
|
crontabfile string
|
||||||
|
services []ServiceTask
|
||||||
|
crones []CronTask
|
||||||
|
)
|
||||||
|
|
||||||
|
wingmate.SetGlobalLevelStr(viper.GetString(GlobalLogConfig))
|
||||||
|
serviceAvailable = false
|
||||||
|
cronAvailable = false
|
||||||
|
outConfig := &Config{
|
||||||
|
viperMtx: &sync.Mutex{},
|
||||||
|
ServiceV0: make([]string, 0),
|
||||||
|
}
|
||||||
|
configPath := viper.GetString(PathConfig)
|
||||||
|
svcdir = filepath.Join(configPath, ServiceDirName)
|
||||||
|
dirent, err = os.ReadDir(svcdir)
|
||||||
|
if err != nil {
|
||||||
|
wingmate.Log().Warn().Msgf("encounter error when reading service directory %s: %+v", svcdir, err)
|
||||||
|
}
|
||||||
|
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.ServiceV0 = append(outConfig.ServiceV0, svcPath)
|
||||||
|
} else {
|
||||||
|
wingmate.Log().Error().Msgf("checking executable access for %s: %+v", svcPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crontabfile = filepath.Join(configPath, CrontabFileName)
|
||||||
|
cron, err = readCrontab(crontabfile)
|
||||||
|
if len(cron) > 0 {
|
||||||
|
outConfig.CronV0 = cron
|
||||||
|
cronAvailable = true
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
wingmate.Log().Warn().Msgf("encounter error when reading crontab %s: %+v", crontabfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return outConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetAppVersion() string {
|
||||||
|
c.viperMtx.Lock()
|
||||||
|
defer c.viperMtx.Unlock()
|
||||||
|
|
||||||
|
return viper.GetString(WingmateVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WMPidProxyPath() string {
|
||||||
|
c.viperMtx.Lock()
|
||||||
|
defer c.viperMtx.Unlock()
|
||||||
|
|
||||||
|
return viper.GetString(PidProxyPathConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WMPidProxyCheckVersion() error {
|
||||||
|
var (
|
||||||
|
binVersion string
|
||||||
|
appVersion string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if binVersion, err = getVersion(c.WMPidProxyPath()); err != nil {
|
||||||
|
return fmt.Errorf("get wmpidproxy version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appVersion = c.GetAppVersion()
|
||||||
|
if appVersion != binVersion {
|
||||||
|
return fmt.Errorf("wmpidproxy version mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WMExecPath() string {
|
||||||
|
c.viperMtx.Lock()
|
||||||
|
defer c.viperMtx.Unlock()
|
||||||
|
|
||||||
|
return viper.GetString(ExecPathConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WMExecCheckVersion() error {
|
||||||
|
var (
|
||||||
|
binVersion string
|
||||||
|
appVersion string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if binVersion, err = getVersion(c.WMExecPath()); err != nil {
|
||||||
|
return fmt.Errorf("get wmexec version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appVersion = c.GetAppVersion()
|
||||||
|
if appVersion != binVersion {
|
||||||
|
return fmt.Errorf("wmexec version mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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(PathConfig, 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, wingmate.Caller|wingmate.Time)
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
243
config/crontab.go
Normal file
243
config/crontab.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CronTimeSpec interface {
|
||||||
|
//Type() wingmate.CronTimeType
|
||||||
|
//Match(uint8) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cron struct {
|
||||||
|
CronSchedule
|
||||||
|
Command string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cronField int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CrontabEntryRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||||
|
CrontabCommentLineRegexPattern = `^\s*#.*$`
|
||||||
|
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
|
||||||
|
CrontabSubMatchLen = 7
|
||||||
|
|
||||||
|
minute cronField = iota
|
||||||
|
hour
|
||||||
|
dom
|
||||||
|
month
|
||||||
|
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
|
||||||
|
parts []string
|
||||||
|
retval []*Cron
|
||||||
|
)
|
||||||
|
|
||||||
|
if file, err = os.Open(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
retval = make([]*Cron, 0)
|
||||||
|
scanner = bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line = scanner.Text()
|
||||||
|
|
||||||
|
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{}
|
||||||
|
if err = c.setField(minute, parts[1]); err != nil {
|
||||||
|
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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.setField(dom, parts[3]); err != nil {
|
||||||
|
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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.setField(dow, parts[5]); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Command = parts[6]
|
||||||
|
|
||||||
|
retval = append(retval, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldRange struct {
|
||||||
|
min int
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRange(min, max int) *fieldRange {
|
||||||
|
return &fieldRange{
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fieldRange) valid(u uint8) bool {
|
||||||
|
i := int(u)
|
||||||
|
|
||||||
|
return i >= f.min && i <= f.max
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cron) setField(field cronField, input string) error {
|
||||||
|
var (
|
||||||
|
fr *fieldRange
|
||||||
|
cField *CronTimeSpec
|
||||||
|
err error
|
||||||
|
parsed64 uint64
|
||||||
|
parsed uint8
|
||||||
|
multi []uint8
|
||||||
|
current uint8
|
||||||
|
multiStr []string
|
||||||
|
)
|
||||||
|
switch field {
|
||||||
|
case minute:
|
||||||
|
fr = newRange(0, 59)
|
||||||
|
cField = &c.Minute
|
||||||
|
case hour:
|
||||||
|
fr = newRange(0, 23)
|
||||||
|
cField = &c.Hour
|
||||||
|
case dom:
|
||||||
|
fr = newRange(1, 31)
|
||||||
|
cField = &c.DoM
|
||||||
|
case month:
|
||||||
|
fr = newRange(1, 12)
|
||||||
|
cField = &c.Month
|
||||||
|
case dow:
|
||||||
|
fr = newRange(0, 6)
|
||||||
|
cField = &c.DoW
|
||||||
|
default:
|
||||||
|
return errors.New("invalid cron field descriptor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if input == "*" {
|
||||||
|
*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
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
|
||||||
|
for fr.valid(current) {
|
||||||
|
multi = append(multi, current)
|
||||||
|
current += parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
*cField = &SpecMultiOccurrence{
|
||||||
|
values: multi,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
multiStr = strings.Split(input, ",")
|
||||||
|
if len(multiStr) > 1 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
if !fr.valid(parsed) {
|
||||||
|
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
multi = append(multi, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
if !fr.valid(parsed) {
|
||||||
|
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
*cField = &SpecExact{
|
||||||
|
value: parsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecAny struct{}
|
||||||
|
|
||||||
|
type SpecExact struct {
|
||||||
|
value uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SpecExact) Value() uint8 {
|
||||||
|
return e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecMultiOccurrence struct {
|
||||||
|
values []uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SpecMultiOccurrence) Values() []uint8 {
|
||||||
|
out := make([]uint8, len(m.values))
|
||||||
|
copy(out, m.values)
|
||||||
|
return out
|
||||||
|
}
|
||||||
129
config/crontab_test.go
Normal file
129
config/crontab_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
crontabFileName = "crontab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCrontab(t *testing.T) {
|
||||||
|
type testEntry struct {
|
||||||
|
name string
|
||||||
|
crontab string
|
||||||
|
wantErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
|
||||||
|
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 * * /path/to/executable
|
||||||
|
76 13 3,5,7 * * /path/to/executable
|
||||||
|
*/75 13 3,5,7 * * /path/to/executable
|
||||||
|
*/5 13 3,x,7 * * /path/to/executable
|
||||||
|
*/5 13 3,5,67 * * /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"))
|
||||||
|
}
|
||||||
26
config/flags.go
Normal file
26
config/flags.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseFlags() {
|
||||||
|
version := cli.Version(fmt.Sprintln(viper.GetString(WingmateVersion)))
|
||||||
|
version.FlagSet(pflag.CommandLine)
|
||||||
|
|
||||||
|
pflag.String(WMPidProxyPathFlag, "", "wmpidproxy path")
|
||||||
|
pflag.String(WMExecPathFlag, "", "wmexec path")
|
||||||
|
pflag.StringP(PathConfigFlag, "c", "", "config path")
|
||||||
|
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
_ = viper.BindPFlag(PathConfig, pflag.CommandLine.Lookup(PathConfigFlag))
|
||||||
|
_ = viper.BindPFlag(PidProxyPathConfig, pflag.CommandLine.Lookup(WMPidProxyPathFlag))
|
||||||
|
_ = viper.BindPFlag(ExecPathConfig, pflag.CommandLine.Lookup(WMExecPathFlag))
|
||||||
|
|
||||||
|
version.FlagHook()
|
||||||
|
}
|
||||||
37
config/util.go
Normal file
37
config/util.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVersion(binPath string) (string, error) {
|
||||||
|
var (
|
||||||
|
outBytes []byte
|
||||||
|
err error
|
||||||
|
output string
|
||||||
|
stdout io.ReadCloser
|
||||||
|
n int
|
||||||
|
)
|
||||||
|
cmd := exec.Command(binPath, "version")
|
||||||
|
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||||
|
return "", fmt.Errorf("setting up stdout reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
return "", fmt.Errorf("starting process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outBytes = make([]byte, 1024)
|
||||||
|
if n, err = stdout.Read(outBytes); err != nil {
|
||||||
|
return "", fmt.Errorf("reading stdout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = cmd.Wait()
|
||||||
|
|
||||||
|
output = string(outBytes[:n])
|
||||||
|
output = strings.TrimRight(output, versionTrimRightCutSet)
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
203
config/yaml.go
Normal file
203
config/yaml.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CrontabScheduleRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s*$`
|
||||||
|
CrontabScheduleSubMatchLen = 6
|
||||||
|
ServiceConfigGroup = "service"
|
||||||
|
CronConfigGroup = "cron"
|
||||||
|
ServiceKeyFormat = "service.%s"
|
||||||
|
CronKeyFormat = "cron.%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
crontabScheduleRegex = regexp.MustCompile(CrontabScheduleRegexPattern)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
//findUtils *FindUtils
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
if cronTask.CronSchedule, err = parseYamlSchedule(cronTask.Schedule); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("parsing cron schedule: %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
crones = append(crones, cronTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, crones, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseYamlSchedule(input string) (schedule CronSchedule, err error) {
|
||||||
|
var (
|
||||||
|
parts []string
|
||||||
|
pSched *CronSchedule
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = crontabScheduleRegex.FindStringSubmatch(input)
|
||||||
|
if len(parts) != CrontabScheduleSubMatchLen {
|
||||||
|
return schedule, fmt.Errorf("invalid schedule: %s", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pSched = &schedule
|
||||||
|
if err = pSched.setField(minute, parts[1]); err != nil {
|
||||||
|
return schedule, fmt.Errorf("error parsing Minute field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pSched.setField(hour, parts[2]); err != nil {
|
||||||
|
return schedule, fmt.Errorf("error parsing Hour field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pSched.setField(dom, parts[3]); err != nil {
|
||||||
|
return schedule, fmt.Errorf("error parsing Day of Month field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pSched.setField(month, parts[4]); err != nil {
|
||||||
|
return schedule, fmt.Errorf("error parsing Month field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = pSched.setField(dow, parts[5]); err != nil {
|
||||||
|
return schedule, fmt.Errorf("error parsing Day of Week field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronSchedule) setField(field cronField, input string) error {
|
||||||
|
var (
|
||||||
|
fr *fieldRange
|
||||||
|
cField *CronTimeSpec
|
||||||
|
err error
|
||||||
|
parsed64 uint64
|
||||||
|
parsed uint8
|
||||||
|
multi []uint8
|
||||||
|
current uint8
|
||||||
|
multiStr []string
|
||||||
|
)
|
||||||
|
switch field {
|
||||||
|
case minute:
|
||||||
|
fr = newRange(0, 59)
|
||||||
|
cField = &c.Minute
|
||||||
|
case hour:
|
||||||
|
fr = newRange(0, 23)
|
||||||
|
cField = &c.Hour
|
||||||
|
case dom:
|
||||||
|
fr = newRange(1, 31)
|
||||||
|
cField = &c.DoM
|
||||||
|
case month:
|
||||||
|
fr = newRange(1, 12)
|
||||||
|
cField = &c.Month
|
||||||
|
case dow:
|
||||||
|
fr = newRange(0, 6)
|
||||||
|
cField = &c.DoW
|
||||||
|
default:
|
||||||
|
return errors.New("invalid cron field descriptor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if input == "*" {
|
||||||
|
*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
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
|
||||||
|
for fr.valid(current) {
|
||||||
|
multi = append(multi, current)
|
||||||
|
current += parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
*cField = &SpecMultiOccurrence{
|
||||||
|
values: multi,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
multiStr = strings.Split(input, ",")
|
||||||
|
if len(multiStr) > 1 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
if !fr.valid(parsed) {
|
||||||
|
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
multi = append(multi, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
*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)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = uint8(parsed64)
|
||||||
|
if !fr.valid(parsed) {
|
||||||
|
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
*cField = &SpecExact{
|
||||||
|
value: parsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
258
config/yaml_test.go
Normal file
258
config/yaml_test.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const configName = "wingmate.yaml"
|
||||||
|
|
||||||
|
func TestYaml(t *testing.T) {
|
||||||
|
type testEntry struct {
|
||||||
|
name string
|
||||||
|
config string
|
||||||
|
wantErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
|
||||||
|
tests := []testEntry{
|
||||||
|
{
|
||||||
|
name: "positive",
|
||||||
|
config: yamlTestCase0,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service only",
|
||||||
|
config: yamlTestCase1,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cron only",
|
||||||
|
config: yamlTestCase2,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid content - service",
|
||||||
|
config: yamlTestCase3,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range yamlBlobs {
|
||||||
|
tests = append(tests, testEntry{
|
||||||
|
name: fmt.Sprintf("negative - %d", i),
|
||||||
|
config: tc,
|
||||||
|
wantErr: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 * * * 2,3"`
|
||||||
|
|
||||||
|
const yamlTestCase1 = `version: "1"
|
||||||
|
service:
|
||||||
|
one:
|
||||||
|
command: ["command", "arg0", "arg1"]
|
||||||
|
environ: ["ENV1=value1", "ENV2=valueX"]
|
||||||
|
user: "user1"
|
||||||
|
group: "999"
|
||||||
|
working_dir: "/path/to/working"`
|
||||||
|
|
||||||
|
const yamlTestCase2 = `version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 * * * 2,3"`
|
||||||
|
|
||||||
|
const yamlTestCase3 = `version: "1"
|
||||||
|
service:
|
||||||
|
one:
|
||||||
|
command: 12345
|
||||||
|
environ: ["ENV1=value1", "ENV2=valueX"]
|
||||||
|
user: "user1"
|
||||||
|
group: "999"
|
||||||
|
working_dir: "/path/to/working"`
|
||||||
|
|
||||||
|
var yamlBlobs = []string{
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "a 13 3,5,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 a 3,5,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 a * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 3,5,7 a *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 3,5,7 * a"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/x 13 3,5,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "76 13 3,5,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/75 13 3,5,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 3,x,7 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 3,5,67 * *"`,
|
||||||
|
`version: "1"
|
||||||
|
cron:
|
||||||
|
cron-one:
|
||||||
|
command:
|
||||||
|
- command-cron
|
||||||
|
- arg0
|
||||||
|
- arg1
|
||||||
|
environ: ["ENV1=v1", "ENV2=var2"]
|
||||||
|
user: "1001"
|
||||||
|
group: "978"
|
||||||
|
schedule: "*/5 13 * *"`,
|
||||||
|
}
|
||||||
24
docker/alpine/Dockerfile
Normal file
24
docker/alpine/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM golang:1.24-alpine3.22 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
|
||||||
|
RUN apk update && apk add git make && \
|
||||||
|
make all && make DESTDIR=/usr/local/bin/wingmate install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
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
|
||||||
59
docker/alpine/etc/wingmate/wingmate.yaml
Normal file
59
docker/alpine/etc/wingmate/wingmate.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
service:
|
||||||
|
# one:
|
||||||
|
# command: [ "wmstarter" ]
|
||||||
|
# environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ]
|
||||||
|
|
||||||
|
spawner:
|
||||||
|
command: [ "wmspawner" ]
|
||||||
|
user: "1200"
|
||||||
|
|
||||||
|
bgtest:
|
||||||
|
command:
|
||||||
|
- "wmstarter"
|
||||||
|
- "--no-wait"
|
||||||
|
- "--"
|
||||||
|
- "wmexec"
|
||||||
|
- "--setsid"
|
||||||
|
- "--"
|
||||||
|
- "wmbg"
|
||||||
|
- "--name"
|
||||||
|
- "test-run"
|
||||||
|
- "--pause"
|
||||||
|
- "10"
|
||||||
|
- "--log-path"
|
||||||
|
- "/var/log/wmbg.log"
|
||||||
|
- "--pid-file"
|
||||||
|
- "/var/run/wmbg.pid"
|
||||||
|
pidfile: "/var/run/wmbg.pid"
|
||||||
|
|
||||||
|
cron:
|
||||||
|
cron1:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "*/5 * * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron1.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
|
||||||
|
cron2:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "17,42 */2 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron2.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
|
||||||
|
cron3:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "7,19,23,47 22 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron3.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"
|
||||||
20
docker/bookworm/Dockerfile
Normal file
20
docker/bookworm/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:1.24-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
|
||||||
59
docker/bookworm/etc/wingmate/wingmate.yaml
Normal file
59
docker/bookworm/etc/wingmate/wingmate.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
service:
|
||||||
|
# one:
|
||||||
|
# command: [ "wmstarter" ]
|
||||||
|
# environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ]
|
||||||
|
|
||||||
|
spawner:
|
||||||
|
command: [ "wmspawner" ]
|
||||||
|
user: "1200"
|
||||||
|
|
||||||
|
bgtest:
|
||||||
|
command:
|
||||||
|
- "wmstarter"
|
||||||
|
- "--no-wait"
|
||||||
|
- "--"
|
||||||
|
- "wmexec"
|
||||||
|
- "--setsid"
|
||||||
|
- "--"
|
||||||
|
- "wmbg"
|
||||||
|
- "--name"
|
||||||
|
- "test-run"
|
||||||
|
- "--pause"
|
||||||
|
- "10"
|
||||||
|
- "--log-path"
|
||||||
|
- "/var/log/wmbg.log"
|
||||||
|
- "--pid-file"
|
||||||
|
- "/var/run/wmbg.pid"
|
||||||
|
pidfile: "/var/run/wmbg.pid"
|
||||||
|
|
||||||
|
cron:
|
||||||
|
cron1:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "*/5 * * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron1.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
|
||||||
|
cron2:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "17,42 */2 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron2.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
|
||||||
|
cron3:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "7,19,23,47 22 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron3.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"
|
||||||
8
docker/test/no-config/alpine/Dockerfile
Normal file
8
docker/test/no-config/alpine/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM suyono/wingmate:test AS source
|
||||||
|
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
COPY --from=source /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||||
|
CMD [ "/usr/local/bin/wingmate" ]
|
||||||
8
docker/test/no-config/bookworm/Dockerfile
Normal file
8
docker/test/no-config/bookworm/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM suyono/wingmate:test AS source
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:bookworm
|
||||||
|
COPY --from=source /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||||
|
CMD [ "/usr/local/bin/wingmate" ]
|
||||||
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
|
||||||
31
go.mod
Normal file
31
go.mod
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
module gitea.suyono.dev/suyono/wingmate
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/spf13/pflag v1.0.6
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
golang.org/x/sys v0.33.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
69
go.sum
Normal file
69
go.sum
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||||
|
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
86
init/cron.go
Normal file
86
init/cron.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cronTag = "cron"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
var (
|
||||||
|
iwg *sync.WaitGroup
|
||||||
|
err error
|
||||||
|
stdout io.ReadCloser
|
||||||
|
stderr io.ReadCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
patchedEnv []string
|
||||||
|
)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Second * 30)
|
||||||
|
cron:
|
||||||
|
for {
|
||||||
|
if cron.TimeToRun(time.Now()) {
|
||||||
|
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing")
|
||||||
|
if err = cron.UtilDepCheck(); err != nil {
|
||||||
|
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("%+v", err)
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
patchedEnv = cron.PatchEnv(os.Environ())
|
||||||
|
cmd = exec.Command(cron.Command(patchedEnv...), cron.Arguments(patchedEnv...)...)
|
||||||
|
cmd.Env = patchedEnv
|
||||||
|
|
||||||
|
if len(cron.WorkingDir()) > 0 {
|
||||||
|
cmd.Dir = cron.WorkingDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
_ = cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
fail:
|
||||||
|
select {
|
||||||
|
case <-exitFlag:
|
||||||
|
ticker.Stop()
|
||||||
|
break cron
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
init/init.go
Normal file
95
init/init.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tasks interface {
|
||||||
|
List() []Task
|
||||||
|
Services() []ServiceTask
|
||||||
|
Crones() []CronTask
|
||||||
|
Get(string) (Task, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserGroup interface {
|
||||||
|
String() string
|
||||||
|
IsSet() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskStatus interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task interface {
|
||||||
|
Name() string
|
||||||
|
Command(...string) string
|
||||||
|
Arguments(...string) []string
|
||||||
|
EnvLen() int
|
||||||
|
Environ() []string
|
||||||
|
PatchEnv([]string) []string
|
||||||
|
Setsid() bool
|
||||||
|
UserGroup() UserGroup
|
||||||
|
WorkingDir() string
|
||||||
|
Status() TaskStatus
|
||||||
|
UtilDepCheck() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronTask interface {
|
||||||
|
Task
|
||||||
|
TimeToRun(time.Time) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceTask interface {
|
||||||
|
Task
|
||||||
|
Background() bool //NOTE: implies using wmpidproxy
|
||||||
|
PidFile() string //NOTE: implies using wmpidproxy
|
||||||
|
StartSecs() uint
|
||||||
|
AutoStart() bool
|
||||||
|
AutoRestart() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
Tasks() Tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
type Init struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInit(config Config) *Init {
|
||||||
|
return &Init{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Init) Start() {
|
||||||
|
var (
|
||||||
|
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, sigchld)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
|
||||||
|
|
||||||
|
for _, s := range i.config.Tasks().Services() {
|
||||||
|
wg.Add(1)
|
||||||
|
go i.service(wg, s, signalTrigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range i.config.Tasks().Crones() {
|
||||||
|
wg.Add(1)
|
||||||
|
go i.cron(wg, c, signalTrigger)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
111
init/service.go
Normal file
111
init/service.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceTag = "service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *Init) service(wg *sync.WaitGroup, task ServiceTask, exitFlag <-chan any) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
iwg *sync.WaitGroup
|
||||||
|
stderr io.ReadCloser
|
||||||
|
stdout io.ReadCloser
|
||||||
|
failStatus bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
patchedEnv []string
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
|
||||||
|
}()
|
||||||
|
|
||||||
|
service:
|
||||||
|
for {
|
||||||
|
failStatus = false
|
||||||
|
if err = task.UtilDepCheck(); err != nil {
|
||||||
|
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("%+v", err)
|
||||||
|
failStatus = true
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
patchedEnv = task.PatchEnv(os.Environ())
|
||||||
|
cmd = exec.Command(task.Command(patchedEnv...), task.Arguments(patchedEnv...)...)
|
||||||
|
cmd.Env = patchedEnv
|
||||||
|
|
||||||
|
if len(task.WorkingDir()) > 0 {
|
||||||
|
cmd.Dir = task.WorkingDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
_ = cmd.Wait()
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if failStatus {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
failStatus = false
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-exitFlag:
|
||||||
|
break service
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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().Debug().Str(tag, serviceName).Msg("closing pipe")
|
||||||
|
}
|
||||||
49
init/sighandler.go
Normal file
49
init/sighandler.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"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, sigchld chan<- os.Signal) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
wingmate.Log().Warn().Msg("signal handler: exiting")
|
||||||
|
}()
|
||||||
|
|
||||||
|
isOpen := true
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, unix.SIGINT, unix.SIGTERM, unix.SIGQUIT, unix.SIGCHLD)
|
||||||
|
|
||||||
|
signal:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case s := <-c:
|
||||||
|
switch s {
|
||||||
|
case unix.SIGTERM, unix.SIGINT, unix.SIGQUIT:
|
||||||
|
if isOpen {
|
||||||
|
wingmate.Log().Info().Msg("initiating shutdown...")
|
||||||
|
close(trigger)
|
||||||
|
wg.Add(1)
|
||||||
|
go i.signalPump(wg, selfExit)
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
case unix.SIGCHLD:
|
||||||
|
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().Debug().Msg("start pumping SIGTERM signal")
|
||||||
|
defer func() {
|
||||||
|
wingmate.Log().Debug().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
|
||||||
|
}
|
||||||
69
init/waiter.go
Normal file
69
init/waiter.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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, sigchld <-chan os.Signal) {
|
||||||
|
var (
|
||||||
|
ws unix.WaitStatus
|
||||||
|
err error
|
||||||
|
running bool
|
||||||
|
flagged bool
|
||||||
|
waitingForSignal bool
|
||||||
|
)
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
wingmate.Log().Debug().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().Debug().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().Debug().Msg("waiter: inner flag")
|
||||||
|
}
|
||||||
|
wingmate.Log().Debug().Msg("waiter: no child left")
|
||||||
|
break wait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wingmate.Log().Debug().Msgf("Wait4 returns error: %+v", err)
|
||||||
|
waitingForSignal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
logger.go
Normal file
166
logger.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package wingmate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate/logger"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeTag = "time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Time = 1 << iota
|
||||||
|
Caller
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PanicLevel = 5 - iota
|
||||||
|
FatalLevel
|
||||||
|
ErrorLevel
|
||||||
|
WarnLevel
|
||||||
|
InfoLevel
|
||||||
|
DebugLevel
|
||||||
|
TraceLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PanicLevelStr = "panic"
|
||||||
|
FatalLevelStr = "fatal"
|
||||||
|
ErrorLevelStr = "error"
|
||||||
|
WarnLevelStr = "warn"
|
||||||
|
InfoLevelStr = "info"
|
||||||
|
DebugLevelStr = "debug"
|
||||||
|
TraceLevelStr = "trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
w *wrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrapper struct {
|
||||||
|
log zerolog.Logger
|
||||||
|
flag int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLog(wc io.WriteCloser, flag int) error {
|
||||||
|
w = &wrapper{
|
||||||
|
log: zerolog.New(wc),
|
||||||
|
flag: flag,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Log() logger.Log {
|
||||||
|
if w == nil {
|
||||||
|
panic("nil internal logger")
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetGlobalLevel(level int) {
|
||||||
|
switch level {
|
||||||
|
case PanicLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||||
|
case FatalLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||||
|
case ErrorLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case WarnLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
case InfoLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case DebugLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
case TraceLevel:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetGlobalLevelStr(level string) {
|
||||||
|
switch level {
|
||||||
|
case PanicLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||||
|
case FatalLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||||
|
case ErrorLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case WarnLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
case InfoLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case DebugLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
case TraceLevelStr:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) flagPass(e *zerolog.Event) *zerolog.Event {
|
||||||
|
if w.flag&Time != 0 {
|
||||||
|
e = e.Time(timeTag, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.flag&Caller != 0 {
|
||||||
|
e = e.Caller(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Info() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Info()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Debug() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Debug()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Trace() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Trace()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Warn() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Warn()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Error() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) Fatal() logger.Content {
|
||||||
|
return (*eventWrapper)(w.flagPass(w.log.Fatal()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *eventWrapper) Err(err error) logger.Content {
|
||||||
|
rv := (*zerolog.Event)(w).Err(err)
|
||||||
|
return (*eventWrapper)(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *eventWrapper) Caller() logger.Content {
|
||||||
|
rv := (*zerolog.Event)(w).Caller()
|
||||||
|
return (*eventWrapper)(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *eventWrapper) Time(tag string, now time.Time) logger.Content {
|
||||||
|
rv := (*zerolog.Event)(w).Time(tag, now)
|
||||||
|
return (*eventWrapper)(rv)
|
||||||
|
}
|
||||||
21
logger/logger.go
Normal file
21
logger/logger.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
type Content interface {
|
||||||
|
Msg(string)
|
||||||
|
Msgf(string, ...any)
|
||||||
|
Str(string, string) Content
|
||||||
|
Err(error) Content
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogLevel interface {
|
||||||
|
Info() Content
|
||||||
|
Warn() Content
|
||||||
|
Error() Content
|
||||||
|
Fatal() Content
|
||||||
|
Debug() Content
|
||||||
|
Trace() Content
|
||||||
|
}
|
||||||
|
|
||||||
|
type Log interface {
|
||||||
|
LogLevel
|
||||||
|
}
|
||||||
311
task/cron.go
Normal file
311
task/cron.go
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
|
||||||
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 CronTask struct {
|
||||||
|
CronSchedule
|
||||||
|
userGroup
|
||||||
|
cronScheduleString string
|
||||||
|
name string
|
||||||
|
command []string
|
||||||
|
cmdLine []string
|
||||||
|
environ []string
|
||||||
|
fallbackEnv []string
|
||||||
|
setsid bool
|
||||||
|
workingDir string
|
||||||
|
lastRun time.Time
|
||||||
|
hasRun bool //NOTE: make sure initialised as false
|
||||||
|
config config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCronTask(name string) *CronTask {
|
||||||
|
return &CronTask{
|
||||||
|
name: name,
|
||||||
|
hasRun: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetCommand(cmds ...string) *CronTask {
|
||||||
|
c.command = make([]string, len(cmds))
|
||||||
|
copy(c.command, cmds)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetEnv(envs ...string) *CronTask {
|
||||||
|
c.environ = make([]string, len(envs))
|
||||||
|
copy(c.environ, envs)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetFallbackEnv(envs ...string) *CronTask {
|
||||||
|
c.fallbackEnv = make([]string, len(envs))
|
||||||
|
copy(c.fallbackEnv, envs)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetFlagSetsid(flag bool) *CronTask {
|
||||||
|
c.setsid = flag
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetWorkingDir(path string) *CronTask {
|
||||||
|
c.workingDir = path
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetUser(user string) *CronTask {
|
||||||
|
c.user = user
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetGroup(group string) *CronTask {
|
||||||
|
c.group = group
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetSchedule(scheduleStr string, schedule CronSchedule) *CronTask {
|
||||||
|
c.cronScheduleString = scheduleStr
|
||||||
|
c.CronSchedule = schedule
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) SetConfig(config config) *CronTask {
|
||||||
|
c.config = config
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Equals(another *CronTask) bool {
|
||||||
|
if another == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type toCompare struct {
|
||||||
|
Name string
|
||||||
|
Command string
|
||||||
|
Arguments []string
|
||||||
|
Environ []string
|
||||||
|
Setsid bool
|
||||||
|
UserGroup string
|
||||||
|
WorkingDir string
|
||||||
|
Schedule string
|
||||||
|
}
|
||||||
|
|
||||||
|
cmpStruct := func(p *CronTask) ([]byte, error) {
|
||||||
|
s := &toCompare{
|
||||||
|
Name: p.Name(),
|
||||||
|
Command: p.Command(),
|
||||||
|
Arguments: p.Arguments(),
|
||||||
|
Environ: p.Environ(),
|
||||||
|
Setsid: p.Setsid(),
|
||||||
|
UserGroup: p.UserGroup().String(),
|
||||||
|
WorkingDir: p.WorkingDir(),
|
||||||
|
Schedule: p.cronScheduleString,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
ours, theirs []byte
|
||||||
|
ourHash, theirHash [sha256.Size]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if ours, err = cmpStruct(c); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("cron task equals: %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ourHash = sha256.Sum256(ours)
|
||||||
|
|
||||||
|
if theirs, err = cmpStruct(another); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("cron task equals: %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
theirHash = sha256.Sum256(theirs)
|
||||||
|
|
||||||
|
for i := 0; i < sha256.Size; i++ {
|
||||||
|
if ourHash[i] != theirHash[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) UtilDepCheck() error {
|
||||||
|
c.cmdLine = make([]string, 0)
|
||||||
|
if c.setsid || c.UserGroup().IsSet() {
|
||||||
|
if err := c.config.WMExecCheckVersion(); err != nil {
|
||||||
|
return fmt.Errorf("utility dependency check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmdLine = append(c.cmdLine, c.config.WMExecPath())
|
||||||
|
|
||||||
|
if c.setsid {
|
||||||
|
c.cmdLine = append(c.cmdLine, "--setsid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UserGroup().IsSet() {
|
||||||
|
c.cmdLine = append(c.cmdLine, "--user", c.UserGroup().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmdLine = append(c.cmdLine, "--")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmdLine = append(c.cmdLine, c.command...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Command(env ...string) string {
|
||||||
|
if len(env) > 0 {
|
||||||
|
return wmenv.ExpandEnv(env, []string{c.cmdLine[0]})[0]
|
||||||
|
}
|
||||||
|
return c.cmdLine[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Arguments(env ...string) []string {
|
||||||
|
if len(c.cmdLine) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
argCopy := make([]string, len(c.cmdLine)-1)
|
||||||
|
copy(argCopy, c.cmdLine[1:])
|
||||||
|
if len(env) > 0 {
|
||||||
|
argCopy = wmenv.ExpandEnv(env, argCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return argCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) EnvLen() int {
|
||||||
|
return len(c.environ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Environ() []string {
|
||||||
|
retval := make([]string, len(c.environ))
|
||||||
|
copy(retval, c.environ)
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) PatchEnv(env []string) []string {
|
||||||
|
env = wmenv.PatchEnv(env, c.environ)
|
||||||
|
return wmenv.FallbackEnv(env, c.fallbackEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Setsid() bool {
|
||||||
|
return c.setsid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) UserGroup() wminit.UserGroup {
|
||||||
|
return &(c.userGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) WorkingDir() string {
|
||||||
|
return c.workingDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) Status() wminit.TaskStatus {
|
||||||
|
//TODO: implement me!
|
||||||
|
panic("not implemented")
|
||||||
|
// return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronTask) 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
|
||||||
|
}
|
||||||
127
task/env/env.go
vendored
Normal file
127
task/env/env.go
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
envCapture = regexp.MustCompile(`\$+[a-zA-Z_][a-zA-Z0-9_]*|\$+{[a-zA-Z_][a-zA-Z0-9_]*}`)
|
||||||
|
envEsc = regexp.MustCompile(`^\$\$+[^$]+$`) // escaped, starts with two or more $ character
|
||||||
|
envRef = regexp.MustCompile(`^\$([^$]+)$`) // capture the variable name
|
||||||
|
envRefExplicit = regexp.MustCompile(`^\${([^$]+)}$`) // capture the variable name - explicit
|
||||||
|
)
|
||||||
|
|
||||||
|
func expandEnv(envMap map[string]string, input string) string {
|
||||||
|
if envEsc.MatchString(input) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
if envName := envRefExplicit.FindStringSubmatch(input); envName != nil && envName[1] != "" {
|
||||||
|
exVal, ok := envMap[envName[1]]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return exVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if envName := envRef.FindStringSubmatch(input); envName != nil && envName[1] != "" {
|
||||||
|
exVal, ok := envMap[envName[1]]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return exVal
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatchEnv(existing, new []string) []string {
|
||||||
|
tMap := make(map[string]string)
|
||||||
|
for _, e := range existing {
|
||||||
|
key, value, ok := strings.Cut(e, "=")
|
||||||
|
if !ok {
|
||||||
|
wingmate.Log().Warn().Msgf("removing invalid environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range new {
|
||||||
|
key, value, ok := strings.Cut(e, "=")
|
||||||
|
if !ok {
|
||||||
|
wingmate.Log().Warn().Msgf("removing invalid environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ContainsAny(key, "$") {
|
||||||
|
wingmate.Log().Error().Err(fmt.Errorf("variable name contains $")).Msgf("removing invalid environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value = envCapture.ReplaceAllStringFunc(value, func(rep string) string {
|
||||||
|
return expandEnv(tMap, rep)
|
||||||
|
})
|
||||||
|
|
||||||
|
tMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
outEnv := make([]string, 0, len(existing))
|
||||||
|
for key, val := range tMap {
|
||||||
|
outEnv = append(outEnv, fmt.Sprintf("%s=%s", key, val))
|
||||||
|
}
|
||||||
|
|
||||||
|
return outEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpandEnv(env []string, input []string) []string {
|
||||||
|
envMap := make(map[string]string)
|
||||||
|
for _, e := range env {
|
||||||
|
key, value, ok := strings.Cut(e, "=")
|
||||||
|
if !ok {
|
||||||
|
wingmate.Log().Warn().Msgf("removing bad environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range input {
|
||||||
|
s = envCapture.ReplaceAllStringFunc(s, func(rep string) string {
|
||||||
|
return expandEnv(envMap, rep)
|
||||||
|
})
|
||||||
|
input[i] = s
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func FallbackEnv(env []string, fallbacks []string) []string {
|
||||||
|
envMap := make(map[string]string)
|
||||||
|
for _, e := range env {
|
||||||
|
key, value, ok := strings.Cut(e, "=")
|
||||||
|
if !ok {
|
||||||
|
wingmate.Log().Warn().Msgf("removing bad environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range fallbacks {
|
||||||
|
key, value, ok := strings.Cut(e, "=")
|
||||||
|
if !ok {
|
||||||
|
wingmate.Log().Warn().Msgf("removing bad environment:", e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value = envCapture.ReplaceAllStringFunc(value, func(rep string) string {
|
||||||
|
return expandEnv(envMap, rep)
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, ok = envMap[key]; !ok {
|
||||||
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
392
task/task.go
Normal file
392
task/task.go
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
|
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config interface {
|
||||||
|
WMPidProxyPath() string
|
||||||
|
WMPidProxyCheckVersion() error
|
||||||
|
WMExecPath() string
|
||||||
|
WMExecCheckVersion() error
|
||||||
|
}
|
||||||
|
|
||||||
|
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.AddService(NewServiceTask(path)).SetCommand(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *Tasks) AddService(serviceTask *ServiceTask) *ServiceTask {
|
||||||
|
ts.services = append(ts.services, serviceTask)
|
||||||
|
return serviceTask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) {
|
||||||
|
ts.AddCron(NewCronTask(path)).SetCommand(path).SetSchedule("", schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *Tasks) AddCron(cronTask *CronTask) *CronTask {
|
||||||
|
ts.crones = append(ts.crones, cronTask)
|
||||||
|
return cronTask
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
//TODO: implement me!
|
||||||
|
panic("not implemented")
|
||||||
|
// return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceTask struct {
|
||||||
|
name string
|
||||||
|
command []string
|
||||||
|
cmdLine []string
|
||||||
|
environ []string
|
||||||
|
fallbackEnv []string
|
||||||
|
setsid bool
|
||||||
|
background bool
|
||||||
|
workingDir string
|
||||||
|
startSecs uint
|
||||||
|
pidFile string
|
||||||
|
config config
|
||||||
|
userGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceTask(name string) *ServiceTask {
|
||||||
|
return &ServiceTask{
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetCommand(cmds ...string) *ServiceTask {
|
||||||
|
t.command = make([]string, len(cmds))
|
||||||
|
copy(t.command, cmds)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetEnv(envs ...string) *ServiceTask {
|
||||||
|
t.environ = make([]string, len(envs))
|
||||||
|
copy(t.environ, envs)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetFallbackEnv(envs ...string) *ServiceTask {
|
||||||
|
t.fallbackEnv = make([]string, len(envs))
|
||||||
|
copy(t.fallbackEnv, envs)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetFlagSetsid(flag bool) *ServiceTask {
|
||||||
|
t.setsid = flag
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetWorkingDir(path string) *ServiceTask {
|
||||||
|
t.workingDir = path
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetUser(user string) *ServiceTask {
|
||||||
|
t.user = user
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetGroup(group string) *ServiceTask {
|
||||||
|
t.group = group
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetStartSecs(secs uint) *ServiceTask {
|
||||||
|
t.startSecs = secs
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetPidFile(path string) *ServiceTask {
|
||||||
|
t.pidFile = path
|
||||||
|
if len(path) > 0 {
|
||||||
|
t.background = true
|
||||||
|
} else {
|
||||||
|
t.background = false
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) SetConfig(config config) *ServiceTask {
|
||||||
|
t.config = config
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Equals(another *ServiceTask) bool {
|
||||||
|
if another == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type toCompare struct {
|
||||||
|
Name string
|
||||||
|
Command string
|
||||||
|
Arguments []string
|
||||||
|
Environ []string
|
||||||
|
Setsid bool
|
||||||
|
UserGroup string
|
||||||
|
WorkingDir string
|
||||||
|
PidFile string
|
||||||
|
StartSecs uint
|
||||||
|
AutoStart bool
|
||||||
|
AutoRestart bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cmpStruct := func(p *ServiceTask) ([]byte, error) {
|
||||||
|
s := &toCompare{
|
||||||
|
Name: p.Name(),
|
||||||
|
Command: p.Command(),
|
||||||
|
Arguments: p.Arguments(),
|
||||||
|
Environ: p.Environ(),
|
||||||
|
Setsid: p.Setsid(),
|
||||||
|
UserGroup: p.UserGroup().String(),
|
||||||
|
WorkingDir: p.WorkingDir(),
|
||||||
|
PidFile: p.PidFile(),
|
||||||
|
StartSecs: p.StartSecs(),
|
||||||
|
AutoStart: p.AutoStart(),
|
||||||
|
AutoRestart: p.AutoRestart(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
ours, theirs []byte
|
||||||
|
ourHash, theirHash [sha256.Size]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if ours, err = cmpStruct(t); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("task equals: %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ourHash = sha256.Sum256(ours)
|
||||||
|
|
||||||
|
if theirs, err = cmpStruct(another); err != nil {
|
||||||
|
wingmate.Log().Error().Msgf("task equals: %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
theirHash = sha256.Sum256(theirs)
|
||||||
|
|
||||||
|
for i := 0; i < sha256.Size; i++ {
|
||||||
|
if ourHash[i] != theirHash[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Validate() error {
|
||||||
|
// call this function for validate the field
|
||||||
|
return validate( /* input the validators here */ )
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) prepareCommandLine() []string {
|
||||||
|
//TODO: is this method used somewhere? if not, can I remove this?
|
||||||
|
if len(t.cmdLine) > 0 {
|
||||||
|
return t.cmdLine
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = make([]string, 0)
|
||||||
|
if t.background {
|
||||||
|
t.cmdLine = append(t.cmdLine, t.config.WMPidProxyPath(), "--pid-file", t.pidFile, "--")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.setsid || t.UserGroup().IsSet() {
|
||||||
|
t.cmdLine = append(t.cmdLine, t.config.WMExecPath())
|
||||||
|
|
||||||
|
if t.setsid {
|
||||||
|
t.cmdLine = append(t.cmdLine, "--setsid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.UserGroup().IsSet() {
|
||||||
|
t.cmdLine = append(t.cmdLine, "--user", t.UserGroup().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, "--")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, t.command...)
|
||||||
|
|
||||||
|
return t.cmdLine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) UtilDepCheck() error {
|
||||||
|
t.cmdLine = make([]string, 0)
|
||||||
|
if t.background {
|
||||||
|
if err := t.config.WMPidProxyCheckVersion(); err != nil {
|
||||||
|
return fmt.Errorf("utility dependency check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, t.config.WMPidProxyPath(), "--pid-file", t.pidFile, "--")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.setsid || t.UserGroup().IsSet() {
|
||||||
|
if err := t.config.WMExecCheckVersion(); err != nil {
|
||||||
|
return fmt.Errorf("utility dependency check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, t.config.WMExecPath())
|
||||||
|
|
||||||
|
if t.setsid {
|
||||||
|
t.cmdLine = append(t.cmdLine, "--setsid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.UserGroup().IsSet() {
|
||||||
|
t.cmdLine = append(t.cmdLine, "--user", t.UserGroup().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, "--")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cmdLine = append(t.cmdLine, t.command...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Command(env ...string) string {
|
||||||
|
if len(env) > 0 {
|
||||||
|
return wmenv.ExpandEnv(env, []string{t.cmdLine[0]})[0]
|
||||||
|
}
|
||||||
|
return t.cmdLine[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Arguments(env ...string) []string {
|
||||||
|
if len(t.cmdLine) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
argCopy := make([]string, len(t.cmdLine)-1)
|
||||||
|
copy(argCopy, t.cmdLine[1:])
|
||||||
|
if len(env) > 0 {
|
||||||
|
argCopy = wmenv.ExpandEnv(env, argCopy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return argCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) EnvLen() int {
|
||||||
|
return len(t.environ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Environ() []string {
|
||||||
|
retval := make([]string, len(t.environ))
|
||||||
|
copy(retval, t.environ)
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) PatchEnv(env []string) []string {
|
||||||
|
env = wmenv.PatchEnv(env, t.environ)
|
||||||
|
return wmenv.FallbackEnv(env, t.fallbackEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Setsid() bool {
|
||||||
|
return t.setsid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) UserGroup() wminit.UserGroup {
|
||||||
|
return &(t.userGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Background() bool {
|
||||||
|
return t.background
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) WorkingDir() string {
|
||||||
|
return t.workingDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) Status() wminit.TaskStatus {
|
||||||
|
//TODO: implement me!
|
||||||
|
panic("not implemented")
|
||||||
|
// return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) AutoStart() bool {
|
||||||
|
//TODO: implement me!
|
||||||
|
panic("not implemented")
|
||||||
|
// return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) AutoRestart() bool {
|
||||||
|
//TODO: implement me!
|
||||||
|
panic("not implemented")
|
||||||
|
// return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) StartSecs() uint {
|
||||||
|
return t.startSecs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ServiceTask) PidFile() string {
|
||||||
|
return t.pidFile
|
||||||
|
}
|
||||||
|
|
||||||
|
type userGroup struct {
|
||||||
|
user string
|
||||||
|
group string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ug *userGroup) IsSet() bool {
|
||||||
|
return len(ug.user) > 0 || len(ug.group) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ug *userGroup) String() string {
|
||||||
|
if len(ug.group) > 0 {
|
||||||
|
return fmt.Sprintf("%s:%s", ug.user, ug.group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ug.user
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(validators ...func() error) error {
|
||||||
|
var err error
|
||||||
|
for _, v := range validators {
|
||||||
|
if err = v(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
131
task/task_test.go
Normal file
131
task/task_test.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.suyono.dev/suyono/wingmate"
|
||||||
|
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
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{
|
||||||
|
&CronTask{
|
||||||
|
CronSchedule: CronSchedule{
|
||||||
|
Minute: NewCronAnySpec(),
|
||||||
|
Hour: NewCronAnySpec(),
|
||||||
|
DoM: NewCronAnySpec(),
|
||||||
|
Month: NewCronAnySpec(),
|
||||||
|
DoW: NewCronAnySpec(),
|
||||||
|
},
|
||||||
|
name: "cron-one",
|
||||||
|
command: []string{"/path/to/executable"},
|
||||||
|
},
|
||||||
|
&CronTask{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceTaskPatchEnv(t *testing.T) {
|
||||||
|
type testEntry struct {
|
||||||
|
name string
|
||||||
|
systemEnv []string
|
||||||
|
serviceEnv []string
|
||||||
|
expected []string
|
||||||
|
}
|
||||||
|
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
|
||||||
|
|
||||||
|
tests := []testEntry{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
systemEnv: []string{"PATH=/bin:/usr/bin:/usr/local/bin"},
|
||||||
|
serviceEnv: []string{
|
||||||
|
"SPARK_HOME=/opt/spark",
|
||||||
|
"PATH=$SPARK_HOME/bin:$PATH",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"SPARK_HOME=/opt/spark",
|
||||||
|
"PATH=/opt/spark/bin:/bin:/usr/bin:/usr/local/bin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit",
|
||||||
|
systemEnv: []string{"PART=hello "},
|
||||||
|
serviceEnv: []string{"GREET=${PART}world"},
|
||||||
|
expected: []string{"PART=hello ", "GREET=hello world"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Run("service", func(t *testing.T) {
|
||||||
|
st := NewServiceTask(tt.name)
|
||||||
|
st.SetEnv(tt.serviceEnv...)
|
||||||
|
result := st.PatchEnv(tt.systemEnv)
|
||||||
|
assert.ElementsMatch(t, result, tt.expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cron", func(t *testing.T) {
|
||||||
|
st := NewCronTask(tt.name)
|
||||||
|
st.SetEnv(tt.serviceEnv...)
|
||||||
|
result := st.PatchEnv(tt.systemEnv)
|
||||||
|
assert.ElementsMatch(t, result, tt.expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
11
types.go
Normal file
11
types.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package wingmate
|
||||||
|
|
||||||
|
type CronTimeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Any CronTimeType = iota
|
||||||
|
Exact
|
||||||
|
MultipleOccurrence
|
||||||
|
|
||||||
|
EnvPrefix = "WINGMATE"
|
||||||
|
)
|
||||||
183
wingmate.yaml.md
Normal file
183
wingmate.yaml.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
YAML Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Table of content
|
||||||
|
- [Service](#service)
|
||||||
|
- [Command](#command)
|
||||||
|
- [Environ](#environ)
|
||||||
|
- [User and Group](#user-and-group)
|
||||||
|
- [Working Directory](#working-directory)
|
||||||
|
- [setsid](#setsid)
|
||||||
|
- [PID File](#pid-file)
|
||||||
|
- [Cron](#cron)
|
||||||
|
- [Schedule](#schedule)
|
||||||
|
|
||||||
|
Example
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
spawner:
|
||||||
|
command: [ wmspawner ]
|
||||||
|
user: "1200"
|
||||||
|
working_dir: "/var/run/test"
|
||||||
|
|
||||||
|
bgtest:
|
||||||
|
command:
|
||||||
|
- "wmstarter"
|
||||||
|
- "--no-wait"
|
||||||
|
- "--"
|
||||||
|
- "wmexec"
|
||||||
|
- "--setsid"
|
||||||
|
- "--"
|
||||||
|
- "wmbg"
|
||||||
|
- "--name"
|
||||||
|
- "test-run"
|
||||||
|
- "--pause"
|
||||||
|
- "10"
|
||||||
|
- "--log-path"
|
||||||
|
- "/var/log/wmbg.log"
|
||||||
|
- "--pid-file"
|
||||||
|
- "/var/run/wmbg.pid"
|
||||||
|
pidfile: "/var/run/wmbg.pid"
|
||||||
|
|
||||||
|
cron:
|
||||||
|
cron1:
|
||||||
|
command: ["wmoneshot", "--", "sleep", "5"]
|
||||||
|
schedule: "*/5 * * * *"
|
||||||
|
working_dir: "/var/run/cron"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron1.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
|
||||||
|
cron2:
|
||||||
|
command: ["wmoneshot", "--", "sleep", "5"]
|
||||||
|
schedule: "17,42 */2 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron2.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
|
||||||
|
cron3:
|
||||||
|
command:
|
||||||
|
- "wmoneshot"
|
||||||
|
- "--"
|
||||||
|
- "sleep"
|
||||||
|
- "5"
|
||||||
|
schedule: "7,19,23,47 22 * * *"
|
||||||
|
environ:
|
||||||
|
- "WINGMATE_LOG=/var/log/cron3.log"
|
||||||
|
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
At the top-level, there are two possible entries: Service and Cron.
|
||||||
|
|
||||||
|
## Service
|
||||||
|
|
||||||
|
`service` is a top-level element that hosts the definition of services to be started by `wingmate`.
|
||||||
|
|
||||||
|
Example
|
||||||
|
```yaml
|
||||||
|
service:
|
||||||
|
svc1:
|
||||||
|
command: [ some_executable ]
|
||||||
|
user: "1200"
|
||||||
|
working_dir: "/var/run/test"
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, we declare a service called `svc1`. `wingmate` will start a process based on all
|
||||||
|
elements defined under `svc1`. To learn more about elements for a service, read below.
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
`command` element is an array of strings consists of an executable name (optionally with path) and
|
||||||
|
its arguments (if any). `wingmate` will start the service as its child process by executing
|
||||||
|
the executable with its arguments.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
command: [ executable1, argument1, argument2 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Based on YAML standard, the above example can also be written like
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
command:
|
||||||
|
- executable1
|
||||||
|
- argument1
|
||||||
|
- argument2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environ
|
||||||
|
|
||||||
|
`environ` element is an array of strings. It is a list of environment variables `wingmate` will pass to
|
||||||
|
the child process or service. The format of each environment variable is a pair of key and value separated
|
||||||
|
by `=` sign. By default, the child process or service will inherit all environment variables of its parent.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environ:
|
||||||
|
- "S3_BUCKET=YOURS3BUCKET"
|
||||||
|
- "SECRET_KEY=YOUR_SECRET_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: don't worry if an environment variable value has one or more `=` character(s) in it. `wingmate` will
|
||||||
|
separate key and value using the first `=` character only.
|
||||||
|
|
||||||
|
### Working Directory
|
||||||
|
|
||||||
|
`working_dir` is a string contains the path where the child process will be running in. By default, the child
|
||||||
|
process will run in the `wingmate` current directory.
|
||||||
|
|
||||||
|
### User and Group
|
||||||
|
|
||||||
|
Both `user` and `group` take string value. `user` and `group` refer to the operating system's user and group.
|
||||||
|
They can be in the form of name, like username or groupname, or in the form of id, like uid or gid.
|
||||||
|
If they are set, the child process will run as the specified user and group. By default, the child process
|
||||||
|
will run as the same user and group as the `wingmate` process. The `user` and `group` are only effective
|
||||||
|
when the `wingmate` running as privileged user, such as `root`. The `user` and `group` configuration depends
|
||||||
|
on the [wmexec](README.md#wingmate-exec-binary).
|
||||||
|
|
||||||
|
### setsid
|
||||||
|
|
||||||
|
`setsid` takes a boolean value, `true` or `false`. This feature is operating system dependant. If set to `true`,
|
||||||
|
the child process will run in a new session. Read `man setsid` on Linux/UNIX. The `setsid` configuration depends
|
||||||
|
on the [wmexec](README.md#wingmate-exec-binary).
|
||||||
|
|
||||||
|
### PID File
|
||||||
|
|
||||||
|
This feature is designated to handle service that run in the background. This kind of service usually forks a
|
||||||
|
new process, terminate the parent process, and continue running in the background child process. It writes its
|
||||||
|
background process PID in a file. This file is referred as PID file. Put the path of the PID file to this
|
||||||
|
`pidfile` element. It will help `wingmate` to restart the service if its process exited / terminated. The `pidfile`
|
||||||
|
configuration depends on the [wmpidproxy](README.md#wingmate-pid-proxy-binary).
|
||||||
|
|
||||||
|
## Cron
|
||||||
|
|
||||||
|
`cron` is a top-level element that hosts the definition of crones to run by `wingmate` on the specified schedule.
|
||||||
|
Cron shares almost all configuration elements with Service, except `schedule` and `pidfile`. For the following
|
||||||
|
elements, please refer to the [Service](#service) section
|
||||||
|
|
||||||
|
- [Command](#command)
|
||||||
|
- [Environ](#environ)
|
||||||
|
- [Working Directory](#working-directory)
|
||||||
|
- [setsid](#setsid)
|
||||||
|
- [User and Group](#user-and-group)
|
||||||
|
|
||||||
|
`pidfile` is an invalid config parameter for cron because `wingmate` cannot start cron in background mode. This
|
||||||
|
limitation is intentionally built into `wingmate` because it doesn't make any sense to run a periodic cron process
|
||||||
|
in background.
|
||||||
|
|
||||||
|
### Schedule
|
||||||
|
|
||||||
|
The schedule configuration field uses a format similar to the one described in the [README.md](README.md).
|
||||||
|
|
||||||
|
```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)
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
* * * * *
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user