wip: daemon
This commit is contained in:
@@ -1,9 +1,71 @@
|
||||
package daemon
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type InstanceType int
|
||||
|
||||
type WingMateDescriptor interface {
|
||||
String() string
|
||||
Name() string
|
||||
InstanceName() string
|
||||
InstanceType() InstanceType
|
||||
Error() string // so the descriptor can be wrapped inside an error
|
||||
}
|
||||
|
||||
type process struct {
|
||||
cmd *exec.Cmd
|
||||
descriptor WingMateDescriptor
|
||||
}
|
||||
|
||||
type daemon struct {
|
||||
running map[string]process
|
||||
available map[string]process
|
||||
services map[string]WingMateDescriptor
|
||||
cron map[string]WingMateDescriptor
|
||||
}
|
||||
|
||||
const (
|
||||
Service InstanceType = iota
|
||||
Cron
|
||||
)
|
||||
|
||||
func (t InstanceType) String() string {
|
||||
switch t {
|
||||
case Service:
|
||||
return config.ServiceKey
|
||||
case Cron:
|
||||
return config.CronKey
|
||||
default:
|
||||
return "[unknown instance type]"
|
||||
}
|
||||
}
|
||||
|
||||
func Start(cmd *cobra.Command, args []string) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
_, _ = cmd, args // prevent warnings for unused arguments
|
||||
|
||||
d := &daemon{
|
||||
running: make(map[string]process),
|
||||
available: make(map[string]process),
|
||||
services: make(map[string]WingMateDescriptor),
|
||||
cron: make(map[string]WingMateDescriptor),
|
||||
}
|
||||
if err = d.buildServiceMap(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *daemon) buildServiceMap() error {
|
||||
for s := range viper.GetStringMap(Service.String()) {
|
||||
_ = s //TODO: resume work
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +1,117 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type ProcessConfig interface {
|
||||
Descriptor() WingMateDescriptor
|
||||
|
||||
// Name returns the binary name to be executed, to be passed in to exec.Command
|
||||
Name() string
|
||||
|
||||
// Args returns the arguments for the process, to be passed in to exec.Command
|
||||
Args() []string
|
||||
Env() map[string]string
|
||||
Dir() string
|
||||
WorkingDir() string
|
||||
LogChannel() chan<- any
|
||||
ControlChannel() chan<- any
|
||||
}
|
||||
|
||||
func StartProcess(config ProcessConfig) error {
|
||||
cmd := exec.Command(config.Name(), config.Args()...)
|
||||
for k, v := range config.Env() {
|
||||
type StreamID int
|
||||
|
||||
type ProcessLogEntry struct {
|
||||
ID WingMateDescriptor
|
||||
LogEntry string
|
||||
}
|
||||
|
||||
const (
|
||||
Stdout StreamID = iota
|
||||
Stderr
|
||||
)
|
||||
|
||||
func (s StreamID) String() string {
|
||||
switch s {
|
||||
case Stdout:
|
||||
return "stdout"
|
||||
case Stderr:
|
||||
return "stderr"
|
||||
default:
|
||||
return "[unknown stream]"
|
||||
}
|
||||
}
|
||||
|
||||
func StartProcess(config ProcessConfig) (*exec.Cmd, error) {
|
||||
var (
|
||||
err error
|
||||
cmd *exec.Cmd
|
||||
k, v string
|
||||
stdoutPipe, stderrPipe io.ReadCloser
|
||||
)
|
||||
cmd = exec.Command(config.Name(), config.Args()...)
|
||||
cmd.Env = os.Environ()
|
||||
for k, v = range config.Env() {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
cmd.Dir = config.Dir()
|
||||
cmd.Dir = config.WorkingDir()
|
||||
if stdoutPipe, err = cmd.StdoutPipe(); err != nil {
|
||||
return nil, fmt.Errorf("set up stdout pipe on process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
|
||||
}
|
||||
if stderrPipe, err = cmd.StderrPipe(); err != nil {
|
||||
return nil, fmt.Errorf("set up stderr pipe on process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
|
||||
}
|
||||
|
||||
return nil
|
||||
if err = cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("starting process %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
|
||||
}
|
||||
|
||||
go serviceLogReader(config, stdoutPipe, Stdout)
|
||||
go serviceLogReader(config, stderrPipe, Stderr)
|
||||
|
||||
go serviceWaiter(config, cmd)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func serviceLogReader(config ProcessConfig, stream io.Reader, id StreamID) {
|
||||
out := config.LogChannel()
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
out <- o
|
||||
//TODO: include stack trace, make the return object an error
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stream)
|
||||
for scanner.Scan() {
|
||||
out <- ProcessLogEntry{
|
||||
ID: config.Descriptor(),
|
||||
LogEntry: scanner.Text(),
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
out <- fmt.Errorf("read stream from %w: %w %w", config.Descriptor(), err, debugframes.GetTraces())
|
||||
}
|
||||
}
|
||||
|
||||
func serviceWaiter(config ProcessConfig, cmd *exec.Cmd) {
|
||||
ctrl := config.ControlChannel()
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
ctrl <- o
|
||||
//TODO: include stack trace
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
ctrl <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
|
||||
} else {
|
||||
ctrl <- config.Descriptor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
package daemon
|
||||
|
||||
func DetectDeadChildProcess() {
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
anyChildProcess = -1
|
||||
)
|
||||
|
||||
func WaitChildProcess() (int, error) {
|
||||
var ws unix.WaitStatus
|
||||
return unix.Wait4(anyChildProcess, &ws, 0, nil)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ func TestWait(t *testing.T) {
|
||||
err error
|
||||
pid int
|
||||
testpid int
|
||||
ws unix.WaitStatus
|
||||
)
|
||||
|
||||
cmd := exec.Command("sleep", "10")
|
||||
@@ -37,22 +36,14 @@ func TestWait(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
testpid, err = unix.Wait4(-1, &ws, 0, nil)
|
||||
testpid, err = WaitChildProcess()
|
||||
if err != nil {
|
||||
t.Fatal("wait4 error:", err)
|
||||
}
|
||||
|
||||
if ws.Signaled() {
|
||||
t.Log("the child process got signal:", ws.Signal())
|
||||
}
|
||||
|
||||
if ws.Exited() {
|
||||
t.Log("the child process exit with status:", ws.ExitStatus())
|
||||
}
|
||||
|
||||
// if err = cmd.Wait(); err != nil {
|
||||
// t.Fatal("is this expected?", err)
|
||||
// }
|
||||
//if err = cmd.Wait(); err != nil {
|
||||
// t.Fatal("is this expected?", err)
|
||||
//}
|
||||
|
||||
assert.Equal(t, pid, testpid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user