github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap-recovery-chooser/main.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 // The `snap-recovery-chooser` acts as a proxy between the chooser UI process 21 // and the actual snapd daemon. 22 // 23 // It obtains the list of seed systems and their actions from the snapd API and 24 // passed that directly to the standard input of the UI process. The UI process 25 // is expected to present the list of options to the user and print out a JSON 26 // object with the choice to its standard output. 27 // 28 // The JSON object carrying the list of systems is the client.ChooserSystems 29 // structure. The response is defined as follows: 30 // { 31 // "label": "<system-label", 32 // "action": {} // client.SystemAction object 33 // } 34 // 35 // No action is forwarded to snapd if the chooser UI exits with an error code or 36 // the response structure is invalid. 37 // 38 package main 39 40 import ( 41 "bytes" 42 "encoding/json" 43 "fmt" 44 "io" 45 "log/syslog" 46 "os" 47 "os/exec" 48 "path/filepath" 49 "syscall" 50 51 "github.com/snapcore/snapd/client" 52 "github.com/snapcore/snapd/dirs" 53 "github.com/snapcore/snapd/logger" 54 ) 55 56 var ( 57 // default marker file location 58 defaultMarkerFile = "/run/snapd-recovery-chooser-triggered" 59 60 Stdout io.Writer = os.Stdout 61 Stderr io.Writer = os.Stderr 62 63 chooserTool = consoleConfWrapperUITool 64 ) 65 66 // consoleConfWrapperUITool returns a hardcoded path to the console conf wrapper 67 func consoleConfWrapperUITool() (*exec.Cmd, error) { 68 tool := filepath.Join(dirs.GlobalRootDir, "usr/bin/console-conf") 69 70 if _, err := os.Stat(tool); err != nil { 71 if os.IsNotExist(err) { 72 return nil, fmt.Errorf("chooser UI tool %q does not exist", tool) 73 } 74 return nil, fmt.Errorf("cannot stat UI tool binary: %v", err) 75 } 76 return exec.Command(tool, "--recovery-chooser-mode"), nil 77 } 78 79 // ChooserSystems carries the list of available recovery systems 80 type ChooserSystems struct { 81 Systems []client.System `json:"systems,omitempty"` 82 } 83 84 func outputForUI(out io.Writer, sys *ChooserSystems) error { 85 enc := json.NewEncoder(out) 86 if err := enc.Encode(sys); err != nil { 87 return fmt.Errorf("cannot serialize chooser options: %v", err) 88 } 89 return nil 90 } 91 92 // Response is sent by the UI tool and contains the choice made by the user 93 type Response struct { 94 Label string `json:"label"` 95 Action client.SystemAction `json:"action"` 96 } 97 98 func runUI(cmd *exec.Cmd, sys *ChooserSystems) (rsp *Response, err error) { 99 var asBytes bytes.Buffer 100 if err := outputForUI(&asBytes, sys); err != nil { 101 return nil, err 102 } 103 104 logger.Noticef("spawning UI") 105 // the UI uses the same tty as current process 106 cmd.Stdin = &asBytes 107 // reuse stderr 108 cmd.Stderr = os.Stderr 109 // the chooser may be invoked via console-conf service which uses 110 // KillMethod=process, so make sure the UI process dies when we die 111 cmd.SysProcAttr = &syscall.SysProcAttr{ 112 Pdeathsig: syscall.SIGKILL, 113 } 114 115 out, err := cmd.Output() 116 if err != nil { 117 return nil, fmt.Errorf("cannot collect output of the UI process: %v", err) 118 } 119 120 logger.Noticef("UI completed") 121 122 var resp Response 123 dec := json.NewDecoder(bytes.NewBuffer(out)) 124 if err := dec.Decode(&resp); err != nil { 125 return nil, fmt.Errorf("cannot decode response: %v", err) 126 } 127 return &resp, nil 128 } 129 130 func cleanupTriggerMarker() error { 131 if err := os.Remove(defaultMarkerFile); err != nil && !os.IsNotExist(err) { 132 return err 133 } 134 return nil 135 } 136 137 func chooser(cli *client.Client) (reboot bool, err error) { 138 if _, err := os.Stat(defaultMarkerFile); err != nil { 139 if os.IsNotExist(err) { 140 return false, fmt.Errorf("cannot run chooser without the marker file") 141 } else { 142 return false, fmt.Errorf("cannot check the marker file: %v", err) 143 } 144 } 145 // consume the trigger file 146 defer cleanupTriggerMarker() 147 148 systems, err := cli.ListSystems() 149 if err != nil { 150 return false, err 151 } 152 153 systemsForUI := &ChooserSystems{ 154 Systems: systems, 155 } 156 157 uiTool, err := chooserTool() 158 if err != nil { 159 return false, fmt.Errorf("cannot locate the chooser UI tool: %v", err) 160 } 161 162 response, err := runUI(uiTool, systemsForUI) 163 if err != nil { 164 return false, fmt.Errorf("UI process failed: %v", err) 165 } 166 167 logger.Noticef("got response: %+v", response) 168 169 if err := cli.DoSystemAction(response.Label, &response.Action); err != nil { 170 return false, fmt.Errorf("cannot request system action: %v", err) 171 } 172 if maintErr, ok := cli.Maintenance().(*client.Error); ok && maintErr.Kind == client.ErrorKindSystemRestart { 173 reboot = true 174 } 175 return reboot, nil 176 } 177 178 var syslogNew = func(p syslog.Priority, tag string) (io.Writer, error) { return syslog.New(p, tag) } 179 180 func loggerWithSyslogMaybe() error { 181 maybeSyslog := func() error { 182 if os.Getenv("TERM") == "" { 183 // set up the syslog logger only when we're running on a 184 // terminal 185 return fmt.Errorf("not on terminal, syslog not needed") 186 } 187 syslogWriter, err := syslogNew(syslog.LOG_INFO|syslog.LOG_DAEMON, "snap-recovery-chooser") 188 if err != nil { 189 return err 190 } 191 l, err := logger.New(syslogWriter, logger.DefaultFlags) 192 if err != nil { 193 return err 194 } 195 logger.SetLogger(l) 196 return nil 197 } 198 199 if err := maybeSyslog(); err != nil { 200 // try simple setup 201 return logger.SimpleSetup() 202 } 203 return nil 204 } 205 206 func main() { 207 if err := loggerWithSyslogMaybe(); err != nil { 208 fmt.Fprintf(Stderr, "cannot initialize logger: %v\n", err) 209 os.Exit(1) 210 } 211 212 reboot, err := chooser(client.New(nil)) 213 if err != nil { 214 logger.Noticef("cannot run recovery chooser: %v", err) 215 fmt.Fprintf(Stderr, "%v\n", err) 216 os.Exit(1) 217 } 218 if reboot { 219 fmt.Fprintf(Stderr, "The system is rebooting...\n") 220 } 221 }