update.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package admin
  2. import (
  3. "fmt"
  4. "io"
  5. "io/ioutil"
  6. "net/http"
  7. "os"
  8. "os/exec"
  9. "strconv"
  10. "strings"
  11. "github.com/owncast/owncast/config"
  12. "github.com/owncast/owncast/controllers"
  13. log "github.com/sirupsen/logrus"
  14. )
  15. /*
  16. The auto-update relies on some guesses and hacks to determine how the binary
  17. is being run.
  18. It determines if is running under systemd by asking systemd about the PID
  19. and checking the parent pid or INVOCATION_ID property is set for it.
  20. It also determines if the binary is running under a container by figuring out
  21. the container ID as a fallback to refuse an in-place update within a container.
  22. In general it's disabled for everyone and the features are enabled only if
  23. specific conditions are met.
  24. 1. Cannot be run inside a container.
  25. 2. Cannot be run from source (aka platform is "dev")
  26. 3. Must be run under systemd to support auto-restart.
  27. */
  28. // AutoUpdateOptions will return what auto update options are available.
  29. func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) {
  30. type autoUpdateOptionsResponse struct {
  31. SupportsUpdate bool `json:"supportsUpdate"`
  32. CanRestart bool `json:"canRestart"`
  33. }
  34. updateOptions := autoUpdateOptionsResponse{
  35. SupportsUpdate: false,
  36. CanRestart: false,
  37. }
  38. // Nothing is supported when running under "dev" or the feature is
  39. // explicitly disabled.
  40. if config.BuildPlatform == "dev" || !config.EnableAutoUpdate {
  41. controllers.WriteResponse(w, updateOptions)
  42. return
  43. }
  44. // If we are not in a container then we can update in place.
  45. if getContainerID() == "" {
  46. updateOptions.SupportsUpdate = true
  47. }
  48. updateOptions.CanRestart = isRunningUnderSystemD()
  49. controllers.WriteResponse(w, updateOptions)
  50. }
  51. // AutoUpdateStart will begin the auto update process.
  52. func AutoUpdateStart(w http.ResponseWriter, r *http.Request) {
  53. // We return the console output directly to the client.
  54. w.Header().Set("Content-Type", "text/plain")
  55. // Download the installer and save it to a temp file.
  56. updater, err := downloadInstaller()
  57. if err != nil {
  58. log.Errorln(err)
  59. controllers.WriteSimpleResponse(w, false, "failed to download and run installer")
  60. return
  61. }
  62. fw := flushWriter{w: w}
  63. if f, ok := w.(http.Flusher); ok {
  64. fw.f = f
  65. }
  66. // Run the installer.
  67. cmd := exec.Command("bash", updater)
  68. cmd.Env = append(os.Environ(), "NO_COLOR=true")
  69. cmd.Stdout = &fw
  70. cmd.Stderr = &fw
  71. if err := cmd.Run(); err != nil {
  72. log.Debugln(err)
  73. if _, err := w.Write([]byte("Unable to complete update: " + err.Error())); err != nil {
  74. log.Errorln(err)
  75. }
  76. return
  77. }
  78. }
  79. // AutoUpdateForceQuit will force quit the service.
  80. func AutoUpdateForceQuit(w http.ResponseWriter, r *http.Request) {
  81. log.Warnln("Owncast is exiting due to request.")
  82. go func() {
  83. os.Exit(0)
  84. }()
  85. controllers.WriteSimpleResponse(w, true, "forcing quit")
  86. }
  87. func downloadInstaller() (string, error) {
  88. installer := "https://owncast.online/install.sh"
  89. out, err := os.CreateTemp(os.TempDir(), "updater.sh")
  90. if err != nil {
  91. log.Errorln(err)
  92. return "", err
  93. }
  94. defer out.Close()
  95. // Get the installer script
  96. resp, err := http.Get(installer)
  97. if err != nil {
  98. return "", err
  99. }
  100. defer resp.Body.Close()
  101. // Write the installer to file
  102. _, err = io.Copy(out, resp.Body)
  103. if err != nil {
  104. return "", err
  105. }
  106. return out.Name(), nil
  107. }
  108. // Check to see if owncast is listed as a running service under systemd.
  109. func isRunningUnderSystemD() bool {
  110. // Our current PID
  111. ppid := os.Getppid()
  112. // A randomized, unique 128-bit ID identifying each runtime cycle of the unit.
  113. invocationID, hasInvocationID := os.LookupEnv("INVOCATION_ID")
  114. // systemd's pid should be 1, so if our process' parent pid is 1
  115. // then we are running under systemd.
  116. return ppid == 1 || (hasInvocationID && invocationID != "")
  117. }
  118. // Taken from https://stackoverflow.com/questions/23513045/how-to-check-if-a-process-is-running-inside-docker-container
  119. func getContainerID() string {
  120. pid := os.Getppid()
  121. cgroupPath := fmt.Sprintf("/proc/%s/cgroup", strconv.Itoa(pid))
  122. containerID := ""
  123. content, err := ioutil.ReadFile(cgroupPath) //nolint:gosec
  124. if err != nil {
  125. return containerID
  126. }
  127. lines := strings.Split(string(content), "\n")
  128. for _, line := range lines {
  129. field := strings.Split(line, ":")
  130. if len(field) < 3 {
  131. continue
  132. }
  133. cgroupPath := field[2]
  134. if len(cgroupPath) < 64 {
  135. continue
  136. }
  137. // Non-systemd Docker
  138. // 5:net_prio,net_cls:/docker/de630f22746b9c06c412858f26ca286c6cdfed086d3b302998aa403d9dcedc42
  139. // 3:net_cls:/kubepods/burstable/pod5f399c1a-f9fc-11e8-bf65-246e9659ebfc/9170559b8aadd07d99978d9460cf8d1c71552f3c64fefc7e9906ab3fb7e18f69
  140. pos := strings.LastIndex(cgroupPath, "/")
  141. if pos > 0 {
  142. idLen := len(cgroupPath) - pos - 1
  143. if idLen == 64 {
  144. // docker id
  145. containerID = cgroupPath[pos+1 : pos+1+64]
  146. return containerID
  147. }
  148. }
  149. // systemd Docker
  150. // 5:net_cls:/system.slice/docker-afd862d2ed48ef5dc0ce8f1863e4475894e331098c9a512789233ca9ca06fc62.scope
  151. dockerStr := "docker-"
  152. pos = strings.Index(cgroupPath, dockerStr)
  153. if pos > 0 {
  154. posScope := strings.Index(cgroupPath, ".scope")
  155. idLen := posScope - pos - len(dockerStr)
  156. if posScope > 0 && idLen == 64 {
  157. containerID = cgroupPath[pos+len(dockerStr) : pos+len(dockerStr)+64]
  158. return containerID
  159. }
  160. }
  161. }
  162. return containerID
  163. }
  164. type flushWriter struct {
  165. f http.Flusher
  166. w io.Writer
  167. }
  168. func (fw *flushWriter) Write(p []byte) (n int, err error) {
  169. n, err = fw.w.Write(p)
  170. if fw.f != nil {
  171. fw.f.Flush()
  172. }
  173. return
  174. }