package main import ( "bufio" "errors" "flag" "fmt" "log" "os" "os/exec" "strings" "time" ) var ( serviceFile = "/adm/services" services = make(map[string]Service) controlSocket = "/adm/headless9/ctl/headless9.ctl" logPath = "/adm/headless9/log/" execWait = false headless9Start = time.Now() ) func main() { flag.Parse() if flag.NArg() == 0 { logFile, err := os.Create(fmt.Sprintf("%+v/%+v.log", logPath, "headless9")) if err != nil { panic(err) } log.SetOutput(logFile) log.Println(DAEMON_START) runDaemon() } if flag.NArg() != 2 { log.Println(CMD_SYNTAX) return } verb := flag.Args()[0] service := flag.Args()[1] f := getCtl() _, err := fmt.Fprintf(f, "%+v %+v", verb, service) if err != nil { log.Fatalf(CTL_UNABLE_WRITE, controlSocket) } f.Close() err = watchFile(controlSocket) if err != nil { log.Println(err) } fmt.Println(sysTail(10, controlSocket)) } func getCtl() *os.File { f, err := os.OpenFile(controlSocket, os.O_TRUNC, 0660) if err != nil { log.Fatalf(CTL_NOT_OPEN, controlSocket) } err = f.Truncate(0) if err != nil { log.Fatalf(CTL_NOT_CLEAR, controlSocket) } _, err = f.Seek(0, 0) if err != nil { log.Fatalf(CTL_NOT_REWOUND, controlSocket) } f, err = os.OpenFile(controlSocket, os.O_RDWR, 0660) if err != nil { log.Fatalf(CTL_NOT_OPEN, controlSocket) } return f } func runDaemon() { go headlessControls() for { log.Printf(DAEMON_FILE_REFRESH, serviceFile) startup, err := readLines(serviceFile) if err != nil { if errors.Is(err, os.ErrNotExist) { test, err := os.Create(serviceFile) if err != nil { log.Fatalln(err) } test.Close() } else { log.Fatalln(err) } } for _, svc := range startup { running := false svcArgs := strings.Split(svc, " ") for runningProc := range services { if svcArgs[0] == runningProc { running = true log.Printf(DAEMON_SVC_EXISTS, svcArgs[0], services[svcArgs[0]]) } } if !running { log.Printf(DAEMON_SVC_MISSING, svcArgs[0]) go execCommand(svcArgs[0], svcArgs[1:]...) } } err = watchFile(serviceFile) if err != nil { log.Println(err) } } } func headlessControls() { for { err := watchFile(controlSocket) if err != nil { log.Println(err) } pendingCommands, err := readLines(controlSocket) if err != nil { log.Println(err) } else { for _, cmd := range pendingCommands { log.Printf(DAEMON_CTL_PROCESSING, cmd) start := time.Now() err = processCommand(cmd) if err != nil { log.Printf(DAEMON_CTL_FAILED, err, time.Since(start)) } else { log.Printf(DAEMON_CTL_PROCESSED, cmd, time.Since(start)) } } } } } func processCommand(cmd string) error { defer PanicSafe() parts := strings.Split(cmd, " ") verb := parts[0] svc := parts[1] start := time.Now() messages := 0 if verb == "restart" || verb == "stop" { f, err := os.OpenFile(fmt.Sprintf("/proc/%d/ctl", services[svc].ProcessHandle.Pid), os.O_WRONLY, 0660) if err != nil { log.Printf(DAEMON_CTL_FAILED, err, time.Since(start)) messages++ } defer f.Close() _, err = f.WriteString("kill") if err != nil { log.Printf(DAEMON_CTL_FAILED, err, time.Since(start)) messages++ } else { delete(services, svc) log.Printf(DAEMON_CTL_PROCESSED, svc, time.Since(start)) messages++ } } if verb == "restart" || verb == "start" { startup, err := readLines(serviceFile) if err != nil { log.Fatalln(err) } for _, testSvc := range startup { svcArgs := strings.Split(testSvc, " ") if svc != testSvc { continue } log.Printf(DAEMON_SVC_MISSING, svcArgs[0]) messages++ execWait = true go execCommand(svcArgs[0], svcArgs[1:]...) } } for execWait { time.Sleep(5 * time.Millisecond) } ctlOut := "" if verb == "status" { ctlOut += fmt.Sprintf("%+v up %+v PID %+v\n", svc, time.Since(services[svc].StartTime), services[svc].ProcessHandle.Pid) ctlOut += strings.Join(sysTail(5, fmt.Sprintf("%+v/%+v.log", logPath, svc)), "\n") } else { ctlOut += fmt.Sprintf("%+v up %+v\n", "headless9", time.Since(headless9Start)) ctlOut += strings.Join(sysTail(messages, fmt.Sprintf("%+v/%+v.log", logPath, "headless9")), "\n") } f := getCtl() _, err := fmt.Fprintf(f, "%s", ctlOut) if err != nil { log.Printf(CTL_UNABLE_WRITE, controlSocket) } f.Close() return nil } func execCommand(cmd string, arg ...string) { defer PanicSafe() if len(cmd) < 2 { log.Printf(DAEMON_SVC_INVALID, cmd, arg) return } if strings.HasPrefix(cmd, "#") { log.Printf(DAEMON_SVC_DISABLED, cmd) return } proc := exec.Command(cmd, arg...) // open the out file for writing outfile, err := os.Create(fmt.Sprintf("%+v/%+v.log", logPath, cmd)) if err != nil { panic(err) } infile, err := os.Create(fmt.Sprintf("%+v/%+v.ctl", logPath, cmd)) if err != nil { panic(err) } defer outfile.Close() defer infile.Close() proc.Stderr = outfile proc.Stdout = outfile proc.Stdin = infile err = proc.Start() if err != nil { log.Printf(DAEMON_SVC_FAIL, cmd) return } services[cmd] = Service{ ProcessHandle: proc.Process, StartupArgs: arg, StartTime: time.Now(), } execWait = false proc.Wait() } // readLines reads a whole file into memory // and returns a slice of its lines. func readLines(path string) ([]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } return lines, scanner.Err() } func watchFile(filePath string) error { initialStat, err := os.Stat(filePath) if err != nil { return err } for { stat, err := os.Stat(filePath) if err != nil { return err } if stat.Size() != initialStat.Size() || stat.ModTime() != initialStat.ModTime() { break } time.Sleep(250 * time.Millisecond) } return nil } // PanicSafe is a deferrable function to recover from a panic operation. func PanicSafe(a ...interface{}) { if r := recover(); r != nil { log.Printf("Panic detected: %+v", r) log.Printf("Optional panic data: %+v", a...) } } // count for number of lines, path of file func sysTail(count int, path string) []string { c := exec.Command("tail", fmt.Sprintf("-%d", count+1), path) output, _ := c.Output() lines := strings.Split(string(output), "\n") return lines[:len(lines)-1] }