revamp: start simple
This commit is contained in:
parent
4f54db3cbd
commit
dbb703db61
4
.devcontainer/devcontainer.json
Normal file
4
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Golang Dev",
|
||||
"image": "golang:1.21-bookworm"
|
||||
}
|
||||
34
cmd/cmd.go
34
cmd/cmd.go
@ -1,34 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
initpkg "gitea.suyono.dev/suyono/wingmate/cmd/init"
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "wingmate",
|
||||
Short: "your service companion",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
config.BindFlags(rootCmd)
|
||||
|
||||
rootCmd.AddCommand(
|
||||
initpkg.Command(),
|
||||
)
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
if os.Args[0] != rootCmd.Use {
|
||||
rootCmd.SetArgs(os.Args)
|
||||
} else {
|
||||
rootCmd.SetArgs(os.Args[1:])
|
||||
}
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"gitea.suyono.dev/suyono/wingmate/daemon"
|
||||
"gitea.suyono.dev/suyono/wingmate/middleware"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Command() *cobra.Command {
|
||||
init := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "dummy init",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return middleware.CallChain(cmd, args, config.Read, daemon.Start)
|
||||
},
|
||||
}
|
||||
|
||||
return init
|
||||
}
|
||||
5
cmd/wingmate/wingmate.go
Normal file
5
cmd/wingmate/wingmate.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
||||
@ -1,84 +1 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvPrefix = "wingmate"
|
||||
PathKey = "config_path"
|
||||
SearchPathKey = "config_search_path"
|
||||
|
||||
defaultPath = "/etc/wingmate/"
|
||||
defaultName = "config"
|
||||
|
||||
ServiceKey = "service"
|
||||
CronKey = "cron"
|
||||
CommandKey = "command"
|
||||
)
|
||||
|
||||
var (
|
||||
configPath string
|
||||
configSearchPath []string
|
||||
configRead bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
viper.SetEnvPrefix(EnvPrefix)
|
||||
_ = viper.BindEnv(PathKey)
|
||||
_ = viper.BindEnv(SearchPathKey)
|
||||
}
|
||||
|
||||
func BindFlags(command *cobra.Command) {
|
||||
command.PersistentFlags().StringVarP(&configPath, "config", "c", defaultPath+defaultName+".yml", "configuration path")
|
||||
command.PersistentFlags().StringSliceVar(&configSearchPath, "config-dir", []string{}, "configuration search path")
|
||||
}
|
||||
|
||||
func Read(cmd *cobra.Command, args []string) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
_, _ = cmd, args // prevent warning for unused arguments
|
||||
|
||||
if viper.IsSet(PathKey) {
|
||||
configPath = viper.GetString(PathKey)
|
||||
}
|
||||
|
||||
if viper.IsSet(SearchPathKey) {
|
||||
if err = viper.UnmarshalKey(SearchPathKey, &configSearchPath); err != nil {
|
||||
return fmt.Errorf("reading %s: %w %w", SearchPathKey, err, debugframes.GetTraces())
|
||||
}
|
||||
}
|
||||
|
||||
if configRead {
|
||||
return nil
|
||||
}
|
||||
|
||||
viper.SetConfigType("yaml")
|
||||
if len(configSearchPath) > 0 {
|
||||
name := path.Base(configPath)
|
||||
dir := path.Dir(configPath)
|
||||
if dir != "." {
|
||||
configSearchPath = append([]string{dir}, configSearchPath...)
|
||||
}
|
||||
|
||||
viper.SetConfigName(name)
|
||||
for _, p := range configSearchPath {
|
||||
viper.AddConfigPath(p)
|
||||
}
|
||||
} else {
|
||||
viper.SetConfigFile(configPath)
|
||||
}
|
||||
|
||||
if err = viper.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("reading config: %w %w", err, debugframes.GetTraces())
|
||||
}
|
||||
configRead = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,191 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testReadArgs struct {
|
||||
cmd *cobra.Command
|
||||
args []string
|
||||
}
|
||||
|
||||
type testRead struct {
|
||||
name string
|
||||
env map[string]string
|
||||
args testReadArgs
|
||||
wantErr bool
|
||||
pre func(t *testing.T, tc *testRead)
|
||||
post func(t *testing.T, tc *testRead)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
type args struct {
|
||||
cmd *cobra.Command
|
||||
args []string
|
||||
}
|
||||
tests := []testRead{
|
||||
{
|
||||
name: "env",
|
||||
env: map[string]string{
|
||||
strings.ToUpper(EnvPrefix + "_" + PathKey): "/path/to/config",
|
||||
strings.ToUpper(EnvPrefix + "_" + SearchPathKey): "/path/one,/path/two",
|
||||
},
|
||||
args: testReadArgs{
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "env exist",
|
||||
args: testReadArgs{
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
wantErr: false,
|
||||
pre: func(t *testing.T, tc *testRead) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
fname string
|
||||
)
|
||||
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
|
||||
t.Fatal("create temp:", err)
|
||||
}
|
||||
fname = f.Name()
|
||||
if _, err = f.WriteString(testconfig.One); err != nil {
|
||||
t.Fatal("writing temp:", err)
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
t.Fatal("closing temp:", err)
|
||||
}
|
||||
|
||||
tc.env = map[string]string{
|
||||
strings.ToUpper(EnvPrefix + "_" + PathKey): fname,
|
||||
}
|
||||
|
||||
tc.post = func(t *testing.T, tc *testRead) {
|
||||
_ = os.Remove(fname)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
if tt.pre != nil {
|
||||
tt.pre(t, &tt)
|
||||
}
|
||||
|
||||
if tt.post != nil {
|
||||
defer tt.post(t, &tt)
|
||||
}
|
||||
|
||||
for k, v := range tt.env {
|
||||
if err = os.Setenv(k, v); err != nil {
|
||||
t.Fatal("failed", err)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
for k := range tt.env {
|
||||
if err = os.Unsetenv(k); err != nil {
|
||||
t.Fatal("failed", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = Read(tt.args.cmd, tt.args.args); (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Read() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
t.Log("case:", tt.name, "; expected error:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
type args struct {
|
||||
cmd *cobra.Command
|
||||
args []string
|
||||
}
|
||||
tests := []testRead{
|
||||
{
|
||||
name: "env exist",
|
||||
args: testReadArgs{
|
||||
nil,
|
||||
[]string{},
|
||||
},
|
||||
wantErr: false,
|
||||
pre: func(t *testing.T, tc *testRead) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
fname string
|
||||
)
|
||||
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
|
||||
t.Fatal("create temp:", err)
|
||||
}
|
||||
fname = f.Name()
|
||||
if _, err = f.WriteString(testconfig.One); err != nil {
|
||||
t.Fatal("writing temp:", err)
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
t.Fatal("closing temp:", err)
|
||||
}
|
||||
|
||||
tc.env = map[string]string{
|
||||
strings.ToUpper(EnvPrefix + "_" + PathKey): fname,
|
||||
}
|
||||
|
||||
tc.post = func(t *testing.T, tc *testRead) {
|
||||
_ = os.Remove(fname)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
if tt.pre != nil {
|
||||
tt.pre(t, &tt)
|
||||
}
|
||||
|
||||
if tt.post != nil {
|
||||
defer tt.post(t, &tt)
|
||||
}
|
||||
|
||||
for k, v := range tt.env {
|
||||
if err = os.Setenv(k, v); err != nil {
|
||||
t.Fatal("failed", err)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
for k := range tt.env {
|
||||
if err = os.Unsetenv(k); err != nil {
|
||||
t.Fatal("failed", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = Read(tt.args.cmd, tt.args.args); err != nil {
|
||||
t.Fatal("fail to read config:", err)
|
||||
}
|
||||
|
||||
t.Log(viper.AllKeys())
|
||||
m := viper.GetStringMap(ServiceKey)
|
||||
for s := range m {
|
||||
t.Log(s)
|
||||
t.Log(viper.GetStringMap(ServiceKey + "." + s))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
109
daemon/daemon.go
109
daemon/daemon.go
@ -1,109 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os/exec"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
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
|
||||
services map[string]*service
|
||||
cron map[string]WingMateDescriptor
|
||||
logChannel chan<- any
|
||||
errorChannel chan<- error
|
||||
}
|
||||
|
||||
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) (err error) {
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("panic: %v %w and error: %w", o, debugframes.PanicTrace(debug.Stack()), err)
|
||||
} else {
|
||||
err = fmt.Errorf("panic: %v %w", o, debugframes.PanicTrace(debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
_, _ = cmd, args // prevent warnings for unused arguments
|
||||
|
||||
_, err = start()
|
||||
return
|
||||
}
|
||||
|
||||
func start() (*daemon, error) {
|
||||
var (
|
||||
err error
|
||||
k string
|
||||
svc *service
|
||||
cmd *exec.Cmd
|
||||
)
|
||||
|
||||
d := &daemon{
|
||||
running: make(map[string]process),
|
||||
services: make(map[string]*service),
|
||||
cron: make(map[string]WingMateDescriptor),
|
||||
logChannel: make(chan<- any, 16),
|
||||
errorChannel: make(chan<- error, 4),
|
||||
}
|
||||
if err = d.buildServiceMap(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, svc = range d.services {
|
||||
if cmd, err = StartProcess(svc); err != nil {
|
||||
return nil, err //TODO: this is not supposed to return, log and start the next service
|
||||
}
|
||||
d.running[k] = process{
|
||||
cmd: cmd,
|
||||
descriptor: svc,
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: create loop to receive and process log
|
||||
//TODO: create loop to receive error
|
||||
//TODO: create signal handler
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *daemon) buildServiceMap() error {
|
||||
for s := range viper.GetStringMap(Service.String()) {
|
||||
d.services[s] = newService(s, d.errorChannel, d.logChannel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
type ProcessConfig interface {
|
||||
Descriptor() WingMateDescriptor
|
||||
|
||||
// ExecutableName returns the executable/binary name to be executed, to be passed in to exec.Command
|
||||
ExecutableName() string
|
||||
|
||||
// Args returns the arguments for the process, to be passed in to exec.Command
|
||||
Args() []string
|
||||
Env() map[string]string
|
||||
WorkingDir() string
|
||||
LogChannel() chan<- any
|
||||
ErrorChannel() chan<- error
|
||||
}
|
||||
|
||||
type StreamID int
|
||||
|
||||
type ProcessLogEntry struct {
|
||||
Descriptor 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.ExecutableName(), 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.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())
|
||||
}
|
||||
|
||||
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()
|
||||
eChan := config.ErrorChannel()
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
eChan <- fmt.Errorf("log reader %s of %w panic: %v %w",
|
||||
id.String(), config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
|
||||
} else {
|
||||
eChan <- fmt.Errorf("log reader %s of %w returned", id.String(), config.Descriptor())
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stream)
|
||||
for scanner.Scan() {
|
||||
out <- ProcessLogEntry{
|
||||
Descriptor: config.Descriptor(),
|
||||
LogEntry: scanner.Text(),
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
eChan <- fmt.Errorf("read %s from %w: %w %w",
|
||||
id.String(), config.Descriptor(), err, debugframes.GetTraces())
|
||||
}
|
||||
}
|
||||
|
||||
func serviceWaiter(config ProcessConfig, cmd *exec.Cmd) {
|
||||
eChan := config.ErrorChannel()
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
eChan <- fmt.Errorf("%w panic: %v %w", config.Descriptor(), o, debugframes.PanicTrace(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
eChan <- fmt.Errorf("wait error on service %w: %w", config.Descriptor(), err)
|
||||
} else {
|
||||
eChan <- config.Descriptor()
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
anyChildProcess = -1
|
||||
)
|
||||
|
||||
func WaitChildProcess() (int, error) {
|
||||
var ws unix.WaitStatus
|
||||
return unix.Wait4(anyChildProcess, &ws, 0, nil)
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
var (
|
||||
err error
|
||||
pid int
|
||||
testpid int
|
||||
)
|
||||
|
||||
cmd := exec.Command("sleep", "10")
|
||||
if err = cmd.Start(); err != nil {
|
||||
t.Fatal("failed to start command:", err)
|
||||
}
|
||||
|
||||
pid = cmd.Process.Pid
|
||||
t.Log("started pid:", pid)
|
||||
go func() {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
p, terr := os.FindProcess(pid)
|
||||
if terr == nil {
|
||||
if terr = p.Signal(unix.SIGTERM); terr != nil {
|
||||
t.Log("sending signal failed:", terr)
|
||||
}
|
||||
} else {
|
||||
t.Logf("find process %d: %v", pid, terr)
|
||||
}
|
||||
}()
|
||||
|
||||
testpid, err = WaitChildProcess()
|
||||
if err != nil {
|
||||
t.Fatal("wait4 error:", err)
|
||||
}
|
||||
|
||||
//if err = cmd.Wait(); err != nil {
|
||||
// t.Fatal("is this expected?", err)
|
||||
//}
|
||||
|
||||
assert.Equal(t, pid, testpid)
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
name string
|
||||
stdoutLogWriter io.WriteCloser
|
||||
stderrLogWriter io.WriteCloser
|
||||
logChannel chan<- any
|
||||
errChannel chan<- error
|
||||
}
|
||||
|
||||
func newService(name string, error chan<- error, logging chan<- any) *service {
|
||||
return &service{
|
||||
name: name,
|
||||
// stdoutLogWriter: log.NewLogFile(fmt.Sprintf("%s.%s.%s", Service.String(), name, config.CommandKey)),
|
||||
logChannel: logging,
|
||||
errChannel: error,
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: review back the implementation of service as WingMateDescriptor
|
||||
|
||||
func (s *service) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *service) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *service) InstanceName() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *service) InstanceType() InstanceType {
|
||||
return Service
|
||||
}
|
||||
|
||||
func (s *service) Error() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *service) parseCommand() []string {
|
||||
key := fmt.Sprintf("%s.%s.%s", Service.String(), s.name, config.CommandKey)
|
||||
if !viper.IsSet(key) {
|
||||
panic(fmt.Errorf("parse command: key %s is not set", key))
|
||||
}
|
||||
result := viper.GetStringSlice(key)
|
||||
if len(result) < 1 {
|
||||
panic(fmt.Errorf("parse command: zero command length"))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *service) ExecutableName() string {
|
||||
return s.parseCommand()[0]
|
||||
}
|
||||
|
||||
func (s *service) Args() []string {
|
||||
return s.parseCommand()[1:]
|
||||
}
|
||||
|
||||
func (s *service) Descriptor() WingMateDescriptor {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *service) Env() map[string]string {
|
||||
//TODO: stub implementation; FIX!
|
||||
return make(map[string]string)
|
||||
}
|
||||
|
||||
func (s *service) WorkingDir() string {
|
||||
//TODO: stub implementation; FIX!
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *service) LogChannel() chan<- any {
|
||||
return s.logChannel
|
||||
}
|
||||
|
||||
func (s *service) ErrorChannel() chan<- error {
|
||||
return s.errChannel
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/config"
|
||||
"gitea.suyono.dev/suyono/wingmate/files/testconfig"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type parseCommandTestCase struct {
|
||||
name string
|
||||
pre func(t *testing.T, tt *parseCommandTestCase)
|
||||
post func(t *testing.T, tt *parseCommandTestCase)
|
||||
}
|
||||
|
||||
func TestParseCommand(t *testing.T) {
|
||||
tests := []parseCommandTestCase{
|
||||
{
|
||||
name: "initial",
|
||||
pre: func(t *testing.T, tt *parseCommandTestCase) {
|
||||
var (
|
||||
f *os.File
|
||||
err error
|
||||
fname, key string
|
||||
)
|
||||
if f, err = os.CreateTemp("", "config-*.yml"); err != nil {
|
||||
t.Fatal("create temp:", err)
|
||||
}
|
||||
fname = f.Name()
|
||||
if _, err = f.WriteString(testconfig.One); err != nil {
|
||||
t.Fatal("writing temp:", err)
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
t.Fatal("closing temp:", err)
|
||||
}
|
||||
|
||||
key = strings.ToUpper(config.EnvPrefix + "_" + config.PathKey)
|
||||
if err = os.Setenv(key, fname); err != nil {
|
||||
t.Fatal("set up env failed", err)
|
||||
}
|
||||
|
||||
tt.post = func(t *testing.T, tt *parseCommandTestCase) {
|
||||
_ = os.Unsetenv(key)
|
||||
_ = os.Remove(fname)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var (
|
||||
err error
|
||||
d *daemon
|
||||
s *service
|
||||
)
|
||||
|
||||
if tt.pre != nil {
|
||||
tt.pre(t, &tt)
|
||||
}
|
||||
if tt.post != nil {
|
||||
defer tt.post(t, &tt)
|
||||
}
|
||||
|
||||
if err = config.Read(nil, []string{}); err != nil {
|
||||
t.Fatal("reading config", err)
|
||||
}
|
||||
|
||||
if d, err = start(); err != nil {
|
||||
t.Fatal("starting daemon", err)
|
||||
}
|
||||
|
||||
for _, s = range d.services {
|
||||
s.parseCommand()
|
||||
}
|
||||
|
||||
x := viper.GetStringSlice("non.existent")
|
||||
t.Log(x)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package debugframes
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/log"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Trace struct {
|
||||
Frames []runtime.Frame
|
||||
}
|
||||
|
||||
func (t *Trace) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *Trace) AppendFields(h log.FieldsHandler) log.FieldsHandler {
|
||||
return h.HandleFields("trace", t.Frames)
|
||||
}
|
||||
|
||||
func GetTraces() error {
|
||||
trace := new(Trace)
|
||||
pc := make([]uintptr, 16)
|
||||
|
||||
if n := runtime.Callers(2, pc); n > 0 {
|
||||
frames := runtime.CallersFrames(pc[:n])
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
trace.Frames = append(trace.Frames, frame)
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return trace
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package debugframes
|
||||
|
||||
import "gitea.suyono.dev/suyono/wingmate/log"
|
||||
|
||||
type PanicTrace []byte
|
||||
|
||||
func (p PanicTrace) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p PanicTrace) AppendFields(h log.FieldsHandler) log.FieldsHandler {
|
||||
return h.HandleFields("panic_stacktrace", p.String())
|
||||
}
|
||||
|
||||
func (p PanicTrace) String() string {
|
||||
b := []byte(p)
|
||||
return string(b)
|
||||
}
|
||||
7
init/cron.go
Normal file
7
init/cron.go
Normal file
@ -0,0 +1,7 @@
|
||||
package init
|
||||
|
||||
import "sync"
|
||||
|
||||
func (i *Init) cron(wg *sync.WaitGroup, cron Cron) {
|
||||
defer wg.Done()
|
||||
}
|
||||
80
init/init.go
Normal file
80
init/init.go
Normal file
@ -0,0 +1,80 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Path interface {
|
||||
Path() string
|
||||
}
|
||||
|
||||
type CronExactSpec interface {
|
||||
CronTimeSpec
|
||||
Value() uint8
|
||||
}
|
||||
|
||||
type CronMultipleOccurrenceSpec interface {
|
||||
CronTimeSpec
|
||||
MultipleValues() []uint8
|
||||
}
|
||||
|
||||
type CronTimeSpec interface {
|
||||
Type() wingmate.CronTimeType
|
||||
}
|
||||
|
||||
type Cron interface {
|
||||
Minute() CronTimeSpec
|
||||
Hour() CronTimeSpec
|
||||
DayOfMonth() CronTimeSpec
|
||||
Month() CronTimeSpec
|
||||
DayOfWeek() CronTimeSpec
|
||||
Command() Path
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
Services() []Path
|
||||
Cron() []Cron
|
||||
}
|
||||
|
||||
type Init struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
func NewInit(config Config) *Init {
|
||||
return &Init{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Init) Start() {
|
||||
var (
|
||||
wg *sync.WaitGroup
|
||||
signalTrigger chan any
|
||||
sighandlerExit chan any
|
||||
)
|
||||
|
||||
signalTrigger = make(chan any)
|
||||
sighandlerExit = make(chan any)
|
||||
|
||||
wg = &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go i.sighandler(wg, signalTrigger, sighandlerExit)
|
||||
|
||||
for _, s := range i.config.Services() {
|
||||
wg.Add(1)
|
||||
go func(p Path) {
|
||||
for {
|
||||
if err := i.service(wg, p); err != nil {
|
||||
wingmate.Log().Error().Msgf("starting service %s error %#v", p.Path(), err)
|
||||
}
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
for _, c := range i.config.Cron() {
|
||||
wg.Add(1)
|
||||
go i.cron(wg, c)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
21
init/service.go
Normal file
21
init/service.go
Normal file
@ -0,0 +1,21 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (i *Init) service(wg *sync.WaitGroup, path Path) error {
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
cmd := exec.Command(path.Path())
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
31
init/sighandler.go
Normal file
31
init/sighandler.go
Normal file
@ -0,0 +1,31 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
|
||||
defer wg.Wait()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, unix.SIGINT, unix.SIGTERM, unix.SIGCHLD)
|
||||
|
||||
signal:
|
||||
for {
|
||||
select {
|
||||
case s := <-c:
|
||||
switch s {
|
||||
case unix.SIGTERM, unix.SIGINT:
|
||||
close(trigger)
|
||||
case unix.SIGCHLD:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
case <-selfExit:
|
||||
break signal
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/rs/zerolog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrint(t *testing.T) {
|
||||
Print("key", errors.New("hello world"))
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
}
|
||||
|
||||
func (t testWriter) Write(b []byte) (int, error) {
|
||||
return 0, errors.New("write error")
|
||||
}
|
||||
|
||||
func TestErrWrite(t *testing.T) {
|
||||
var (
|
||||
tw testWriter
|
||||
logger zerolog.Logger
|
||||
)
|
||||
logger = zerolog.New(tw)
|
||||
logger.Error().Str("test", "one").Msg("hello")
|
||||
}
|
||||
52
log/file.go
52
log/file.go
@ -1,52 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type File os.File
|
||||
|
||||
const (
|
||||
filenameKey = "file"
|
||||
)
|
||||
|
||||
func NewLogFile(configKey string) (*File, error) {
|
||||
var (
|
||||
err error
|
||||
path string
|
||||
file *os.File
|
||||
)
|
||||
if viper.IsSet(configKey) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fileKey := fmt.Sprintf("%s.%s", configKey, filenameKey)
|
||||
if !viper.IsSet(fileKey) {
|
||||
return nil, fmt.Errorf("missing file key config %s", fileKey)
|
||||
}
|
||||
|
||||
path = viper.GetString(fileKey)
|
||||
if file, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*File)(file), nil
|
||||
}
|
||||
|
||||
func (f *File) Write(b []byte) (int, error) {
|
||||
file := (*os.File)(f)
|
||||
return file.Write(b)
|
||||
}
|
||||
|
||||
func (f *File) Close() error {
|
||||
of := (*os.File)(f)
|
||||
return of.Close()
|
||||
}
|
||||
|
||||
func (f *File) Rotate() error {
|
||||
//TODO: implement log rotation
|
||||
return nil
|
||||
}
|
||||
306
log/log.go
306
log/log.go
@ -1,306 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Level zerolog.Level
|
||||
|
||||
type Payload zerolog.Event
|
||||
|
||||
type ProcessLogConfig interface {
|
||||
Method() string
|
||||
FileName() string
|
||||
}
|
||||
|
||||
type Traceable interface {
|
||||
error
|
||||
Trace() []string
|
||||
}
|
||||
|
||||
type FieldsHandler interface {
|
||||
HandleFields(a ...any) FieldsHandler
|
||||
}
|
||||
|
||||
type ErrorLogFields interface {
|
||||
error
|
||||
AppendFields(fh FieldsHandler) FieldsHandler
|
||||
}
|
||||
|
||||
const (
|
||||
Trace Level = Level(zerolog.TraceLevel)
|
||||
Debug Level = Level(zerolog.DebugLevel)
|
||||
Info Level = Level(zerolog.InfoLevel)
|
||||
Warn Level = Level(zerolog.WarnLevel)
|
||||
Error Level = Level(zerolog.ErrorLevel)
|
||||
Fatal Level = Level(zerolog.FatalLevel)
|
||||
Panic Level = Level(zerolog.PanicLevel)
|
||||
|
||||
LogKey = "log"
|
||||
)
|
||||
|
||||
var (
|
||||
globalLevel Level = Error
|
||||
logger zerolog.Logger
|
||||
onLogWriterError zerolog.Logger
|
||||
writer *Writer
|
||||
DefaultOutlet io.Writer
|
||||
errType reflect.Type
|
||||
errInvalidType error = errors.New("invalid type")
|
||||
errNoValidField error = errors.New("no valid field")
|
||||
)
|
||||
|
||||
func init() {
|
||||
errType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
DefaultOutlet = os.Stderr
|
||||
writer = NewWriter()
|
||||
logger = zerolog.New(writer)
|
||||
onLogWriterError = logger.Output(DefaultOutlet)
|
||||
SetGlobalLevel(globalLevel)
|
||||
}
|
||||
|
||||
func (l Level) Print(a ...any) {
|
||||
l.Fields(a...).Send()
|
||||
}
|
||||
|
||||
func (l Level) PrintTo(outlets []io.Writer, a ...any) {
|
||||
l.Fields(a...).Send(outlets...)
|
||||
}
|
||||
|
||||
func (l Level) Fields(a ...any) (p *Payload) {
|
||||
return payload(logger.WithLevel(zerolog.Level(l)).Timestamp()).Fields(a...)
|
||||
}
|
||||
|
||||
func (p *Payload) HandleFields(a ...any) FieldsHandler {
|
||||
return p.Fields(a...)
|
||||
}
|
||||
|
||||
func (p *Payload) Fields(a ...any) (result *Payload) {
|
||||
var (
|
||||
event *zerolog.Event
|
||||
sub *zerolog.Event
|
||||
key string
|
||||
val reflect.Value
|
||||
typ reflect.Type
|
||||
arr *zerolog.Array
|
||||
err error
|
||||
errVal error
|
||||
elf ErrorLogFields
|
||||
)
|
||||
|
||||
event = p.event()
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
// in case there is mishandling reflect
|
||||
result = payload(event.Str("logger_panic", string(debug.Stack())))
|
||||
}
|
||||
}()
|
||||
enumerateFields:
|
||||
for i, part := range a {
|
||||
if i%2 == 0 {
|
||||
key = part.(string)
|
||||
} else {
|
||||
val = reflect.ValueOf(part)
|
||||
typ = val.Type()
|
||||
|
||||
if typ.Implements(errType) {
|
||||
errVal = val.Interface().(error)
|
||||
event = event.Err(errVal)
|
||||
if errors.As(errVal, &elf) {
|
||||
event = elf.AppendFields(payload(event)).(*Payload).event()
|
||||
}
|
||||
continue enumerateFields
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
case reflect.String:
|
||||
event = event.Str(key, val.String())
|
||||
case reflect.Slice, reflect.Array:
|
||||
if arr, err = p.slice(val); err != nil {
|
||||
event = payload(event).unhandledField(key, part)
|
||||
} else {
|
||||
event = event.Array(key, arr)
|
||||
}
|
||||
case reflect.Bool:
|
||||
event = event.Bool(key, val.Bool())
|
||||
case reflect.Struct:
|
||||
if sub, err = p.structType(val); err != nil {
|
||||
event = payload(event).unhandledField(key, part)
|
||||
} else {
|
||||
event = event.Dict(key, sub)
|
||||
}
|
||||
case reflect.Pointer:
|
||||
if sub, err = p.pointer(val); err != nil {
|
||||
event = payload(event).unhandledField(key, part)
|
||||
} else {
|
||||
event = event.Dict(key, sub)
|
||||
}
|
||||
case reflect.Int:
|
||||
event = event.Int(key, int(val.Int()))
|
||||
case reflect.Int8:
|
||||
event = event.Int8(key, int8(val.Int()))
|
||||
case reflect.Int16:
|
||||
event = event.Int16(key, int16(val.Int()))
|
||||
case reflect.Int32:
|
||||
event = event.Int32(key, int32(val.Int()))
|
||||
case reflect.Int64:
|
||||
event = event.Int64(key, val.Int())
|
||||
case reflect.Float32:
|
||||
event = event.Float32(key, float32(val.Float()))
|
||||
case reflect.Float64:
|
||||
event = event.Float64(key, val.Float())
|
||||
case reflect.Uint:
|
||||
event = event.Uint(key, uint(val.Uint()))
|
||||
case reflect.Uint8:
|
||||
event = event.Uint8(key, uint8(val.Uint()))
|
||||
case reflect.Uint16:
|
||||
event = event.Uint16(key, uint16(val.Uint()))
|
||||
case reflect.Uint32:
|
||||
event = event.Uint32(key, uint32(val.Uint()))
|
||||
case reflect.Uint64:
|
||||
event = event.Uint64(key, val.Uint())
|
||||
default:
|
||||
event = payload(event).unhandledField(key, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload(event)
|
||||
}
|
||||
|
||||
func (p *Payload) Send(ws ...io.Writer) {
|
||||
event := (*zerolog.Event)(p)
|
||||
|
||||
writer.Lock()
|
||||
defer writer.Unlock()
|
||||
|
||||
if len(ws) == 0 {
|
||||
writer.Writers(DefaultOutlet)
|
||||
} else {
|
||||
writer.Writers(ws...)
|
||||
}
|
||||
event.Send()
|
||||
}
|
||||
|
||||
func (p *Payload) unhandledField(key string, v any) *zerolog.Event {
|
||||
event := (*zerolog.Event)(p)
|
||||
return event.Str(key, fmt.Sprintf("[unhandled data type / invalid] %+v", v))
|
||||
}
|
||||
|
||||
func (p *Payload) slice(val reflect.Value) (*zerolog.Array, error) {
|
||||
var (
|
||||
vlen int
|
||||
i int
|
||||
rval *zerolog.Array
|
||||
event *zerolog.Event
|
||||
item reflect.Value
|
||||
err error
|
||||
)
|
||||
|
||||
vlen = val.Len()
|
||||
rval = zerolog.Arr()
|
||||
for i = 0; i < vlen; i++ {
|
||||
item = val.Index(i)
|
||||
switch item.Kind() {
|
||||
//TODO: handle more cases
|
||||
case reflect.String:
|
||||
rval = rval.Str(item.String())
|
||||
case reflect.Struct:
|
||||
if event, err = p.structType(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rval = rval.Dict(event)
|
||||
case reflect.Int:
|
||||
rval = rval.Int(int(item.Int()))
|
||||
case reflect.Int8:
|
||||
rval = rval.Int8(int8(item.Int()))
|
||||
case reflect.Int16:
|
||||
rval = rval.Int16(int16(item.Int()))
|
||||
case reflect.Int32:
|
||||
rval = rval.Int32(int32(item.Int()))
|
||||
case reflect.Int64:
|
||||
rval = rval.Int64(item.Int())
|
||||
case reflect.Float32:
|
||||
rval = rval.Float32(float32(item.Float()))
|
||||
case reflect.Float64:
|
||||
rval = rval.Float64(item.Float())
|
||||
case reflect.Uint:
|
||||
rval = rval.Uint(uint(item.Uint()))
|
||||
case reflect.Uint8:
|
||||
rval = rval.Uint8(uint8(item.Uint()))
|
||||
case reflect.Uint16:
|
||||
rval = rval.Uint16(uint16(item.Uint()))
|
||||
case reflect.Uint32:
|
||||
rval = rval.Uint32(uint32(item.Uint()))
|
||||
case reflect.Uint64:
|
||||
rval = rval.Uint64(item.Uint())
|
||||
default:
|
||||
rval = rval.Str(fmt.Sprintf("[unhandled data type / invalid] %+v", item.Interface()))
|
||||
}
|
||||
}
|
||||
return rval, nil
|
||||
}
|
||||
|
||||
// pointer only supports pointer to a struct
|
||||
func (p *Payload) pointer(val reflect.Value) (*zerolog.Event, error) {
|
||||
val = val.Elem()
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil, errInvalidType
|
||||
}
|
||||
|
||||
return p.structType(val)
|
||||
}
|
||||
|
||||
func (p *Payload) structType(val reflect.Value) (*zerolog.Event, error) {
|
||||
var (
|
||||
typ reflect.Type
|
||||
event *zerolog.Event
|
||||
gotValidField bool
|
||||
)
|
||||
|
||||
typ = val.Type()
|
||||
event = zerolog.Dict()
|
||||
for _, sf := range reflect.VisibleFields(typ) {
|
||||
switch sf.Type.Kind() {
|
||||
case reflect.String:
|
||||
event = event.Str(sf.Name, val.FieldByIndex(sf.Index).String())
|
||||
gotValidField = true
|
||||
case reflect.Int:
|
||||
event = event.Int64(sf.Name, val.FieldByIndex(sf.Index).Int())
|
||||
gotValidField = true
|
||||
}
|
||||
}
|
||||
|
||||
if !gotValidField {
|
||||
return nil, errNoValidField
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (p *Payload) event() *zerolog.Event {
|
||||
return (*zerolog.Event)(p)
|
||||
}
|
||||
|
||||
func payload(event *zerolog.Event) *Payload {
|
||||
return (*Payload)(event)
|
||||
}
|
||||
|
||||
func Print(a ...any) {
|
||||
globalLevel.Print(a...)
|
||||
}
|
||||
|
||||
func PrintTo(outlets []io.Writer, a ...any) {
|
||||
globalLevel.PrintTo(outlets, a...)
|
||||
}
|
||||
|
||||
func SetGlobalLevel(l Level) {
|
||||
globalLevel = l
|
||||
zerolog.SetGlobalLevel(zerolog.Level(globalLevel))
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package wrapped
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.suyono.dev/suyono/wingmate/debugframes"
|
||||
"gitea.suyono.dev/suyono/wingmate/log"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrintWrappedError(t *testing.T) {
|
||||
log.Print("test", fmt.Errorf("wrapped error trace%w", debugframes.GetTraces()))
|
||||
}
|
||||
|
||||
func TestPrintWrappedPanic(t *testing.T) {
|
||||
f := func() (err error) {
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
err = fmt.Errorf("panic: %v%w", o, debugframes.PanicTrace(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
panic("test")
|
||||
return nil
|
||||
}
|
||||
|
||||
err := f()
|
||||
log.Print("panic_test", err)
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
noError = errors.New("no error")
|
||||
ec = make(chan error)
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
writers []io.Writer
|
||||
mtx *sync.Mutex
|
||||
}
|
||||
|
||||
func NewWriter() *Writer {
|
||||
return &Writer{
|
||||
writers: make([]io.Writer, 0),
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) Write(b []byte) (int, error) {
|
||||
var (
|
||||
ctr int
|
||||
sw io.Writer
|
||||
err error
|
||||
)
|
||||
|
||||
for _, ww := range w.writers {
|
||||
if ww == DefaultOutlet {
|
||||
sw = ww
|
||||
} else {
|
||||
ctr++
|
||||
go subWriter(ww, b, ec)
|
||||
}
|
||||
}
|
||||
|
||||
if sw != nil {
|
||||
_, _ = sw.Write(b)
|
||||
}
|
||||
|
||||
for i := 0; i < ctr; i++ {
|
||||
if err = <-ec; err != noError {
|
||||
onLogWriterError.Error().Err(err).Msg("subWriter error")
|
||||
}
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w *Writer) Lock() {
|
||||
w.mtx.Lock()
|
||||
}
|
||||
|
||||
func (w *Writer) Unlock() {
|
||||
w.writers[0] = DefaultOutlet
|
||||
w.writers = w.writers[:1]
|
||||
w.mtx.Unlock()
|
||||
}
|
||||
|
||||
func (w *Writer) Writers(ws ...io.Writer) {
|
||||
if len(ws) == 0 {
|
||||
w.writers[0] = DefaultOutlet
|
||||
w.writers = w.writers[:1]
|
||||
} else {
|
||||
w.writers = append(w.writers, ws...)
|
||||
}
|
||||
}
|
||||
|
||||
func subWriter(w io.Writer, b []byte, errchan chan<- error) {
|
||||
defer func() {
|
||||
if o := recover(); o != nil {
|
||||
errchan <- fmt.Errorf("subWriter panic: %v", o)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := w.Write(b); err != nil {
|
||||
errchan <- err
|
||||
} else {
|
||||
errchan <- noError
|
||||
}
|
||||
}
|
||||
|
||||
//func (w *Writer) Close() {
|
||||
//}
|
||||
41
logger.go
Normal file
41
logger.go
Normal file
@ -0,0 +1,41 @@
|
||||
package wingmate
|
||||
|
||||
import (
|
||||
"gitea.suyono.dev/suyono/wingmate/logger"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
w *wrapper
|
||||
)
|
||||
|
||||
type wrapper struct {
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLog(wc io.WriteCloser) error {
|
||||
w = &wrapper{
|
||||
log: zerolog.New(wc),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Log() logger.Log {
|
||||
if w == nil {
|
||||
panic("nil internal logger")
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *wrapper) Info() logger.Content {
|
||||
return w.log.Info()
|
||||
}
|
||||
|
||||
func (w *wrapper) Warn() logger.Content {
|
||||
return w.log.Warn()
|
||||
}
|
||||
|
||||
func (w *wrapper) Error() logger.Content {
|
||||
return w.log.Error()
|
||||
}
|
||||
16
logger/logger.go
Normal file
16
logger/logger.go
Normal file
@ -0,0 +1,16 @@
|
||||
package logger
|
||||
|
||||
type Content interface {
|
||||
Msg(string)
|
||||
Msgf(string, ...any)
|
||||
}
|
||||
|
||||
type Level interface {
|
||||
Info() Content
|
||||
Warn() Content
|
||||
Error() Content
|
||||
}
|
||||
|
||||
type Log interface {
|
||||
Level
|
||||
}
|
||||
7
main.go
7
main.go
@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
import "gitea.suyono.dev/suyono/wingmate/cmd"
|
||||
|
||||
func main() {
|
||||
_ = cmd.Execute()
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ChainFunc func(cmd *cobra.Command, args []string) error
|
||||
|
||||
func CallChain(cmd *cobra.Command, args []string, functions ...ChainFunc) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
for i, f := range functions {
|
||||
if err = f(cmd, args); err != nil {
|
||||
return fmt.Errorf("error when calling function number %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user