21 Commits

Author SHA1 Message Date
439baa60fb wip: ready for initial test 2023-12-07 04:47:00 +00:00
ca6c2de4dc wip: config 2023-12-07 03:16:37 +00:00
c5e20f7910 wip 2023-12-06 23:04:19 +11:00
dbb703db61 revamp: start simple 2023-12-06 17:01:43 +11:00
4f54db3cbd chore: update golang version and tools-versions 2023-12-06 11:27:51 +11:00
f482eb8631 wip: default writer tested 2023-09-11 13:56:38 +10:00
d406fe698f wip: log 2023-09-11 12:28:53 +10:00
277e8feba1 wip: log 2023-09-11 10:53:21 +10:00
5050a4fdba wip: log 2023-09-09 17:42:39 +10:00
5f7befe02c wip: log 2023-09-09 14:07:49 +10:00
8a480acde7 wip: log 2023-09-06 03:37:43 +10:00
1f25e5249c wip: log 2023-09-05 14:22:36 +10:00
a5a019b28b wip: log 2023-09-05 13:09:53 +10:00
9f7c004105 wip: log 2023-09-05 10:20:49 +10:00
6eada2d348 wip: log 2023-09-04 15:58:41 +10:00
5621312be1 wip: log 2023-09-04 12:49:48 +10:00
badd2e3543 wip: log 2023-09-03 12:11:35 +10:00
91147f073f wip: daemon 2023-09-01 12:29:31 +10:00
eac78ec322 wip: test signal 2023-08-31 08:55:48 +10:00
92f7873f98 wip: process management 2023-08-31 08:05:20 +10:00
9cbdbebee5 wip: initial work 2023-08-28 18:12:31 +10:00
76 changed files with 732 additions and 3889 deletions

View File

@@ -1,25 +1,10 @@
{
"name": "Golang Dev",
"image": "golang-dev:1.22-bookworm-user",
"mounts": [
{
"source": "WingmateGoPath",
"target": "/go",
"type": "volume"
},
{
"source": "WingmateGolangDevHome",
"target": "/home/golang",
"type": "volume"
}
],
"image": "golang-dev:1.21-bookworm-user",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools",
"ms-vscode.cpptools-extension-pack"
"golang.go"
]
}
}

8
.gitignore vendored
View File

@@ -1,6 +1,2 @@
/cmd/wingmate/wingmate
/cmd/wingmate/version.txt
/cmd/pidproxy/pidproxy
/cmd/pidproxy/version.txt
/cmd/exec/exec
/cmd/exec/version.txt
/.idea
/wingmate

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# 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
View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<targets>
<target name="golang-dev:1.21-bookworm-user" type="docker" uuid="5b79636b-3db9-4e2a-9fd3-03277653ae58">
<config>
<option name="targetPlatform">
<TargetPlatform />
</option>
<option name="buildImageConfig">
<BuildImageConfig>
<option name="dockerFile" value="docker/bookworm/Dockerfile" />
</BuildImageConfig>
</option>
<option name="buildNotPull" value="false" />
<option name="containerConfig">
<ContainerConfig>
<option name="runCliOptions" value="-u &quot;1000:1000&quot; --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
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

9
.idea/wingmate.iml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.21.4

View File

@@ -1,59 +0,0 @@
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
View File

@@ -1,157 +1,2 @@
# 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 (059)
│ ┌───────────── hour (023)
│ │ ┌───────────── day of the month (131)
│ │ │ ┌───────────── month (112)
│ │ │ │ ┌───────────── day of the week (06) (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)

View File

@@ -1,37 +0,0 @@
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
}

View File

@@ -1,7 +0,0 @@
package cli
import "testing"
func TestSplitArgs(t *testing.T) {
SplitArgs([]string{"wmexec", "--user", "1200", "--", "wmspawner"})
}

View File

@@ -1,45 +0,0 @@
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()
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install exec ${DESTDIR}/wmexec

View File

@@ -1,11 +0,0 @@
//go:build !(cgo && linux)
package main
func getUid(user string) (uint64, error) {
panic("not implemented")
}
func getGid(group string) (uint64, error) {
panic("not implemented")
}

View File

@@ -1,51 +0,0 @@
//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
}

View File

@@ -1,142 +0,0 @@
package main
import (
_ "embed"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
"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 = 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
}

View File

@@ -1,10 +0,0 @@
/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

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install bg ${DESTDIR}/wmbg

View File

@@ -1,67 +0,0 @@
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
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install dummy ${DESTDIR}/wmdummy

View File

@@ -1,17 +0,0 @@
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")
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install oneshot ${DESTDIR}/wmoneshot

View File

@@ -1,79 +0,0 @@
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); 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)
}
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install spawner ${DESTDIR}/wmspawner

View File

@@ -1,40 +0,0 @@
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
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install starter ${DESTDIR}/wmstarter

View File

@@ -1,110 +0,0 @@
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()
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install pidproxy ${DESTDIR}/wmpidproxy

View File

@@ -1,168 +0,0 @@
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
}
}

View File

@@ -1,11 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install wingmate ${DESTDIR}/wingmate

View File

@@ -1,86 +1,60 @@
package main
import (
"sync"
"time"
"gitea.suyono.dev/suyono/wingmate/config"
wminit "gitea.suyono.dev/suyono/wingmate/init"
"gitea.suyono.dev/suyono/wingmate/task"
)
type wPath struct {
path string
}
func (p wPath) Path() string {
return p.path
}
type wConfig struct {
tasks *task.Tasks
config *config.Config
viperMtx *sync.Mutex
services []wminit.Path
cron []wminit.Cron
}
func (c *wConfig) Tasks() wminit.Tasks {
return c.tasks
func (c wConfig) Services() []wminit.Path {
return c.services
}
func (c *wConfig) Reload() error {
//NOTE: for future use when reloading is possible
return nil
func (c wConfig) Cron() []wminit.Cron {
return c.cron
}
func convert(cfg *config.Config) *wConfig {
retval := &wConfig{
tasks: task.NewTasks(),
config: cfg,
viperMtx: &sync.Mutex{},
type wCron struct {
iCron *config.Cron
}
for _, s := range cfg.Service {
st := task.NewServiceTask(s.Name).SetCommand(s.Command...).SetEnv(s.Environ...)
st.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)
func (c wCron) TimeToRun(now time.Time) bool {
return c.iCron.TimeToRun(now)
}
for _, s := range cfg.ServiceV0 {
retval.tasks.AddV0Service(s)
func (c wCron) Command() wminit.Path {
return wPath{
path: c.iCron.Command(),
}
}
var schedule task.CronSchedule
for _, c := range cfg.CronV0 {
schedule = configToTaskCronSchedule(c.CronSchedule)
retval.tasks.AddV0Cron(schedule, c.Command)
func convert(cfg *config.Config) wConfig {
retval := wConfig{
services: make([]wminit.Path, 0, len(cfg.ServicePaths)),
cron: make([]wminit.Cron, 0, len(cfg.Cron)),
}
for _, s := range cfg.ServicePaths {
retval.services = append(retval.services, wPath{path: s})
}
for _, c := range cfg.Cron {
schedule = configToTaskCronSchedule(c.CronSchedule)
ct := task.NewCronTask(c.Name).SetCommand(c.Command...).SetEnv(c.Environ...)
ct.SetFlagSetsid(c.Setsid).SetWorkingDir(c.WorkingDir).SetUser(c.User).SetGroup(c.Group)
ct.SetSchedule(c.Schedule, schedule)
ct.SetConfig(cfg)
retval.tasks.AddCron(ct)
retval.cron = append(retval.cron, wCron{iCron: c})
}
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")
}

View File

@@ -1,18 +1,11 @@
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() {
@@ -22,13 +15,8 @@ func main() {
)
_ = wingmate.NewLog(os.Stderr)
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")
wingmate.Log().Error().Msgf("failed to read config %#v", err)
}
initCfg := convert(cfg)

View File

@@ -1,23 +0,0 @@
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()
}

View File

@@ -2,91 +2,30 @@ 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"
EnvConfigPath = "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"
)
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"`
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)
ServicePaths []string
Cron []*Cron
}
func Read() (*Config, error) {
viper.SetEnvPrefix(EnvPrefix)
_ = viper.BindEnv(PathConfig)
_ = viper.BindEnv(PidProxyPathConfig)
_ = viper.BindEnv(ExecPathConfig)
viper.SetDefault(PathConfig, DefaultConfigPath)
viper.SetDefault(PidProxyPathConfig, PidProxyPathDefault)
viper.SetDefault(ExecPathConfig, ExecPathDefault)
viper.BindEnv(EnvConfigPath)
viper.SetDefault(EnvConfigPath, DefaultConfigPath)
var (
dirent []os.DirEntry
@@ -94,123 +33,43 @@ func Read() (*Config, error) {
svcdir string
serviceAvailable bool
cronAvailable bool
wingmateConfigAvailable bool
cron []*Cron
crontabfile string
services []ServiceTask
crones []CronTask
)
serviceAvailable = false
cronAvailable = false
outConfig := &Config{
viperMtx: &sync.Mutex{},
ServiceV0: make([]string, 0),
ServicePaths: make([]string, 0),
}
configPath := viper.GetString(PathConfig)
configPath := viper.GetString(EnvConfigPath)
svcdir = filepath.Join(configPath, ServiceDirName)
dirent, err = os.ReadDir(svcdir)
if err != nil {
wingmate.Log().Error().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)
outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
}
}
}
if err != nil {
wingmate.Log().Error().Msgf("encounter error when reading service directory %s: %#v", svcdir, err)
}
crontabfile = filepath.Join(configPath, CrontabFileName)
cron, err = readCrontab(crontabfile)
if len(cron) > 0 {
outConfig.CronV0 = cron
outConfig.Cron = cron
cronAvailable = true
}
if err != nil {
wingmate.Log().Error().Msgf("encounter error when reading crontab %s: %+v", crontabfile, err)
wingmate.Log().Error().Msgf("encounter error when reading crontab %s: %#v", crontabfile, err)
}
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 {
if !serviceAvailable && !cronAvailable {
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
}

View File

@@ -1,132 +0,0 @@
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)
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)
})
}
}

View File

@@ -4,30 +4,54 @@ import (
"bufio"
"errors"
"fmt"
"gitea.suyono.dev/suyono/wingmate"
"os"
"regexp"
"strconv"
"strings"
"time"
"gitea.suyono.dev/suyono/wingmate"
)
type CronTimeSpec interface {
//Type() wingmate.CronTimeType
//Match(uint8) bool
type CronExactSpec interface {
CronTimeSpec
Value() uint8
}
type CronMultipleOccurrenceSpec interface {
CronTimeSpec
Values() []uint8
}
type CronTimeSpec interface {
Type() wingmate.CronTimeType
Match(uint8) bool
}
// type Cron interface {
// Minute() CronTimeSpec
// Hour() CronTimeSpec
// DayOfMonth() CronTimeSpec
// Month() CronTimeSpec
// DayOfWeek() CronTimeSpec
// }
type Cron struct {
CronSchedule
Command string
minute CronTimeSpec
hour CronTimeSpec
dom CronTimeSpec
month CronTimeSpec
dow CronTimeSpec
command string
lastRun time.Time
hasRun bool
}
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
CrontabEntryRegex = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
CrontabSubmatchLen = 7
minute cronField = iota
hour
@@ -36,22 +60,21 @@ const (
dow
)
var (
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
)
func readCrontab(path string) ([]*Cron, error) {
var (
file *os.File
err error
scanner *bufio.Scanner
line string
re *regexp.Regexp
parts []string
retval []*Cron
)
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
return nil, err
}
if file, err = os.Open(path); err != nil {
return nil, err
}
@@ -64,48 +87,41 @@ func readCrontab(path string) ([]*Cron, error) {
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 {
parts = re.FindStringSubmatch(line)
if len(parts) != CrontabSubmatchLen {
wingmate.Log().Error().Msgf("invalid entry %s", line)
continue
}
c := &Cron{}
c := &Cron{
hasRun: false,
}
if err = c.setField(minute, parts[1]); err != nil {
wingmate.Log().Error().Msgf("error parsing Minute field %+v", err)
wingmate.Log().Error().Msgf("error parsing minute field %#v", err)
continue
}
if err = c.setField(hour, parts[2]); err != nil {
wingmate.Log().Error().Msgf("error parsing Hour field %+v", err)
wingmate.Log().Error().Msgf("error parsing hour field %#v", err)
continue
}
if err = c.setField(dom, parts[3]); err != nil {
wingmate.Log().Error().Msgf("error parsing Day of Month field %+v", err)
wingmate.Log().Error().Msgf("error parsing day of month field %#v", err)
continue
}
if err = c.setField(month, parts[4]); err != nil {
wingmate.Log().Error().Msgf("error parsing Month field %+v", err)
wingmate.Log().Error().Msgf("error parsing month field %#v", err)
continue
}
if err = c.setField(dow, parts[5]); err != nil {
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err)
wingmate.Log().Error().Msgf("error parsing day of week field %#v", err)
continue
}
c.Command = parts[6]
c.command = parts[6]
retval = append(retval, c)
}
@@ -113,6 +129,32 @@ func readCrontab(path string) ([]*Cron, error) {
return retval, nil
}
func (c *Cron) Command() string {
return c.command
}
func (c *Cron) TimeToRun(now time.Time) bool {
if !c.hasRun {
c.lastRun = now
c.hasRun = true
return true
}
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
return false
}
if c.minute.Match(uint8(now.Minute())) &&
c.hour.Match(uint8(now.Hour())) &&
c.dom.Match(uint8(now.Day())) &&
c.month.Match(uint8(now.Month())) &&
c.dow.Match(uint8(now.Weekday())) {
return true
}
return false
}
type fieldRange struct {
min int
max int
@@ -145,33 +187,33 @@ func (c *Cron) setField(field cronField, input string) error {
switch field {
case minute:
fr = newRange(0, 59)
cField = &c.Minute
cField = &c.minute
case hour:
fr = newRange(0, 23)
cField = &c.Hour
cField = &c.hour
case dom:
fr = newRange(1, 31)
cField = &c.DoM
cField = &c.dom
case month:
fr = newRange(1, 12)
cField = &c.Month
cField = &c.month
case dow:
fr = newRange(0, 6)
cField = &c.DoW
cField = &c.dow
default:
return errors.New("invalid cron field descriptor")
}
if input == "*" {
*cField = &SpecAny{}
*cField = &specAny{}
} else if strings.HasPrefix(input, "*/") {
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s parsed to %d: invalid value", field, input, parsed)
if fr.valid(parsed) {
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
}
multi = make([]uint8, 0)
current = parsed
@@ -180,7 +222,7 @@ func (c *Cron) setField(field cronField, input string) error {
current += parsed
}
*cField = &SpecMultiOccurrence{
*cField = &specMultiOccurrence{
values: multi,
}
} else {
@@ -189,31 +231,31 @@ func (c *Cron) setField(field cronField, input string) error {
multi = make([]uint8, 0)
for _, s := range multiStr {
if parsed64, err = strconv.ParseUint(s, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
if fr.valid(parsed) {
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
}
multi = append(multi, parsed)
}
*cField = &SpecMultiOccurrence{
*cField = &specMultiOccurrence{
values: multi,
}
} else {
if parsed64, err = strconv.ParseUint(input, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
if fr.valid(parsed) {
return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
}
*cField = &SpecExact{
*cField = &specExact{
value: parsed,
}
}
@@ -222,21 +264,51 @@ func (c *Cron) setField(field cronField, input string) error {
return nil
}
type SpecAny struct{}
type specAny struct{}
type SpecExact struct {
func (a *specAny) Type() wingmate.CronTimeType {
return wingmate.Any
}
func (a *specAny) Match(u uint8) bool {
return true
}
type specExact struct {
value uint8
}
func (e *SpecExact) Value() uint8 {
func (e *specExact) Type() wingmate.CronTimeType {
return wingmate.Exact
}
func (e *specExact) Match(u uint8) bool {
return u == e.value
}
func (e *specExact) Value() uint8 {
return e.value
}
type SpecMultiOccurrence struct {
type specMultiOccurrence struct {
values []uint8
}
func (m *SpecMultiOccurrence) Values() []uint8 {
func (m *specMultiOccurrence) Type() wingmate.CronTimeType {
return wingmate.MultipleOccurrence
}
func (m *specMultiOccurrence) Match(u uint8) bool {
for _, v := range m.values {
if v == u {
return true
}
}
return false
}
func (m *specMultiOccurrence) Values() []uint8 {
out := make([]uint8, len(m.values))
copy(out, m.values)
return out

View File

@@ -1,129 +0,0 @@
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)
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"))
}

View File

@@ -1,26 +0,0 @@
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()
}

View File

@@ -1,37 +0,0 @@
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
}

View File

@@ -1,203 +0,0 @@
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
}

View File

@@ -1,258 +0,0 @@
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)
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 * *"`,
}

View File

@@ -1,22 +0,0 @@
FROM golang:1.22-alpine3.20 AS builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
ARG TEST_BUILD
RUN apk update && apk add git make build-base && \
CGO_ENABLED=1 make all && \
make DESTDIR=/usr/local/bin/wingmate install
FROM alpine:3.20
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" ]

View File

@@ -1,7 +0,0 @@
#!/bin/sh
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,3 +0,0 @@
*/5 * * * * /etc/wingmate/crontab.d/cron1.sh
17,42 */2 * * * /etc/wingmate/crontab.d/cron2.sh
7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh

View File

@@ -1,9 +0,0 @@
#!/bin/sh
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
export WINGMATE_LOG=/var/log/cron1.log
export WINGMATE_LOG_MESSAGE="cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
echo "I'm runnig with dummy=$WINGMATE_DUMMY_PATH, log=$WINGMATE_LOG and mesage=$WINGMATE_LOG_MESSAGE" >> /var/log/debug-cron.log
exec /usr/local/bin/wmoneshot

View File

@@ -1,7 +0,0 @@
#!/bin/sh
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
export WINGMATE_LOG=/var/log/cron2.log
export WINGMATE_LOG_MESSAGE="cron scheduled using 17,42 */2 * * *"
exec /usr/local/bin/wmoneshot

View File

@@ -1,7 +0,0 @@
#!/bin/sh
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
export WINGMATE_LOG=/var/log/cron3.log
export WINGMATE_LOG_MESSAGE="cron entry: 7,19,23,47 22 * * * /etc/wingmate/crontab.d/cron3.sh"
exec /usr/local/bin/wmoneshot

View File

@@ -1,4 +0,0 @@
#!/bin/sh
export DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmexec --setsid --user user1:user1 -- /usr/local/bin/wmstarter

View File

@@ -1,5 +0,0 @@
#!/bin/sh
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmexec --user 1001 -- /usr/local/bin/wmspawner

View File

@@ -1,59 +0,0 @@
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 * * *"

View File

@@ -1,20 +0,0 @@
FROM golang:1.22-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" ]

View File

@@ -1,7 +0,0 @@
#!/usr/bin/bash
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,59 +0,0 @@
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 * * *"

View File

@@ -1,8 +0,0 @@
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" ]

View File

@@ -1,8 +0,0 @@
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" ]

View File

@@ -1,14 +0,0 @@
FROM suyono/wingmate:alpine as source
FROM alpine:3.19
RUN apk update && apk add tzdata openssh-server && \
ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime && ssh-keygen -A
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/
ADD --chmod=755 example/ssh-docker/entry.sh /usr/local/bin/entry.sh
ADD --chmod=755 example/ssh-docker/etc /etc
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,7 +0,0 @@
#!/bin/sh
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,3 +0,0 @@
#!/bin/sh
exec /usr/local/bin/wmpidproxy --pid-file /var/run/sshd.pid -- /usr/sbin/sshd

9
files/testconfig/one.go Normal file
View File

@@ -0,0 +1,9 @@
package testconfig
var One = `service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /
`

6
files/testconfig/one.yml Normal file
View File

@@ -0,0 +1,6 @@
service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /

28
go.mod
View File

@@ -1,14 +1,13 @@
module gitea.suyono.dev/suyono/wingmate
go 1.22.7
go 1.21
require (
github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.25.0
github.com/rs/zerolog v1.30.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.15.0
)
require (
@@ -18,19 +17,22 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

513
go.sum
View File

@@ -1,110 +1,533 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
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.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc=
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,8 +1,6 @@
package init
import (
"io"
"os"
"os/exec"
"sync"
"time"
@@ -10,73 +8,19 @@ import (
"gitea.suyono.dev/suyono/wingmate"
)
const (
cronTag = "cron"
)
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
defer wg.Done()
var (
iwg *sync.WaitGroup
err error
stdout io.ReadCloser
stderr io.ReadCloser
cmd *exec.Cmd
)
ticker := time.NewTicker(time.Second * 30)
ticker := time.NewTicker(time.Second * 20)
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
cmd := exec.Command(cron.Command().Path())
if err := cmd.Run(); err != nil {
wingmate.Log().Error().Msgf("running cron %s error %#v", cron.Command().Path(), err)
}
cmd = exec.Command(cron.Command(), cron.Arguments()...)
cmd.Env = os.Environ()
if cron.EnvLen() > 0 {
cmd.Env = append(cmd.Env, cron.Environ()...)
}
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()

View File

@@ -1,55 +1,22 @@
package init
import (
"os"
"sync"
"time"
)
type Tasks interface {
List() []Task
Services() []ServiceTask
Crones() []CronTask
Get(string) (Task, error)
type Path interface {
Path() string
}
type UserGroup interface {
String() string
IsSet() bool
}
type TaskStatus interface {
}
type Task interface {
Name() string
Command() string
Arguments() []string
EnvLen() int
Environ() []string
Setsid() bool
UserGroup() UserGroup
WorkingDir() string
Status() TaskStatus
UtilDepCheck() error
}
type CronTask interface {
Task
type Cron interface {
Command() Path
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
Services() []Path
Cron() []Cron
}
type Init struct {
@@ -67,26 +34,24 @@ func (i *Init) Start() {
wg *sync.WaitGroup
signalTrigger chan any
sighandlerExit chan any
sigchld chan os.Signal
)
signalTrigger = make(chan any)
sighandlerExit = make(chan any)
sigchld = make(chan os.Signal, 1)
wg = &sync.WaitGroup{}
wg.Add(1)
go i.waiter(wg, signalTrigger, sighandlerExit, sigchld)
go i.waiter(wg, signalTrigger, sighandlerExit)
wg.Add(1)
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
go i.sighandler(wg, signalTrigger, sighandlerExit)
for _, s := range i.config.Tasks().Services() {
for _, s := range i.config.Services() {
wg.Add(1)
go i.service(wg, s, signalTrigger)
}
for _, c := range i.config.Tasks().Crones() {
for _, c := range i.config.Cron() {
wg.Add(1)
go i.cron(wg, c, signalTrigger)
}

View File

@@ -1,91 +1,26 @@
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) {
func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
defer wg.Done()
var (
err error
iwg *sync.WaitGroup
stderr io.ReadCloser
stdout io.ReadCloser
failStatus bool
cmd *exec.Cmd
)
defer func() {
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
}()
service:
for {
failStatus = false
if err = task.UtilDepCheck(); err != nil {
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("%+v", err)
failStatus = true
goto fail
}
cmd = exec.Command(task.Command(), task.Arguments()...)
cmd.Env = os.Environ()
if task.EnvLen() > 0 {
cmd.Env = append(cmd.Env, task.Environ()...)
cmd := exec.Command(path.Path())
if err = cmd.Run(); err != nil {
wingmate.Log().Error().Msgf("starting service %s error %#v", path.Path(), err)
}
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
@@ -94,18 +29,3 @@ service:
}
}
func (i *Init) pipeReader(wg *sync.WaitGroup, pipe io.ReadCloser, tag, serviceName string) {
defer wg.Done()
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
wingmate.Log().Info().Str(tag, serviceName).Msg(scanner.Text())
}
if err := scanner.Err(); err != nil {
wingmate.Log().Error().Str(tag, serviceName).Msgf("got error when reading pipe: %#v", err)
}
wingmate.Log().Info().Str(tag, serviceName).Msg("closing pipe")
}

View File

@@ -5,16 +5,11 @@ import (
"os/signal"
"sync"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix"
)
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any, sigchld chan<- os.Signal) {
defer wg.Done()
defer func() {
wingmate.Log().Warn().Msg("signal handler: exiting")
}()
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
defer wg.Wait()
isOpen := true
@@ -28,21 +23,14 @@ signal:
switch s {
case unix.SIGTERM, unix.SIGINT:
if isOpen {
wingmate.Log().Info().Msg("initiating shutdown...")
close(trigger)
wg.Add(1)
go i.signalPump(wg, selfExit)
isOpen = false
}
case unix.SIGCHLD:
select {
case sigchld <- s:
default:
}
// do nothing
}
case <-selfExit:
wingmate.Log().Warn().Msg("signal handler received completion flag")
break signal
}
}

View File

@@ -1,71 +0,0 @@
package init
import (
"sync"
"time"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix"
)
type status int
const (
triggered status = iota
expired
)
func (i *Init) signalPump(wg *sync.WaitGroup, selfExit <-chan any) {
defer wg.Done()
defer func() {
wingmate.Log().Info().Msg("signal pump completed")
}()
if seStatus := i.sigTermPump(time.Now(), selfExit); seStatus == triggered {
return
}
i.sigKillPump(time.Now(), selfExit)
}
func (i *Init) sigKillPump(startTime time.Time, selfExit <-chan any) {
t := time.NewTicker(time.Millisecond * 200)
defer t.Stop()
wingmate.Log().Info().Msg("start pumping SIGKILL signal")
defer func() {
wingmate.Log().Info().Msg("stop pumping SIGKILL signal")
}()
for time.Since(startTime) < time.Second {
_ = unix.Kill(-1, unix.SIGKILL)
select {
case <-t.C:
case <-selfExit:
return
}
}
}
func (i *Init) sigTermPump(startTime time.Time, selfExit <-chan any) status {
t := time.NewTicker(time.Millisecond * 100)
defer t.Stop()
wingmate.Log().Info().Msg("start pumping SIGTERM signal")
defer func() {
wingmate.Log().Info().Msg("stop pumping SIGTERM signal")
}()
for time.Since(startTime) < time.Duration(time.Second*4) {
_ = unix.Kill(-1, unix.SIGTERM)
select {
case <-t.C:
case <-selfExit:
return triggered
}
}
return expired
}

View File

@@ -2,68 +2,35 @@ 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) {
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any) {
var (
ws unix.WaitStatus
// pid int
err error
running bool
flagged bool
waitingForSignal bool
)
defer wg.Done()
defer func() {
wingmate.Log().Info().Msg("waiter exiting...")
}()
running = true
flagged = true
waitingForSignal = true
wait:
for {
if running {
if waitingForSignal {
select {
case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
case <-sigchld:
waitingForSignal = false
}
} else {
select {
case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
default:
}
}
}
if _, err = unix.Wait4(-1, &ws, 0, nil); err != nil {
if errors.Is(err, unix.ECHILD) {
if !running {
if flagged {
close(sigHandlerFlag)
flagged = false
wingmate.Log().Warn().Msg("waiter: inner flag")
}
wingmate.Log().Warn().Msg("waiter: no child left")
break wait
}
}
wingmate.Log().Warn().Msgf("Wait4 returns error: %+v", err)
waitingForSignal = true
}
}
}

View File

@@ -1,15 +1,9 @@
package wingmate
import (
"io"
"time"
"gitea.suyono.dev/suyono/wingmate/logger"
"github.com/rs/zerolog"
)
const (
timeTag = "time"
"io"
)
var (
@@ -35,37 +29,13 @@ func Log() logger.Log {
}
func (w *wrapper) Info() logger.Content {
return (*eventWrapper)(w.log.Info().Time(timeTag, time.Now()))
return w.log.Info()
}
func (w *wrapper) Warn() logger.Content {
return (*eventWrapper)(w.log.Warn().Time(timeTag, time.Now()))
return w.log.Warn()
}
func (w *wrapper) Error() logger.Content {
return (*eventWrapper)(w.log.Error().Time(timeTag, time.Now()))
}
func (w *wrapper) Fatal() logger.Content {
return (*eventWrapper)(w.log.Fatal().Time(timeTag, time.Now()))
}
type eventWrapper zerolog.Event
func (w *eventWrapper) Msg(msg string) {
(*zerolog.Event)(w).Msg(msg)
}
func (w *eventWrapper) Msgf(format string, data ...any) {
(*zerolog.Event)(w).Msgf(format, data...)
}
func (w *eventWrapper) Str(key, value string) logger.Content {
rv := (*zerolog.Event)(w).Str(key, value)
return (*eventWrapper)(rv)
}
func (w *eventWrapper) Err(err error) logger.Content {
rv := (*zerolog.Event)(w).Err(err)
return (*eventWrapper)(rv)
return w.log.Error()
}

View File

@@ -3,15 +3,12 @@ package logger
type Content interface {
Msg(string)
Msgf(string, ...any)
Str(string, string) Content
Err(error) Content
}
type Level interface {
Info() Content
Warn() Content
Error() Content
Fatal() Content
}
type Log interface {

View File

@@ -1,292 +0,0 @@
package task
import (
"crypto/sha256"
"encoding/json"
"fmt"
"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
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) 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() string {
return c.cmdLine[0]
}
func (c *CronTask) Arguments() []string {
if len(c.cmdLine) == 1 {
return nil
}
retval := make([]string, len(c.cmdLine)-1)
copy(retval, c.cmdLine[1:])
return retval
}
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) 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
}

View File

@@ -1,374 +0,0 @@
package task
import (
"crypto/sha256"
"encoding/json"
"fmt"
"gitea.suyono.dev/suyono/wingmate"
wminit "gitea.suyono.dev/suyono/wingmate/init"
)
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
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) 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 {
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() string {
return t.cmdLine[0]
}
func (t *ServiceTask) Arguments() []string {
if len(t.cmdLine) == 1 {
return nil
}
retval := make([]string, len(t.cmdLine)-1)
copy(retval, t.cmdLine[1:])
return retval
}
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) 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
}

View File

@@ -1,79 +0,0 @@
package task
import (
wminit "gitea.suyono.dev/suyono/wingmate/init"
"github.com/stretchr/testify/assert"
"testing"
)
func TestServicesV0(t *testing.T) {
service := "/path/to/executable"
tasks := NewTasks()
tasks.AddV0Service(service)
assert.Equal(t, tasks.Services()[0].Name(), service)
assert.ElementsMatch(t, tasks.Services()[0].Command(), []string{service})
}
func TestCronV0(t *testing.T) {
cron := "/path/to/executable"
tasks := NewTasks()
tasks.AddV0Cron(CronSchedule{
Minute: NewCronAnySpec(),
Hour: NewCronAnySpec(),
DoM: NewCronAnySpec(),
Month: NewCronAnySpec(),
DoW: NewCronAnySpec(),
}, cron)
assert.Equal(t, tasks.Crones()[0].Name(), cron)
assert.ElementsMatch(t, tasks.Crones()[0].Command(), []string{cron})
}
func TestTasks_List(t *testing.T) {
tasks := NewTasks()
tasks.services = []wminit.ServiceTask{
&ServiceTask{
name: "one",
command: []string{"/path/to/executable"},
},
&ServiceTask{
name: "two",
command: []string{"/path/to/executable"},
},
}
tasks.crones = []wminit.CronTask{
&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)
}

View File

@@ -6,6 +6,4 @@ const (
Any CronTimeType = iota
Exact
MultipleOccurrence
EnvPrefix = "WINGMATE"
)

View File

@@ -1,183 +0,0 @@
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 (059)
│ ┌───────────── hour (023)
│ │ ┌───────────── day of the month (131)
│ │ │ ┌───────────── month (112)
│ │ │ │ ┌───────────── day of the week (06) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *
```