Stage 5: das-update and das-welcome (0.3.0)
das-tools (Go, GTK 3 via gotk3): - das-update: update window, tray icon when updates are available, --cli mode; xbps-install -Su between a snapper pre/post snapshot pair, privileged part through pkexec, rollback and reboot hints - das-welcome: first-login window with install (live), language, home folder renaming, updates, settings and help das-settings 0.3.0: create home folders with translated names. xdg-user-dirs 0.20 only looks for translations in the first XDG_DATA_DIRS entry with a locale directory (/usr/local/share/locale). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// 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 window that lists and installs updates
|
||||
// das-update --tray background check, shows a tray icon when updates are available
|
||||
// das-update --cli the same in a terminal (servers, ssh); -y skips the question
|
||||
// das-update --apply the privileged part, started by the window through pkexec
|
||||
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:
|
||||
os.Exit(runTray())
|
||||
default:
|
||||
os.Exit(runWindow())
|
||||
}
|
||||
}
|
||||
|
||||
// Machine-readable lines that --apply prints for the window, among the
|
||||
// plain output of xbps.
|
||||
const (
|
||||
markStep = "@@step "
|
||||
markSnapshot = "@@snapshot "
|
||||
markReboot = "@@reboot"
|
||||
markUpToDate = "@@uptodate"
|
||||
markError = "@@error "
|
||||
)
|
||||
|
||||
type markReporter struct{ w io.Writer }
|
||||
|
||||
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, markStep+title) }
|
||||
func (r markReporter) Output() io.Writer { return r.w }
|
||||
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", 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(markError + err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(res.Updates) == 0 {
|
||||
fmt.Println(markUpToDate)
|
||||
} else if res.Reboot {
|
||||
fmt.Println(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
|
||||
}
|
||||
Reference in New Issue
Block a user