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  }