From 7e80ddf59c9048e12cf1f0be9d9a8d5058aca939 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Sat, 17 Dec 2022 23:26:37 +0200 Subject: [PATCH] add parser and depgraph implementation --- foreman.go | 117 ++++++++++++++++++++++++- go.mod | 16 ++++ go.sum | 18 ++++ internal/depgraph/depgraph.go | 56 ++++++++++++ internal/procparser/procfile_parser.go | 70 ++++++++++++++- 5 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/foreman.go b/foreman.go index 54a67dd..4efcfe3 100755 --- a/foreman.go +++ b/foreman.go @@ -1 +1,116 @@ -package foreman +package foreman + +import ( + "errors" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/Eslam-Nawara/foreman/internal/depgraph" + parser "github.com/Eslam-Nawara/foreman/internal/procparser" + "gopkg.in/yaml.v3" +) + +const interval = 50 * time.Millisecond + +type Foreman struct { + services map[string]parser.Service + verbose bool + logger *log.Logger + servicesMutex sync.Mutex +} + +func New(filePath string, verbose bool) (*Foreman, error) { + foreman := &Foreman{ + services: map[string]parser.Service{}, + servicesMutex: sync.Mutex{}, + logger: log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime), + verbose: verbose, + } + + yamlMap := make(map[string]map[string]any) + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal([]byte(data), yamlMap) + if err != nil { + return nil, err + } + + for serviceName, serviceInfo := range yamlMap { + service := parser.ParseService(serviceInfo) + service.Name = serviceName + foreman.services[serviceName] = service + } + return foreman, nil +} + +// start all services from yaml file +func (foreman *Foreman) Start() error { + var wg sync.WaitGroup + graph := foreman.buildDepGraph() + + if depgraph.IsCyclic(graph) { + return errors.New("Cyclic dependencies detected") + } + + services := depgraph.TopSort(graph) + + for _, serviceName := range services { + wg.Add(1) + go func(serviceName string) { + defer wg.Done() + foreman.runService(serviceName) + }(serviceName) + } + + go func() { + sigChan := make(chan os.Signal) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + foreman.Exit(1) + }() + wg.Wait() + return nil +} + +// start one service and wait for it +func (foreman *Foreman) runService(serviceName string) error { + return nil +} + +func (foreman *Foreman) checker(serviceName string, stopChecker chan bool) { +} + +func (foreman *Foreman) buildDepGraph() map[string][]string { + graph := make(map[string][]string) + for serviceName, service := range foreman.services { + graph[serviceName] = service.Deps + } + return graph +} + +func (f *Foreman) vLog(msg string) { + if f.verbose { + f.logger.Print(msg) + } +} + +// ُExit kills all the running services and checkers. +// exits foreman with the given exit status. +func (f *Foreman) Exit(exitStatus int) { + f.servicesMutex.Lock() + for _, service := range f.services { + if service.Process != nil { + syscall.Kill(-service.Process.Pid, syscall.SIGINT) + } + } + f.servicesMutex.Unlock() + os.Exit(exitStatus) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f53fff4 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/Eslam-Nawara/foreman + +go 1.19 + +require ( + github.com/shirou/gopsutil v3.21.11+incompatible + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/sys v0.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb11ec8 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/depgraph/depgraph.go b/internal/depgraph/depgraph.go index 1383e7f..5f7406a 100755 --- a/internal/depgraph/depgraph.go +++ b/internal/depgraph/depgraph.go @@ -1 +1,57 @@ package depgraph + +const ( + unvisited = 0 + visited = 1 + currentlyVisiting = 2 +) + +func TopSort(graph map[string][]string) []string { + var out []string + state := make(map[string]int) + var ts func(string) + + ts = func(node string) { + if state[node] == visited { + return + } + state[node] = visited + for _, child := range graph[node] { + ts(child) + } + out = append(out, node) + } + + for node := range graph { + ts(node) + } + + return out +} + +func IsCyclic(graph map[string][]string) bool { + state := make(map[string]int) + cyclic := false + var dfs func(string) + + dfs = func(node string) { + if state[node] == visited { + return + } + if state[node] == currentlyVisiting { + cyclic = true + return + } + + state[node] = currentlyVisiting + for _, child := range graph[node] { + dfs(child) + } + state[node] = visited + } + + for node := range graph { + dfs(node) + } + return cyclic +} diff --git a/internal/procparser/procfile_parser.go b/internal/procparser/procfile_parser.go index a46c2b1..4a32bd2 100755 --- a/internal/procparser/procfile_parser.go +++ b/internal/procparser/procfile_parser.go @@ -1 +1,69 @@ -package procparser +package procparser + +import ( + "fmt" + "os" +) + +type ( + Service struct { + Name string + Status bool + Process *os.Process + Cmd string + RunOnce bool + Checks ServiceChecks + Deps []string + } + + ServiceChecks struct { + Cmd string + TcpPorts []string + UdpPorts []string + } +) + +func ParseService(serviceMap map[string]any) Service { + service := Service{} + + for key, value := range serviceMap { + switch key { + case "cmd": + service.cmd = value.(string) + case "run_once": + service.RunOnce = value.(bool) + case "deps": + for _, dep := range value.([]any) { + service.Deps = append(service.Deps, dep.(string)) + } + case "checks": + service.Checks = parseChecks(value) + } + } + return service +} + +func parseChecks(serviceChecks any) ServiceChecks { + checksMap := ServiceChecks{} + + for key, value := range serviceChecks.(map[string]any) { + switch key { + case "cmd": + checksMap.Cmd = value.(string) + case "tcp_ports": + checksMap.TcpPorts = parsePorts(value) + case "udp_ports": + checksMap.UdpPorts = parsePorts(value) + } + } + return checksMap +} + +func parsePorts(ports any) []string { + var parsedPorts []string + for _, port := range ports.([]any) { + parsedPorts = append(parsedPorts, fmt.Sprint(port.(int))) + } + + return parsedPorts +}