wip: refactor for new config format
This commit is contained in:
parent
b2668287ae
commit
22fee125bc
55
.idea/remote-targets.xml
generated
Normal file
55
.idea/remote-targets.xml
generated
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteTargetsManager">
|
||||
<targets>
|
||||
<target name="golang-dev:1.21-bookworm-user" type="docker" uuid="5b79636b-3db9-4e2a-9fd3-03277653ae58">
|
||||
<config>
|
||||
<option name="targetPlatform">
|
||||
<TargetPlatform />
|
||||
</option>
|
||||
<option name="buildImageConfig">
|
||||
<BuildImageConfig>
|
||||
<option name="dockerFile" value="docker/bookworm/Dockerfile" />
|
||||
</BuildImageConfig>
|
||||
</option>
|
||||
<option name="buildNotPull" value="false" />
|
||||
<option name="containerConfig">
|
||||
<ContainerConfig>
|
||||
<option name="runCliOptions" value="-u "1000:1000" --rm" />
|
||||
</ContainerConfig>
|
||||
</option>
|
||||
<option name="pullImageConfig">
|
||||
<PullImageConfig>
|
||||
<option name="tagToPull" value="golang-dev:1.21-bookworm-user" />
|
||||
</PullImageConfig>
|
||||
</option>
|
||||
</config>
|
||||
<ContributedStateBase type="GoLanguageRuntime">
|
||||
<config>
|
||||
<option name="compiledExecutablesVolume">
|
||||
<VolumeState>
|
||||
<option name="targetSpecificBits">
|
||||
<map>
|
||||
<entry key="mountAsVolume" value="false" />
|
||||
</map>
|
||||
</option>
|
||||
</VolumeState>
|
||||
</option>
|
||||
<option name="goPath" value="/go" />
|
||||
<option name="goRoot" value="/usr/local/go/bin/go" />
|
||||
<option name="goVersion" value="go1.21.5 linux/amd64" />
|
||||
<option name="projectSourcesVolume">
|
||||
<VolumeState>
|
||||
<option name="targetSpecificBits">
|
||||
<map>
|
||||
<entry key="mountAsVolume" value="false" />
|
||||
</map>
|
||||
</option>
|
||||
</VolumeState>
|
||||
</option>
|
||||
</config>
|
||||
</ContributedStateBase>
|
||||
</target>
|
||||
</targets>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,60 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||
"gitea.suyono.dev/suyono/wingmate/task"
|
||||
)
|
||||
|
||||
type wPath struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (p wPath) Path() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
type wConfig struct {
|
||||
services []wminit.Path
|
||||
cron []wminit.Cron
|
||||
tasks *task.Tasks
|
||||
}
|
||||
|
||||
func (c wConfig) Services() []wminit.Path {
|
||||
return c.services
|
||||
func (c *wConfig) Tasks() wminit.Tasks {
|
||||
return c.tasks
|
||||
}
|
||||
|
||||
func (c wConfig) Cron() []wminit.Cron {
|
||||
return c.cron
|
||||
}
|
||||
|
||||
type wCron struct {
|
||||
iCron *config.Cron
|
||||
}
|
||||
|
||||
func (c wCron) TimeToRun(now time.Time) bool {
|
||||
return c.iCron.TimeToRun(now)
|
||||
}
|
||||
|
||||
func (c wCron) Command() wminit.Path {
|
||||
return wPath{
|
||||
path: c.iCron.Command(),
|
||||
}
|
||||
}
|
||||
|
||||
func convert(cfg *config.Config) wConfig {
|
||||
retval := wConfig{
|
||||
services: make([]wminit.Path, 0, len(cfg.ServicePaths)),
|
||||
cron: make([]wminit.Cron, 0, len(cfg.Cron)),
|
||||
func convert(cfg *config.Config) *wConfig {
|
||||
retval := &wConfig{
|
||||
tasks: task.NewTasks(),
|
||||
}
|
||||
|
||||
for _, s := range cfg.ServicePaths {
|
||||
retval.services = append(retval.services, wPath{path: s})
|
||||
retval.tasks.AddV0Service(s)
|
||||
|
||||
}
|
||||
|
||||
var schedule task.CronSchedule
|
||||
for _, c := range cfg.Cron {
|
||||
retval.cron = append(retval.cron, wCron{iCron: c})
|
||||
schedule.Minute = convertSchedule(c.Minute)
|
||||
schedule.Hour = convertSchedule(c.Hour)
|
||||
schedule.DoM = convertSchedule(c.DoM)
|
||||
schedule.Month = convertSchedule(c.Month)
|
||||
schedule.DoW = convertSchedule(c.DoW)
|
||||
|
||||
retval.tasks.AddV0Cron(schedule, c.Command)
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func convertSchedule(cfg config.CronTimeSpec) task.CronTimeSpec {
|
||||
switch v := cfg.(type) {
|
||||
case config.SpecAny:
|
||||
return task.NewCronAnySpec()
|
||||
case config.SpecExact:
|
||||
return task.NewCronExactSpec(v.Value())
|
||||
case config.SpecMultiOccurrence:
|
||||
return task.NewCronMultiOccurrenceSpec(v.Values()...)
|
||||
}
|
||||
|
||||
panic("invalid conversion")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -10,33 +10,35 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
|
||||
type testEntry struct {
|
||||
name string
|
||||
testFunc func(t *testing.T)
|
||||
}
|
||||
const (
|
||||
serviceDir = "service"
|
||||
)
|
||||
|
||||
var (
|
||||
configDir string
|
||||
err error
|
||||
)
|
||||
|
||||
const serviceDir = "service"
|
||||
|
||||
setup := func(t *testing.T) {
|
||||
func setup(t *testing.T) {
|
||||
var err error
|
||||
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
||||
t.Fatal("setup", err)
|
||||
}
|
||||
viper.Set(EnvConfigPath, configDir)
|
||||
}
|
||||
|
||||
tear := func(t *testing.T) {
|
||||
if err = os.RemoveAll(configDir); err != nil {
|
||||
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)
|
||||
|
||||
@ -4,46 +4,34 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
)
|
||||
|
||||
type CronExactSpec interface {
|
||||
CronTimeSpec
|
||||
Value() uint8
|
||||
}
|
||||
|
||||
type CronMultipleOccurrenceSpec interface {
|
||||
CronTimeSpec
|
||||
Values() []uint8
|
||||
}
|
||||
|
||||
type CronTimeSpec interface {
|
||||
Type() wingmate.CronTimeType
|
||||
Match(uint8) bool
|
||||
//Type() wingmate.CronTimeType
|
||||
//Match(uint8) bool
|
||||
}
|
||||
|
||||
type Cron struct {
|
||||
minute CronTimeSpec
|
||||
hour CronTimeSpec
|
||||
dom CronTimeSpec
|
||||
month CronTimeSpec
|
||||
dow CronTimeSpec
|
||||
command string
|
||||
lastRun time.Time
|
||||
hasRun bool
|
||||
Minute CronTimeSpec
|
||||
Hour CronTimeSpec
|
||||
DoM CronTimeSpec
|
||||
Month CronTimeSpec
|
||||
DoW CronTimeSpec
|
||||
Command string
|
||||
}
|
||||
|
||||
type cronField int
|
||||
|
||||
const (
|
||||
CrontabEntryRegex = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||
CrontabSubmatchLen = 7
|
||||
CrontabEntryRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
|
||||
CrontabCommentLineRegexPattern = `^\s*#.*$`
|
||||
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
|
||||
CrontabSubMatchLen = 7
|
||||
|
||||
minute cronField = iota
|
||||
hour
|
||||
@ -52,21 +40,22 @@ const (
|
||||
dow
|
||||
)
|
||||
|
||||
var (
|
||||
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
|
||||
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
|
||||
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
|
||||
)
|
||||
|
||||
func readCrontab(path string) ([]*Cron, error) {
|
||||
var (
|
||||
file *os.File
|
||||
err error
|
||||
scanner *bufio.Scanner
|
||||
line string
|
||||
re *regexp.Regexp
|
||||
parts []string
|
||||
retval []*Cron
|
||||
)
|
||||
|
||||
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file, err = os.Open(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -79,41 +68,48 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
for scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
|
||||
parts = re.FindStringSubmatch(line)
|
||||
if len(parts) != CrontabSubmatchLen {
|
||||
if crontabCommentLineRegex.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = crontabCommentSuffixRegex.FindStringSubmatch(line)
|
||||
if len(parts) == 2 {
|
||||
line = parts[1]
|
||||
}
|
||||
|
||||
parts = crontabEntryRegex.FindStringSubmatch(line)
|
||||
if len(parts) != CrontabSubMatchLen {
|
||||
wingmate.Log().Error().Msgf("invalid entry %s", line)
|
||||
continue
|
||||
}
|
||||
|
||||
c := &Cron{
|
||||
hasRun: false,
|
||||
}
|
||||
c := &Cron{}
|
||||
if err = c.setField(minute, parts[1]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing minute field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Minute field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(hour, parts[2]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing hour field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Hour field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(dom, parts[3]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing day of month field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Day of Month field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(month, parts[4]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing month field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Month field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.setField(dow, parts[5]); err != nil {
|
||||
wingmate.Log().Error().Msgf("error parsing day of week field %+v", err)
|
||||
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
c.command = parts[6]
|
||||
c.Command = parts[6]
|
||||
|
||||
retval = append(retval, c)
|
||||
}
|
||||
@ -121,35 +117,6 @@ func readCrontab(path string) ([]*Cron, error) {
|
||||
return retval, nil
|
||||
}
|
||||
|
||||
func (c *Cron) Command() string {
|
||||
return c.command
|
||||
}
|
||||
|
||||
func (c *Cron) TimeToRun(now time.Time) bool {
|
||||
if c.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
|
||||
}
|
||||
|
||||
type fieldRange struct {
|
||||
min int
|
||||
max int
|
||||
@ -182,25 +149,25 @@ 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)
|
||||
@ -217,7 +184,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
current += parsed
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
@ -237,7 +204,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
multi = append(multi, parsed)
|
||||
}
|
||||
|
||||
*cField = &specMultiOccurrence{
|
||||
*cField = &SpecMultiOccurrence{
|
||||
values: multi,
|
||||
}
|
||||
} else {
|
||||
@ -250,7 +217,7 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
|
||||
}
|
||||
|
||||
*cField = &specExact{
|
||||
*cField = &SpecExact{
|
||||
value: parsed,
|
||||
}
|
||||
}
|
||||
@ -259,51 +226,21 @@ func (c *Cron) setField(field cronField, input string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type specAny struct{}
|
||||
type SpecAny struct{}
|
||||
|
||||
func (a *specAny) Type() wingmate.CronTimeType {
|
||||
return wingmate.Any
|
||||
}
|
||||
|
||||
func (a *specAny) Match(u uint8) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type specExact struct {
|
||||
type SpecExact struct {
|
||||
value uint8
|
||||
}
|
||||
|
||||
func (e *specExact) Type() wingmate.CronTimeType {
|
||||
return wingmate.Exact
|
||||
}
|
||||
|
||||
func (e *specExact) Match(u uint8) bool {
|
||||
return u == e.value
|
||||
}
|
||||
|
||||
func (e *specExact) Value() uint8 {
|
||||
func (e *SpecExact) Value() uint8 {
|
||||
return e.value
|
||||
}
|
||||
|
||||
type specMultiOccurrence struct {
|
||||
type SpecMultiOccurrence struct {
|
||||
values []uint8
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Type() wingmate.CronTimeType {
|
||||
return wingmate.MultipleOccurrence
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Match(u uint8) bool {
|
||||
for _, v := range m.values {
|
||||
if v == u {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *specMultiOccurrence) Values() []uint8 {
|
||||
func (m *SpecMultiOccurrence) Values() []uint8 {
|
||||
out := make([]uint8, len(m.values))
|
||||
copy(out, m.values)
|
||||
return out
|
||||
|
||||
128
config/crontab_test.go
Normal file
128
config/crontab_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
crontabFileName = "crontab"
|
||||
)
|
||||
|
||||
func TestCrontab(t *testing.T) {
|
||||
type testEntry struct {
|
||||
name string
|
||||
crontab string
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
_ = wingmate.NewLog(os.Stderr)
|
||||
tests := []testEntry{
|
||||
{
|
||||
name: "positive",
|
||||
crontab: crontabTestCase0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with comment",
|
||||
crontab: crontabTestCase1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "various values",
|
||||
crontab: crontabTestCase2,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed to parse",
|
||||
crontab: crontabTestCase3,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup(t)
|
||||
defer tear(t)
|
||||
|
||||
writeCrontab(t, tt.crontab)
|
||||
|
||||
cfg, err := Read()
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Fatalf("wantErr is %v but err is %+v", tt.wantErr, err)
|
||||
}
|
||||
|
||||
t.Logf("cfg: %+v", cfg)
|
||||
for _, c := range cfg.Cron {
|
||||
t.Logf("%+v", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeCrontab(t *testing.T, content string) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
if f, err = os.Create(filepath.Join(configDir, crontabFileName)); err != nil {
|
||||
t.Fatal("create crontab file", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
if _, err = f.Write([]byte(content)); err != nil {
|
||||
t.Fatal("writing crontab file", err)
|
||||
}
|
||||
}
|
||||
|
||||
const crontabTestCase0 = `* * * * * /path/to/executable`
|
||||
const crontabTestCase1 = `# this is a comment
|
||||
## comment with space
|
||||
* * * * * /path/to/executable
|
||||
* * * * * /path/to/executable # comment as a suffix
|
||||
`
|
||||
|
||||
const crontabTestCase2 = `# first comment
|
||||
*/5 13 3,5,7 * * /path/to/executable`
|
||||
|
||||
const crontabTestCase3 = `a 13 3,5,7 * * /path/to/executable
|
||||
*/5 a 3,5,7 * * /path/to/executable
|
||||
*/5 13 a * * /path/to/executable
|
||||
*/5 13 3,5,7 a * /path/to/executable
|
||||
*/5 13 3,5,7 * a /path/to/executable
|
||||
*/x 13 3,5,7 * a /path/to/executable
|
||||
76 13 3,5,7 * a /path/to/executable
|
||||
*/75 13 3,5,7 * a /path/to/executable
|
||||
*/5 13 3,x,7 * a /path/to/executable
|
||||
*/5 13 3,5,67 * a /path/to/executable
|
||||
*/5 13 * * /path/to/executable
|
||||
*/5 13 3,5,7 * * /path/to/executable`
|
||||
|
||||
func TestSpecExact(t *testing.T) {
|
||||
var val uint8 = 45
|
||||
s := SpecExact{
|
||||
value: val,
|
||||
}
|
||||
|
||||
assert.Equal(t, val, s.Value())
|
||||
}
|
||||
|
||||
func TestSpecMulti(t *testing.T) {
|
||||
val := []uint8{3, 5, 7, 15}
|
||||
s := SpecMultiOccurrence{
|
||||
values: val,
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, val, s.Values())
|
||||
}
|
||||
|
||||
func TestInvalidField(t *testing.T) {
|
||||
c := &Cron{}
|
||||
assert.NotNil(t, c.setField(cronField(99), "x"))
|
||||
}
|
||||
23
init/cron.go
23
init/cron.go
@ -13,7 +13,7 @@ const (
|
||||
cronTag = "cron"
|
||||
)
|
||||
|
||||
func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
|
||||
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
@ -21,35 +21,40 @@ func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
|
||||
err error
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
cmd *exec.Cmd
|
||||
)
|
||||
|
||||
ticker := time.NewTicker(time.Second * 30)
|
||||
cron:
|
||||
for {
|
||||
if cron.TimeToRun(time.Now()) {
|
||||
wingmate.Log().Info().Str(cronTag, cron.Command().Path()).Msg("executing")
|
||||
cmd := exec.Command(cron.Command().Path())
|
||||
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing")
|
||||
if len(cron.Command()) == 1 {
|
||||
cmd = exec.Command(cron.Command()[0])
|
||||
} else {
|
||||
cmd = exec.Command(cron.Command()[0], cron.Command()[1:]...)
|
||||
}
|
||||
iwg = &sync.WaitGroup{}
|
||||
|
||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(cronTag, cron.Command().Path()).Msgf("stdout pipe: %+v", err)
|
||||
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.Command().Path()).Msgf("stderr pipe: %+v", err)
|
||||
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.Command().Path())
|
||||
go i.pipeReader(iwg, stdout, cronTag, cron.Name())
|
||||
|
||||
iwg.Add(1)
|
||||
go i.pipeReader(iwg, stderr, cronTag, cron.Command().Path())
|
||||
go i.pipeReader(iwg, stderr, cronTag, cron.Name())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Command().Path(), err)
|
||||
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Name(), err)
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
iwg.Wait()
|
||||
@ -59,7 +64,7 @@ cron:
|
||||
iwg.Wait()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
wingmate.Log().Error().Str(cronTag, cron.Command().Path()).Msgf("got error when waiting: %+v", err)
|
||||
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("got error when waiting: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
init/init.go
35
init/init.go
@ -6,18 +6,37 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Path interface {
|
||||
Path() string
|
||||
type Tasks interface {
|
||||
List() []Task
|
||||
Services() []Task
|
||||
Crones() []CronTask
|
||||
Get(string) (Task, error)
|
||||
}
|
||||
|
||||
type Cron interface {
|
||||
Command() Path
|
||||
type UserGroup interface {
|
||||
}
|
||||
|
||||
type TaskStatus interface {
|
||||
}
|
||||
|
||||
type Task interface {
|
||||
Name() string
|
||||
Command() []string
|
||||
Environ() []string
|
||||
Setsid() bool
|
||||
UserGroup() UserGroup
|
||||
Background() bool
|
||||
WorkingDir() string
|
||||
Status() TaskStatus
|
||||
}
|
||||
|
||||
type CronTask interface {
|
||||
Task
|
||||
TimeToRun(time.Time) bool
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
Services() []Path
|
||||
Cron() []Cron
|
||||
Tasks() Tasks
|
||||
}
|
||||
|
||||
type Init struct {
|
||||
@ -49,12 +68,12 @@ func (i *Init) Start() {
|
||||
wg.Add(1)
|
||||
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
|
||||
|
||||
for _, s := range i.config.Services() {
|
||||
for _, s := range i.config.Tasks().Services() {
|
||||
wg.Add(1)
|
||||
go i.service(wg, s, signalTrigger)
|
||||
}
|
||||
|
||||
for _, c := range i.config.Cron() {
|
||||
for _, c := range i.config.Tasks().Crones() {
|
||||
wg.Add(1)
|
||||
go i.cron(wg, c, signalTrigger)
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ const (
|
||||
serviceTag = "service"
|
||||
)
|
||||
|
||||
func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
|
||||
func (i *Init) service(wg *sync.WaitGroup, task Task, exitFlag <-chan any) {
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
@ -23,37 +23,42 @@ func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
|
||||
stderr io.ReadCloser
|
||||
stdout io.ReadCloser
|
||||
failStatus bool
|
||||
cmd *exec.Cmd
|
||||
)
|
||||
|
||||
defer func() {
|
||||
wingmate.Log().Info().Str(serviceTag, path.Path()).Msg("stopped")
|
||||
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
|
||||
}()
|
||||
|
||||
service:
|
||||
for {
|
||||
failStatus = false
|
||||
cmd := exec.Command(path.Path())
|
||||
if len(task.Command()) == 1 {
|
||||
cmd = exec.Command(task.Command()[0])
|
||||
} else {
|
||||
cmd = exec.Command(task.Command()[0], task.Command()[1:]...)
|
||||
}
|
||||
iwg = &sync.WaitGroup{}
|
||||
|
||||
if stdout, err = cmd.StdoutPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("stdout pipe: %#v", err)
|
||||
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, path.Path())
|
||||
go i.pipeReader(iwg, stdout, serviceTag, task.Name())
|
||||
|
||||
if stderr, err = cmd.StderrPipe(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("stderr pipe: %#v", err)
|
||||
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, path.Path())
|
||||
go i.pipeReader(iwg, stderr, serviceTag, task.Name())
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting service %s error %#v", path.Path(), err)
|
||||
wingmate.Log().Error().Msgf("starting service %s error %#v", task.Name(), err)
|
||||
failStatus = true
|
||||
_ = stdout.Close()
|
||||
_ = stderr.Close()
|
||||
@ -64,7 +69,7 @@ service:
|
||||
iwg.Wait()
|
||||
|
||||
if err = cmd.Wait(); err != nil {
|
||||
wingmate.Log().Error().Str(serviceTag, path.Path()).Msgf("got error when waiting: %+v", err)
|
||||
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("got error when waiting: %+v", err)
|
||||
}
|
||||
fail:
|
||||
if failStatus {
|
||||
|
||||
139
task/cron.go
Normal file
139
task/cron.go
Normal file
@ -0,0 +1,139 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CronSchedule struct {
|
||||
Minute CronTimeSpec
|
||||
Hour CronTimeSpec
|
||||
DoM CronTimeSpec
|
||||
Month CronTimeSpec
|
||||
DoW CronTimeSpec
|
||||
}
|
||||
|
||||
type CronTimeSpec interface {
|
||||
Match(uint8) bool
|
||||
}
|
||||
|
||||
type CronAnySpec struct {
|
||||
}
|
||||
|
||||
func NewCronAnySpec() *CronAnySpec {
|
||||
return &CronAnySpec{}
|
||||
}
|
||||
|
||||
func (cas *CronAnySpec) Match(u uint8) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type CronExactSpec struct {
|
||||
value uint8
|
||||
}
|
||||
|
||||
func NewCronExactSpec(v uint8) *CronExactSpec {
|
||||
return &CronExactSpec{
|
||||
value: v,
|
||||
}
|
||||
}
|
||||
|
||||
func (ces *CronExactSpec) Match(u uint8) bool {
|
||||
return u == ces.value
|
||||
}
|
||||
|
||||
type CronMultiOccurrenceSpec struct {
|
||||
values []uint8
|
||||
}
|
||||
|
||||
func NewCronMultiOccurrenceSpec(v ...uint8) *CronMultiOccurrenceSpec {
|
||||
retval := &CronMultiOccurrenceSpec{}
|
||||
if len(v) > 0 {
|
||||
retval.values = make([]uint8, len(v))
|
||||
copy(retval.values, v)
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
func (cms *CronMultiOccurrenceSpec) Match(u uint8) bool {
|
||||
for _, v := range cms.values {
|
||||
if v == u {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type Cron struct {
|
||||
CronSchedule
|
||||
name string
|
||||
command []string
|
||||
lastRun time.Time
|
||||
hasRun bool //NOTE: make sure initialised as false
|
||||
}
|
||||
|
||||
func (c *Cron) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *Cron) Command() []string {
|
||||
retval := make([]string, len(c.command))
|
||||
copy(retval, c.command)
|
||||
return retval
|
||||
}
|
||||
|
||||
func (c *Cron) Environ() []string {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cron) Setsid() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Cron) UserGroup() wminit.UserGroup {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cron) Background() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Cron) WorkingDir() string {
|
||||
panic("not implemented")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Cron) Status() wminit.TaskStatus {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cron) TimeToRun(now time.Time) bool {
|
||||
if c.Minute.Match(uint8(now.Minute())) &&
|
||||
c.Hour.Match(uint8(now.Hour())) &&
|
||||
c.DoM.Match(uint8(now.Day())) &&
|
||||
c.Month.Match(uint8(now.Month())) &&
|
||||
c.DoW.Match(uint8(now.Weekday())) {
|
||||
|
||||
if c.hasRun {
|
||||
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
|
||||
return false
|
||||
} else {
|
||||
c.lastRun = now
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
c.lastRun = now
|
||||
c.hasRun = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
99
task/task.go
Normal file
99
task/task.go
Normal file
@ -0,0 +1,99 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
wminit "gitea.suyono.dev/suyono/wingmate/init"
|
||||
)
|
||||
|
||||
type Tasks struct {
|
||||
//services []wminit.Path
|
||||
//cron []wminit.Cron
|
||||
services []wminit.Task
|
||||
crones []wminit.CronTask
|
||||
}
|
||||
|
||||
func NewTasks() *Tasks {
|
||||
return &Tasks{
|
||||
services: make([]wminit.Task, 0),
|
||||
crones: make([]wminit.CronTask, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *Tasks) AddV0Service(path string) {
|
||||
ts.services = append(ts.services, &Task{
|
||||
name: path,
|
||||
command: []string{path},
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) {
|
||||
ts.crones = append(ts.crones, &Cron{
|
||||
CronSchedule: schedule,
|
||||
name: path,
|
||||
command: []string{path},
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *Tasks) List() []wminit.Task {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *Tasks) Services() []wminit.Task {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *Tasks) Crones() []wminit.CronTask {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *Tasks) Get(name string) (wminit.Task, error) {
|
||||
panic("not implemented")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
name string
|
||||
command []string
|
||||
}
|
||||
|
||||
func (t *Task) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func (t *Task) Command() []string {
|
||||
retval := make([]string, len(t.command))
|
||||
copy(retval, t.command)
|
||||
return retval
|
||||
}
|
||||
|
||||
func (t *Task) Environ() []string {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) Setsid() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Task) UserGroup() wminit.UserGroup {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) Background() bool {
|
||||
panic("not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Task) WorkingDir() string {
|
||||
panic("not implemented")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *Task) Status() wminit.TaskStatus {
|
||||
panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user