package core import ( "fmt" "io" "log" "os" "os/exec" "sort" "strings" ) var IntakeCronBegin = "### begin intake-managed crontab entries" var IntakeCronEnd = "### end intake-managed crontab entries" func makeCrontabEntry(source string, spec string) string { // TODO the /etc/profile setup is NixOS-specific, maybe there's another way to do this return fmt.Sprintf("%-20s . /etc/profile; intake source fetch -s %s", spec, source) } func GetCronSources(db DB) (specs map[string]string, err error) { res, err := db.Query(` select source, value from envs where name = 'INTAKE_CRON' `) if err != nil { return nil, fmt.Errorf("failed to get source crontabs: %v", err) } specs = make(map[string]string) for res.Next() { var source string var value string if err = res.Scan(&source, &value); err != nil { return nil, fmt.Errorf("failed to scan source crontab: %v", err) } specs[source] = makeCrontabEntry(source, value) } return } // Update the intake-managed section of the user's crontab. func UpdateCrontab(db DB, specs map[string]string) (err error) { // If there is no crontab command available, quit early. crontabPath, err := exec.LookPath("crontab") if err != nil { return fmt.Errorf("no crontab found") } // Get the current crontab without extra header lines via `EDITOR=cat crontab -e` cmdLoad := exec.Command(crontabPath, "-e") cmdLoad.Env = append(os.Environ(), "EDITOR=cat") output, err := cmdLoad.Output() if err != nil { return fmt.Errorf("error: failed to get current crontab: %v", err) } lines := strings.Split(string(output), "\n") // Sort the new intake crons var sources []string for source := range specs { sources = append(sources, source) } sort.Strings(sources) // Splice the intake crons into the crontab var newCrontab []string headerFound := false inSection := false for i := range lines { switch { case !headerFound && lines[i] == IntakeCronBegin: headerFound = true inSection = true newCrontab = append(newCrontab, IntakeCronBegin) for _, source := range sources { newCrontab = append(newCrontab, specs[source]) } case lines[i] == IntakeCronEnd: newCrontab = append(newCrontab, IntakeCronEnd) inSection = false case !inSection: newCrontab = append(newCrontab, lines[i]) } } // If the splice mark was never found, append the whole section to the end if !headerFound { newCrontab = append(newCrontab, IntakeCronBegin) for _, source := range sources { newCrontab = append(newCrontab, specs[source]) } newCrontab = append(newCrontab, IntakeCronEnd) } log.Printf("Updating %d crontab entries", len(specs)) // Save the updated crontab cmdSave := exec.Command(crontabPath, "-") stdin, err := cmdSave.StdinPipe() if err != nil { return fmt.Errorf("failed to open stdin: %v", err) } if _, err := io.WriteString(stdin, strings.Join(newCrontab, "\n")); err != nil { return fmt.Errorf("failed to write to crontab: %v", err) } if err := stdin.Close(); err != nil { return fmt.Errorf("failed to close stdin: %v", err) } output, err = cmdSave.CombinedOutput() if err != nil { log.Printf("failed to read crontab output: %v", err) } if len(output) > 0 { log.Printf("crontab output: %s", string(output)) } return nil }