17 Commits

Author SHA1 Message Date
08a040ac72 doc: getting-started 2023-12-27 07:51:31 +11:00
6df8ba2810 fix doc: dir reference 2023-12-26 10:00:04 +11:00
7785b3ec48 fix(pidproxy): replace Timer with Ticker
tested: ssh-docker
2023-12-26 09:53:37 +11:00
70a4d132c3 Merge branch 'main' into readme 2023-12-26 09:21:09 +11:00
b15066b513 fix(pidproxy): handle new line
example: sshd
2023-12-26 09:20:42 +11:00
e2275ef05e wip doc: merge getting started to README.md 2023-12-26 08:23:34 +11:00
541228bf68 wip: writing documentation 2023-12-25 13:38:18 +11:00
a87c568335 wip: config unit test 2023-12-25 13:13:03 +11:00
574d3deb56 wip: getting started 2023-12-19 15:54:28 +11:00
5679faecdb chore: exclude test binary on default build 2023-12-18 12:28:14 +00:00
2a4629c017 wip: README.md 2023-12-18 22:45:19 +11:00
c043c91f0e chore(Makefiles): to simplify Dockerfile
fix(wmexec): setgid before setuid
test(wmexec): tested
2023-12-17 23:45:03 +00:00
15a804aa7d feat(exec): cgo call to getpwnam and getgrnam 2023-12-17 05:23:26 +00:00
653b4ff158 feat(exec): initial 2023-12-17 03:36:47 +00:00
8cf92167df chore: fix .gitignore
feat(waiter): waiting for signal
2023-12-14 00:11:39 +00:00
9c74296c27 chore(pidproxy): remove binary from git and update .gitignore 2023-12-12 01:25:31 +00:00
8a85ad5107 feat(pidproxy): added signal handler 2023-12-12 00:29:32 +00:00
31 changed files with 651 additions and 68 deletions

View File

@@ -18,7 +18,8 @@
"extensions": [
"golang.go",
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools"
"ms-vscode.makefile-tools",
"ms-vscode.cpptools-extension-pack"
]
}
}

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/cmd/wingmate/wingmate
/cmd/pidproxy/pidproxy
/cmd/exec/exec

View File

@@ -1,6 +1,13 @@
DESTDIR = /usr/local/bin
installs = install-dir
programs = wingmate pidproxy exec
ifdef TEST_BUILD
programs += oneshot spawner starter dummy
installs += install-test
endif
all: wingmate dummy oneshot spawner starter pidproxy
all: ${programs}
wingmate:
$(MAKE) -C cmd/wingmate all
@@ -8,6 +15,9 @@ wingmate:
pidproxy:
$(MAKE) -C cmd/pidproxy all
exec:
$(MAKE) -C cmd/exec all
dummy:
$(MAKE) -C cmd/experiment/dummy all
@@ -23,7 +33,22 @@ starter:
clean:
$(MAKE) -C cmd/wingmate clean
$(MAKE) -C cmd/pidproxy clean
$(MAKE) -C cmd/exec clean
$(MAKE) -C cmd/experiment/dummy clean
$(MAKE) -C cmd/experiment/oneshot clean
$(MAKE) -C cmd/experiment/spawner clean
$(MAKE) -C cmd/experiment/starter clean
install: ${installs}
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
install-test:
$(MAKE) -C cmd/experiment/dummy DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/oneshot DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/spawner DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/starter DESTDIR=${DESTDIR} install
install-dir:
install -d ${DESTDIR}

137
README.md
View File

@@ -1,2 +1,139 @@
# wingmate
Wingmate is a process manager for services. It works like init. It starts and restarts services.
It also has cron feature. It is designed to run in a container/docker.
The Wingmate binary do not need any external dependency.
Just copy the binary, and exec from the entry point script.
# Getting Started
## Binaries
There are three binaries in this project: `wingmate`, `wmpidproxy`, and `wmexec`.
`wingmate` is the core binary. It reads config, starts, restarts services. It also
runs cron. Read the [configuration](#configuration) section for files needed to run
`wingmate`.
`wmpidproxy` is a helper binary for monitoring _legacy style_ service (fork, exit
initial proces, and continue in background). Read [here](#wingmate-pid-proxy-binary)
for further details about `wmpidproxy`.
`wmexec` is a helper binary for running process in different `user` or `group`.
It also useful for setting the process as process group leader.
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
## Building a container image based on wingmate image in Docker Hub
Wingmate has no dependency other than `alpine` base image, so you just need to copy
the binaries directly. If you have built your application into an `alpine` based image,
all you need to do is copy whichever binary you need, crontab file (if you use cron)
and add some shell script to glue them together. Here is a Dockerfile example.
```Dockerfile
# Dockerfile
FROM suyono/wingmate:alpine as source
FROM alpine:latest
ADD --chmod=755 wingmate/ /etc/wingmate/
ADD --chmod=755 entry.sh /usr/local/bin/entry.sh
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/wingmate
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/wmpidproxy
COPY --from=source /usr/local/bin/wmexec /usr/local/bin/wmexec
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]
```
You can find some examples for shell script in [alpine docker](docker/alpine) and
[bookworm docker](docker/bookworm).
## Configuration
When `wingmate` binary starts, it will look for some files. By default, it will
try to read the content of `/etc/wingmate` directory. You can change the directory
where it reads by setting `WINGMATE_CONFIG_PATH` environment variable. The structure
inside the config path should look like this.
```shell
/etc
└── wingmate
├── crontab
├── crontab.d
│   ├── cron1.sh
│   ├── cron2.sh
│   └── cron3.sh
└── service
├── one.sh
└── spawner.sh
```
First, `wingmate` will try to read the content of `service` directory. The content of
this directory should be executables (either shell scripts or binaries). The wingmate
will run every executable in `service` directory without going into any subdirectory.
Next, `wingmate` will read the `crontab` file. `wingmate` expects the `crontab` file using
common UNIX crontab file format. Something like this
```shell
┌───────────── minute (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
yet. It is recommended to write a shell script and put the path to shell script in
the command part.
# Appendix
## Wingmate PID Proxy binary
`wingmate` works by monitoring its direct children process. When it sees one of its children
exited, it will start the child process again.
Sometimes you find some services work by running in the background. It means it forks a new
process, disconnect the new child from terminal, exit the parent process, and continue
running in the child process. This kind of service usually write its background process
PID in a pid file.
To monitor the background services, `wingmate` utilizes `wmpidproxy`. `wmpidproxy` runs
in foreground in-place of the background service. It also periodically check whether the
background service is still running, in current implementation it checks every second.
```shell
wmpidproxy --pid-file <path to pid file> -- <background service binary/start script>
```
#### Example
Running sshd background with `wingmate` and `wmpidproxy`: [here](example/ssh-docker)
#### Note
It is recommended to configure services to run in foreground if they support it. When services
running in foreground, they are running as direct children process of `wingmate`.
`wingmate` monitors children process effectively. Whenever a child process exited/terminated,
`wingmate` will start it again quickly. Running in foreground also removes the overhead of running
`wmpidproxy` together with the service.
## Wingmate Exec binary
`wingmate` runs all the services as its children using the same `uid`, `gid`, and in the
same process group. You can use `wmexec` to run service in different `uid`, `gid`, or make
the service process as its own process group leader.
#### Syntax
```shell
wmexec [--user <uid>[:<gid>]] [--setsid] -- <target executable>
```
| Option | Parameter | Description |
|----------|-----------|----------------------------------------------------------------------------------------------------------|
| --user | uid[:gid] | Set the real user ID and the real group id. Uid and Gid can be either in numeric form or in name form |
| --setsid | | Set the process become the leader of its own process group, effectively detaching from parent's terminal |
#### Example
You can find example for `wmexec` in [here](docker/alpine/etc/wingmate) and [here](docker/bookworm/etc/wingmate)

8
cmd/exec/Makefile Normal file
View File

@@ -0,0 +1,8 @@
all:
go build -v
clean:
go clean -i -cache -testcache
install:
install exec ${DESTDIR}/wmexec

11
cmd/exec/ent.go Normal file
View File

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

51
cmd/exec/ent_lin.go Normal file
View File

@@ -0,0 +1,51 @@
//go:build cgo && linux
package main
/*
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<pwd.h>
#include<grp.h>
static uid_t getuid(const char* username) {
struct passwd local, *rv;
errno = 0;
rv = getpwnam(username);
if (errno != 0) {
return 0;
}
memcpy(&local, rv, sizeof(struct passwd));
return local.pw_uid;
}
static gid_t getgid(const char* groupname) {
struct group local, *rv;
errno = 0;
rv = getgrnam(groupname);
if (errno != 0) {
return 0;
}
memcpy(&local, rv, sizeof(struct group));
return local.gr_gid;
}
*/
import "C"
func getUid(user string) (uint64, error) {
u, err := C.getuid(C.CString(user))
if err != nil {
return 0, err
}
return uint64(u), nil
}
func getGid(group string) (uint64, error) {
g, err := C.getgid(C.CString(group))
if err != nil {
return 0, err
}
return uint64(g), nil
}

133
cmd/exec/exec.go Normal file
View File

@@ -0,0 +1,133 @@
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
)
const (
setsidFlag = "setsid"
EnvSetsid = "SETSID"
userFlag = "user"
EnvUser = "USER"
)
var (
rootCmd = &cobra.Command{
Use: "wmexec",
RunE: execCmd,
}
childArgs []string
)
func main() {
var (
found bool
i int
arg string
selfArgs []string
)
rootCmd.PersistentFlags().BoolP(setsidFlag, "s", false, "set to true to run setsid() before exec")
viper.BindPFlag(EnvSetsid, rootCmd.PersistentFlags().Lookup(setsidFlag))
rootCmd.PersistentFlags().StringP(userFlag, "u", "", "\"user:[group]\"")
viper.BindPFlag(EnvUser, rootCmd.PersistentFlags().Lookup(userFlag))
viper.SetEnvPrefix(wingmate.EnvPrefix)
viper.BindEnv(EnvUser)
viper.BindEnv(EnvSetsid)
viper.SetDefault(EnvSetsid, false)
viper.SetDefault(EnvUser, "")
found = false
for i, arg = range os.Args {
if arg == "--" {
found = true
if len(os.Args) <= i+1 {
log.Println("invalid argument")
os.Exit(1)
}
selfArgs = os.Args[1:i]
childArgs = os.Args[i+1:]
break
}
}
if !found {
log.Println("invalid argument")
os.Exit(1)
}
if len(childArgs) == 0 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.SetArgs(selfArgs)
if err := rootCmd.Execute(); err != nil {
log.Println(err)
os.Exit(1)
}
}
func execCmd(cmd *cobra.Command, args []string) error {
if viper.GetBool(EnvSetsid) {
_, _ = unix.Setsid()
}
var (
uid uint64
gid uint64
err error
path string
)
ug := viper.GetString(EnvUser)
if len(ug) > 0 {
user, group, ok := strings.Cut(ug, ":")
if ok {
if gid, err = strconv.ParseUint(group, 10, 32); err != nil {
if gid, err = getGid(group); err != nil {
return fmt.Errorf("cgo getgid: %w", err)
}
}
if err = unix.Setgid(int(gid)); err != nil {
return fmt.Errorf("setgid: %w", err)
}
}
uid, err = strconv.ParseUint(user, 10, 32)
if err != nil {
if uid, err = getUid(user); err != nil {
return fmt.Errorf("cgo getuid: %w", err)
}
}
if err = unix.Setuid(int(uid)); err != nil {
return fmt.Errorf("setuid: %w", err)
}
}
if path, err = exec.LookPath(childArgs[0]); err != nil {
if !errors.Is(err, exec.ErrDot) {
return fmt.Errorf("lookpath: %w", err)
}
}
if err = unix.Exec(path, childArgs, os.Environ()); err != nil {
return fmt.Errorf("exec: %w", err)
}
return nil
}

View File

@@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install dummy ${DESTDIR}/wmdummy

View File

@@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install oneshot ${DESTDIR}/wmoneshot

View File

@@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install spawner ${DESTDIR}/wmspawner

View File

@@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install starter ${DESTDIR}/wmstarter

View File

@@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install pidproxy ${DESTDIR}/wmpidproxy

View File

@@ -1,9 +1,12 @@
package main
import (
"bufio"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
@@ -41,11 +44,6 @@ func main() {
viper.BindEnv(EnvStartSecs)
viper.SetDefault(EnvStartSecs, EnvDefaultStartSecs)
if len(os.Args) <= 2 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.PersistentFlags().StringP(pidFileFlag, "p", "", "location of pid file")
rootCmd.MarkFlagRequired(pidFileFlag)
viper.BindPFlag(pidFileFlag, rootCmd.PersistentFlags().Lookup(pidFileFlag))
@@ -54,11 +52,11 @@ func main() {
for i, arg = range os.Args {
if arg == "--" {
found = true
selfArgs = os.Args[1:i]
if len(os.Args) <= i+1 {
log.Println("invalid argument")
os.Exit(1)
}
selfArgs = os.Args[1:i]
childArgs = os.Args[i+1:]
break
}
@@ -68,6 +66,11 @@ func main() {
os.Exit(1)
}
if len(childArgs) == 0 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.SetArgs(selfArgs)
if err := rootCmd.Execute(); err != nil {
@@ -90,7 +93,16 @@ func pidProxy(cmd *cobra.Command, args []string) error {
var (
err error
pid int
sc chan os.Signal
t *time.Ticker
)
sc = make(chan os.Signal, 1)
signal.Notify(sc, unix.SIGTERM)
t = time.NewTicker(time.Second)
check:
for {
if pid, err = readPid(pidfile); err != nil {
return err
@@ -100,16 +112,26 @@ func pidProxy(cmd *cobra.Command, args []string) error {
return err
}
time.Sleep(time.Second)
select {
case <-t.C:
case <-sc:
if pid, err = readPid(pidfile); err != nil {
return err
}
if err = unix.Kill(pid, unix.SIGTERM); err != nil {
return err
}
break check
}
}
return nil
}
func readPid(pidFile string) (int, error) {
var (
file *os.File
err error
buf []byte
n int
pid64 int64
)
@@ -120,18 +142,15 @@ func readPid(pidFile string) (int, error) {
_ = file.Close()
}()
buf = make([]byte, 1024)
n, err = file.Read(buf)
if err != nil {
return 0, err
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")
}
pid64, err = strconv.ParseInt(string(buf[:n]), 10, 64)
if err != nil {
return 0, err
}
return int(pid64), nil
}
func startProcess(arg0 string, args ...string) {

View File

@@ -3,3 +3,7 @@ all:
clean:
go clean -i -cache -testcache
install:
install wingmate ${DESTDIR}/wingmate

View File

@@ -7,6 +7,7 @@ import (
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
)
const (
@@ -48,8 +49,11 @@ func Read() (*Config, error) {
if len(dirent) > 0 {
for _, d := range dirent {
if d.Type().IsRegular() {
serviceAvailable = true
outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
svcPath := filepath.Join(svcdir, d.Name())
if err = unix.Access(svcPath, unix.X_OK); err == nil {
serviceAvailable = true
outConfig.ServicePaths = append(outConfig.ServicePaths, svcPath)
}
}
}
}

130
config/config_test.go Normal file
View File

@@ -0,0 +1,130 @@
package config
import (
"os"
"path"
"testing"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
type testEntry struct {
name string
testFunc func(t *testing.T)
}
var (
configDir string
err error
)
const serviceDir = "service"
setup := func(t *testing.T) {
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
t.Fatal("setup", err)
}
viper.Set(EnvConfigPath, configDir)
}
tear := func(t *testing.T) {
if err = os.RemoveAll(configDir); err != nil {
t.Fatal("tear", err)
}
}
mkSvcDir := func(t *testing.T) {
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.ServicePaths,
[]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.ServicePaths,
[]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.ServicePaths,
[]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

@@ -2,19 +2,17 @@ FROM golang:1.21-alpine as builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
RUN apk add make && make all
ARG TEST_BUILD
RUN apk add make build-base && CGO_ENABLED=1 make all && make DESTDIR=/usr/local/bin/wingmate install
FROM alpine:3.18
RUN apk add tzdata && ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime
COPY --from=builder /root/wingmate/cmd/wingmate/wingmate /usr/local/bin/wingmate
COPY --from=builder /root/wingmate/cmd/experiment/dummy/dummy /usr/local/bin/wmdummy
COPY --from=builder /root/wingmate/cmd/experiment/starter/starter /usr/local/bin/wmstarter
COPY --from=builder /root/wingmate/cmd/experiment/oneshot/oneshot /usr/local/bin/wmoneshot
COPY --from=builder /root/wingmate/cmd/experiment/spawner/spawner /usr/local/bin/wmspawner
COPY --from=builder /root/wingmate/cmd/pidproxy/pidproxy /usr/local/bin/wmpidproxy
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

View File

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

View File

@@ -2,4 +2,4 @@
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmspawner
exec /usr/local/bin/wmexec --user 1001 -- /usr/local/bin/wmspawner

View File

@@ -2,19 +2,17 @@ FROM golang:1.21-bookworm as builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
RUN make all
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
COPY --from=builder /root/wingmate/cmd/wingmate/wingmate /usr/local/bin/wingmate
COPY --from=builder /root/wingmate/cmd/experiment/dummy/dummy /usr/local/bin/wmdummy
COPY --from=builder /root/wingmate/cmd/experiment/starter/starter /usr/local/bin/wmstarter
COPY --from=builder /root/wingmate/cmd/experiment/oneshot/oneshot /usr/local/bin/wmoneshot
COPY --from=builder /root/wingmate/cmd/experiment/spawner/spawner /usr/local/bin/wmspawner
COPY --from=builder /root/wingmate/cmd/pidproxy/pidproxy /usr/local/bin/wmpidproxy
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

View File

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

View File

@@ -2,4 +2,4 @@
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmspawner
exec /usr/local/bin/wmexec --user 1200 -- /usr/local/bin/wmspawner

View File

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

View File

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

View File

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

1
go.mod
View File

@@ -28,6 +28,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect

3
go.sum
View File

@@ -201,12 +201,15 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=

View File

@@ -1,6 +1,7 @@
package init
import (
"os"
"sync"
"time"
)
@@ -34,17 +35,19 @@ func (i *Init) Start() {
wg *sync.WaitGroup
signalTrigger chan any
sighandlerExit chan any
sigchld chan os.Signal
)
signalTrigger = make(chan any)
sighandlerExit = make(chan any)
sigchld = make(chan os.Signal, 1)
wg = &sync.WaitGroup{}
wg.Add(1)
go i.waiter(wg, signalTrigger, sighandlerExit)
go i.waiter(wg, signalTrigger, sighandlerExit, sigchld)
wg.Add(1)
go i.sighandler(wg, signalTrigger, sighandlerExit)
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
for _, s := range i.config.Services() {
wg.Add(1)

View File

@@ -9,7 +9,7 @@ import (
"golang.org/x/sys/unix"
)
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any, sigchld chan<- os.Signal) {
defer wg.Done()
defer func() {
@@ -35,7 +35,10 @@ signal:
isOpen = false
}
case unix.SIGCHLD:
// do nothing
select {
case sigchld <- s:
default:
}
}
case <-selfExit:

View File

@@ -2,20 +2,20 @@ package init
import (
"errors"
"os"
"sync"
"time"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix"
)
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any) {
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any, sigchld <-chan os.Signal) {
var (
ws unix.WaitStatus
// pid int
err error
running bool
flagged bool
ws unix.WaitStatus
err error
running bool
flagged bool
waitingForSignal bool
)
defer wg.Done()
@@ -25,14 +25,27 @@ func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag
running = true
flagged = true
waitingForSignal = true
wait:
for {
if running {
select {
case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
default:
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:
}
}
}
@@ -50,7 +63,7 @@ wait:
}
wingmate.Log().Warn().Msgf("Wait4 returns error: %+v", err)
time.Sleep(time.Millisecond * 100)
waitingForSignal = true
}
}
}