Files
Das-OS/srcpkgs/das-tools/files/src/cmd/das-update/main.go
T
dasandClaude Opus 5.5 eba4516998 Stage 6: server image with a console installer (0.4.0)
- server/ profile and build-iso.sh [desktop|server]; make iso-server,
  make qemu-server, run-qemu.sh --serial; login on tty and ttyS0
- das-server: SSH, chrony, NetworkManager/dhcpcd, Docker with Compose,
  nftables (closed except SSH, own table), fail2ban reading socklog,
  sshd without root passwords, Docker log rotation, tools
- das-server-install: dialog installer in Ukrainian and English, or
  unattended with --config; GPT + Btrfs subvolumes, GRUB for UEFI and BIOS,
  serial console, SSH key with optional keys-only logins
- das-install-common: mount-source and postinstall shared with Calamares
- dsv: runit service manager (list, enable, disable, start, stop,
  restart, status, logs) with bash completion, also on the desktop
- das-update split into a subpackage without GTK; the window and tray
  icon move to /usr/libexec/das/das-update-gui
- das-snapper-setup enables dbus, which snapper needs
- TODO: ISO size review with measured numbers and options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:28:26 +03:00

203 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}