aptgrade.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package main
  2. import (
  3. "bufio"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "os"
  8. "os/exec"
  9. "strings"
  10. "syscall"
  11. )
  12. /*
  13. NOTES:
  14. * apt detected it wasn't connected to a TTY, so it said on stderr,
  15. WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
  16. * All packages are up to date. <- This isn't seen when using `apt-get update`, only apt.
  17. * We don't see the warning about "apt does not have a stable CLI interface." when using apt-get.
  18. * I can't seem to reproduce the output error (where apt is reading the database and showing
  19. the percentage increasing). See the output directory for am example program that has that type
  20. of output. (Which works fine. Why doesn't apt or apt-get work correctly?!)
  21. */
  22. type CheckResult struct {
  23. Lines int // Number of lines read.
  24. Upgradable bool // 'apt list --upgradable'
  25. // UpToDate bool // All packages are up to date.
  26. AutoRemove bool // Use 'apt autoremove' to remove it.
  27. }
  28. // Check the output for CheckResult conditions.
  29. func check_lines(r io.Reader, output chan CheckResult) {
  30. scanner := bufio.NewScanner(r)
  31. var err error
  32. var result CheckResult
  33. var line string
  34. for scanner.Scan() {
  35. line = scanner.Text()
  36. /*
  37. if line == "All packages are up to date." {
  38. result.UpToDate = true
  39. }
  40. */
  41. if strings.Contains(line, "apt list --upgradable") {
  42. result.Upgradable = true
  43. }
  44. if line == "Use 'apt autoremove' to remove it." || line == "Use 'sudo apt autoremove' to remove it" {
  45. result.AutoRemove = true
  46. }
  47. result.Lines += 1
  48. }
  49. err = scanner.Err()
  50. if err != nil {
  51. panic(err)
  52. }
  53. output <- result
  54. }
  55. // Run the given command, showing output, checking output.
  56. func run_check_command(command []string, debug_output bool) (CheckResult, error) {
  57. var result CheckResult
  58. cmd := exec.Command(command[0], command[1:]...)
  59. r, w, err := os.Pipe()
  60. if err != nil {
  61. fmt.Printf("Failed to create pipe: %s", err)
  62. return result, err
  63. }
  64. // Start output parser.
  65. output := make(chan CheckResult)
  66. go check_lines(r, output)
  67. // Connections
  68. // Connect output with stdout and parser.
  69. if debug_output {
  70. filename := fmt.Sprintf("debug-%s.txt", command[1])
  71. fp, err := os.Create(filename)
  72. if err != nil {
  73. fmt.Printf("Failed to create file [%s]: %s", filename, err)
  74. return result, err
  75. }
  76. defer fp.Close()
  77. cmd.Stdout = io.MultiWriter(os.Stdout, w, fp)
  78. fmt.Printf("See: %s for output.\n", filename)
  79. } else {
  80. cmd.Stdout = io.MultiWriter(os.Stdout, w)
  81. }
  82. cmd.Stderr = os.Stderr
  83. cmd.Stdin = os.Stdin
  84. // cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
  85. // Run the process as root (We should be SETUID).
  86. cmd.SysProcAttr = &syscall.SysProcAttr{}
  87. cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
  88. fmt.Println(strings.Repeat("=", 64))
  89. fmt.Println("Running", command)
  90. err = cmd.Run()
  91. fmt.Println(strings.Repeat("=", 50))
  92. if err != nil {
  93. fmt.Println("Did you forget to:")
  94. fmt.Println(" sudo chown root:root aptgrade")
  95. fmt.Println(" sudo chmod a+s aptgrade")
  96. fmt.Println("Command failed:", err)
  97. }
  98. // If I want check_lines to end, it needs the reader closed.
  99. // So, close the write end of the pipe.
  100. w.Close()
  101. // fmt.Println("Reading results from go routine:")
  102. value := <-output
  103. if !cmd.ProcessState.Success() {
  104. // Process exited with non-success code.
  105. fmt.Printf("Process %d exited with status code %d\n", cmd.ProcessState.Pid(), cmd.ProcessState.ExitCode())
  106. }
  107. // fmt.Printf("Result: %+v\n", value)
  108. return value, err
  109. }
  110. // const APT = "/usr/bin/apt"
  111. const APT = "/usr/bin/apt-get"
  112. func main() {
  113. // const APT = APT_CMD
  114. var show bool
  115. flag.BoolVar(&show, "show", false, "Show run results.")
  116. /*
  117. // Testing
  118. var APT string
  119. flag.StringVar(&APT, "apt", APT_CMD, "Apt command to use.")
  120. */
  121. var output bool
  122. flag.BoolVar(&output, "output", false, "Create debug output files.")
  123. flag.Parse()
  124. if os.Geteuid() != 0 {
  125. fmt.Println("Use `sudo install -g 0 -o 0 -m 4755 aptgrade ~/bin/aptgrade`")
  126. fmt.Println("This program requires SETUID.")
  127. os.Exit(2)
  128. }
  129. // Check for updates.
  130. result, err := run_check_command([]string{APT, "update"}, output)
  131. if err != nil {
  132. fmt.Printf("Error: %s\n", err)
  133. os.Exit(2)
  134. }
  135. if show {
  136. fmt.Printf("Result: {%+v}\n", result)
  137. }
  138. // Is everything up to date?
  139. if result.Upgradable {
  140. // Perform upgrade.
  141. result, err = run_check_command([]string{APT, "upgrade", "-y"}, output)
  142. if err != nil {
  143. fmt.Printf("Error: %s\n", err)
  144. os.Exit(2)
  145. }
  146. if show {
  147. fmt.Printf("Result: {%+v}\n", result)
  148. }
  149. // Is apt suggesting autoremove?
  150. if result.AutoRemove {
  151. // Perform autoremove.
  152. result, err = run_check_command([]string{APT, "autoremove", "-y"}, output)
  153. if err != nil {
  154. fmt.Printf("Error: %s\n", err)
  155. os.Exit(2)
  156. }
  157. if show {
  158. fmt.Printf("Result: {%+v}\n", result)
  159. }
  160. }
  161. }
  162. fmt.Println("Done")
  163. }