- 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>
203 lines
5.4 KiB
Go
203 lines
5.4 KiB
Go
// 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
|
||
}
|