// Command das-update updates Das OS with a snapper snapshot taken before // the update, so a broken update can be rolled back from the boot menu. // // das-update the update window on a desktop, otherwise the same as --cli // das-update --tray background check, shows a tray icon when updates are available // das-update --cli update in a terminal (servers, ssh); -y skips the question // das-update --apply the privileged part, started by the window through pkexec // // The window and the tray icon are a separate program from das-tools // (guiPath), so this one has no GTK dependency. package main import ( "bufio" "context" "flag" "fmt" "io" "os" "os/exec" "os/signal" "strings" "syscall" "dasos/tools/internal/i18n" "dasos/tools/internal/system" "dasos/tools/internal/update" "dasos/tools/internal/xbps" ) func main() { cli := flag.Bool("cli", false, "update in the terminal") yes := flag.Bool("y", false, "with --cli: do not ask for confirmation") apply := flag.Bool("apply", false, "install the updates as root (used by the window)") tray := flag.Bool("tray", false, "check for updates in the background") lang := flag.String("lang", "", "message language, e.g. uk_UA.UTF-8 (pkexec clears LANG)") flag.Parse() if *lang != "" { i18n.SetLang(*lang) } switch { case *apply: os.Exit(runApply()) case *cli: os.Exit(runCLI(*yes)) case *tray: // Nothing to show without the desktop part if haveGUI() { execGUI() } default: if haveGUI() && haveDisplay() { execGUI() } os.Exit(runCLI(*yes)) } } // guiPath is the window and tray icon program from das-tools. const guiPath = "/usr/libexec/das/das-update-gui" func haveGUI() bool { _, err := os.Stat(guiPath) return err == nil } func haveDisplay() bool { return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" } // execGUI replaces this process with the graphical part, passing the // arguments on. It returns only when that fails. func execGUI() { err := syscall.Exec(guiPath, append([]string{guiPath}, os.Args[1:]...), os.Environ()) fmt.Fprintln(os.Stderr, "das-update:", err) } type markReporter struct{ w io.Writer } func (r markReporter) Step(title string) { fmt.Fprintln(r.w, update.MarkStep+title) } func (r markReporter) Output() io.Writer { return r.w } func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", update.MarkSnapshot, n) } func runApply() int { if os.Geteuid() != 0 { fmt.Fprintln(os.Stderr, "das-update: "+i18n.T("update.needroot")) return 1 } // Losing the window must not stop xbps halfway through a transaction. signal.Ignore(syscall.SIGHUP, syscall.SIGPIPE) r := markReporter{os.Stdout} res, err := update.System().Apply(context.Background(), r) if err != nil { fmt.Println(update.MarkError + err.Error()) return 1 } if len(res.Updates) == 0 { fmt.Println(update.MarkUpToDate) } else if res.Reboot { fmt.Println(update.MarkReboot) } return 0 } type cliReporter struct{} func (cliReporter) Step(title string) { fmt.Printf("\n==> %s\n", title) } func (cliReporter) Output() io.Writer { return os.Stdout } func (cliReporter) Snapshot(n int) { fmt.Println(i18n.T("update.snapshot", n)) } func runCLI(yes bool) int { if !yes { fmt.Println(i18n.T("update.checking")) updates, err := xbps.Check(context.Background()) if err != nil { fmt.Fprintln(os.Stderr, i18n.T("update.checkfailed", err)) return 1 } if len(updates) == 0 { fmt.Println(i18n.T("update.uptodate")) return 0 } printUpdates(os.Stdout, updates) if !confirm(os.Stdin) { fmt.Println(i18n.T("update.cancelled")) return 1 } } if os.Geteuid() != 0 { return sudo() } signal.Ignore(syscall.SIGHUP) res, err := update.System().Apply(context.Background(), cliReporter{}) fmt.Println() switch { case err != nil: fmt.Fprintf(os.Stderr, "%s %v\n", i18n.T("update.failed"), err) case len(res.Updates) == 0: fmt.Println(i18n.T("update.uptodate")) default: fmt.Println(i18n.T("update.done")) if res.Reboot { fmt.Println(i18n.T("update.reboot")) } } if res.Snapshot > 0 && len(res.Updates) > 0 { fmt.Println(i18n.T("update.rollback", res.Snapshot, res.Snapshot)) } if err != nil { return 1 } return 0 } // sudo runs the confirmed update again as root. func sudo() int { self, err := os.Executable() if err == nil { var path string path, err = exec.LookPath("sudo") if err == nil { err = syscall.Exec(path, []string{"sudo", self, "--cli", "-y", "--lang", system.Lang()}, os.Environ()) } } fmt.Fprintf(os.Stderr, "das-update: %s: %v\n", i18n.T("update.needroot"), err) return 1 } func printUpdates(w io.Writer, updates []xbps.Update) { fmt.Fprintln(w, i18n.T("update.available")) width := 0 for _, u := range updates { width = max(width, len(u.Name)) } for _, u := range updates { switch { case u.Action == "remove": fmt.Fprintf(w, " %-*s %s → −\n", width, u.Name, u.OldVersion) case u.OldVersion == "": fmt.Fprintf(w, " %-*s + %s\n", width, u.Name, u.NewVersion) default: fmt.Fprintf(w, " %-*s %s → %s\n", width, u.Name, u.OldVersion, u.NewVersion) } } fmt.Fprintln(w, i18n.Plural("update.count", len(updates))) } // confirm accepts Enter, y/yes and the Ukrainian т/так. func confirm(in io.Reader) bool { fmt.Print(i18n.T("update.confirm")) line, err := bufio.NewReader(in).ReadString('\n') if err != nil && line == "" { return false } switch strings.ToLower(strings.TrimSpace(line)) { case "", "y", "yes", "т", "так": return true } return false }