github.com/huiliang/nomad@v0.2.1-0.20151124023127-7a8b664699ff/command/spawn_daemon.go (about)

     1  package command
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"strconv"
    10  	"strings"
    11  	"syscall"
    12  )
    13  
    14  type SpawnDaemonCommand struct {
    15  	Meta
    16  	config   *DaemonConfig
    17  	exitFile io.WriteCloser
    18  }
    19  
    20  func (c *SpawnDaemonCommand) Help() string {
    21  	helpText := `
    22  Usage: nomad spawn-daemon [options] <daemon_config>
    23  
    24    INTERNAL ONLY
    25  
    26    Spawns a daemon process by double forking. The required daemon_config is a
    27    json encoding of the DaemonConfig struct containing the isolation
    28    configuration and command to run. SpawnStartStatus is json serialized to
    29    stdout upon running the user command or if any error prevents its execution.
    30    If there is no error, the process waits on the users command. Once the user
    31    command exits, the exit code is written to a file specified in the
    32    daemon_config and this process exits with the same exit status as the user
    33    command.
    34    `
    35  
    36  	return strings.TrimSpace(helpText)
    37  }
    38  
    39  func (c *SpawnDaemonCommand) Synopsis() string {
    40  	return "Spawn a daemon command with configurable isolation."
    41  }
    42  
    43  // Status of executing the user's command.
    44  type SpawnStartStatus struct {
    45  	// The PID of the user's command.
    46  	UserPID int
    47  
    48  	// ErrorMsg will be empty if the user command was started successfully.
    49  	// Otherwise it will have an error message.
    50  	ErrorMsg string
    51  }
    52  
    53  // Exit status of the user's command.
    54  type SpawnExitStatus struct {
    55  	// The exit code of the user's command.
    56  	ExitCode int
    57  }
    58  
    59  // Configuration for the command to start as a daemon.
    60  type DaemonConfig struct {
    61  	exec.Cmd
    62  
    63  	// The filepath to write the exit status to.
    64  	ExitStatusFile string
    65  
    66  	// The paths, if not /dev/null, must be either in the tasks root directory
    67  	// or in the shared alloc directory.
    68  	StdoutFile string
    69  	StdinFile  string
    70  	StderrFile string
    71  
    72  	// An optional path specifying the directory to chroot the process in.
    73  	Chroot string
    74  }
    75  
    76  // Whether to start the user command or abort.
    77  type TaskStart bool
    78  
    79  // parseConfig reads the DaemonConfig from the passed arguments. If not
    80  // successful, an error is returned.
    81  func (c *SpawnDaemonCommand) parseConfig(args []string) (*DaemonConfig, error) {
    82  	flags := c.Meta.FlagSet("spawn-daemon", FlagSetClient)
    83  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    84  	if err := flags.Parse(args); err != nil {
    85  		return nil, fmt.Errorf("failed to parse args: %v", err)
    86  	}
    87  
    88  	// Check that we got json input.
    89  	args = flags.Args()
    90  	if len(args) != 1 {
    91  		return nil, fmt.Errorf("incorrect number of args; got %v; want 1", len(args))
    92  	}
    93  	jsonInput, err := strconv.Unquote(args[0])
    94  	if err != nil {
    95  		return nil, fmt.Errorf("Failed to unquote json input: %v", err)
    96  	}
    97  
    98  	// De-serialize the passed command.
    99  	var config DaemonConfig
   100  	dec := json.NewDecoder(strings.NewReader(jsonInput))
   101  	if err := dec.Decode(&config); err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	return &config, nil
   106  }
   107  
   108  // configureLogs creates the log files and redirects the process
   109  // stdin/stderr/stdout to them. If unsuccessful, an error is returned.
   110  func (c *SpawnDaemonCommand) configureLogs() error {
   111  	if len(c.config.StdoutFile) != 0 {
   112  		stdo, err := os.OpenFile(c.config.StdoutFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
   113  		if err != nil {
   114  			return fmt.Errorf("Error opening file to redirect stdout: %v", err)
   115  		}
   116  
   117  		c.config.Cmd.Stdout = stdo
   118  	}
   119  
   120  	if len(c.config.StderrFile) != 0 {
   121  		stde, err := os.OpenFile(c.config.StderrFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
   122  		if err != nil {
   123  			return fmt.Errorf("Error opening file to redirect stderr: %v", err)
   124  		}
   125  		c.config.Cmd.Stderr = stde
   126  	}
   127  
   128  	if len(c.config.StdinFile) != 0 {
   129  		stdi, err := os.OpenFile(c.config.StdinFile, os.O_CREATE|os.O_RDONLY, 0666)
   130  		if err != nil {
   131  			return fmt.Errorf("Error opening file to redirect stdin: %v", err)
   132  		}
   133  		c.config.Cmd.Stdin = stdi
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  func (c *SpawnDaemonCommand) Run(args []string) int {
   140  	var err error
   141  	c.config, err = c.parseConfig(args)
   142  	if err != nil {
   143  		return c.outputStartStatus(err, 1)
   144  	}
   145  
   146  	// Open the file we will be using to write exit codes to. We do this early
   147  	// to ensure that we don't start the user process when we can't capture its
   148  	// exit status.
   149  	c.exitFile, err = os.OpenFile(c.config.ExitStatusFile, os.O_WRONLY, 0666)
   150  	if err != nil {
   151  		return c.outputStartStatus(fmt.Errorf("Error opening file to store exit status: %v", err), 1)
   152  	}
   153  
   154  	// Isolate the user process.
   155  	if err := c.isolateCmd(); err != nil {
   156  		return c.outputStartStatus(err, 1)
   157  	}
   158  
   159  	// Redirect logs.
   160  	if err := c.configureLogs(); err != nil {
   161  		return c.outputStartStatus(err, 1)
   162  	}
   163  
   164  	// Chroot jail the process and set its working directory.
   165  	c.configureChroot()
   166  
   167  	// Wait to get the start command.
   168  	var start TaskStart
   169  	dec := json.NewDecoder(os.Stdin)
   170  	if err := dec.Decode(&start); err != nil {
   171  		return c.outputStartStatus(err, 1)
   172  	}
   173  
   174  	// Aborted by Nomad process.
   175  	if !start {
   176  		return 0
   177  	}
   178  
   179  	// Spawn the user process.
   180  	if err := c.config.Cmd.Start(); err != nil {
   181  		return c.outputStartStatus(fmt.Errorf("Error starting user command: %v", err), 1)
   182  	}
   183  
   184  	// Indicate that the command was started successfully.
   185  	c.outputStartStatus(nil, 0)
   186  
   187  	// Wait and then output the exit status.
   188  	return c.writeExitStatus(c.config.Cmd.Wait())
   189  }
   190  
   191  // outputStartStatus is a helper function that outputs a SpawnStartStatus to
   192  // Stdout with the passed error, which may be nil to indicate no error. It
   193  // returns the passed status.
   194  func (c *SpawnDaemonCommand) outputStartStatus(err error, status int) int {
   195  	startStatus := &SpawnStartStatus{}
   196  	enc := json.NewEncoder(os.Stdout)
   197  
   198  	if err != nil {
   199  		startStatus.ErrorMsg = err.Error()
   200  	}
   201  
   202  	if c.config != nil && c.config.Cmd.Process != nil {
   203  		startStatus.UserPID = c.config.Process.Pid
   204  	}
   205  
   206  	enc.Encode(startStatus)
   207  	return status
   208  }
   209  
   210  // writeExitStatus takes in the error result from calling wait and writes out
   211  // the exit status to a file. It returns the same exit status as the user
   212  // command.
   213  func (c *SpawnDaemonCommand) writeExitStatus(exit error) int {
   214  	// Parse the exit code.
   215  	exitStatus := &SpawnExitStatus{}
   216  	if exit != nil {
   217  		// Default to exit code 1 if we can not get the actual exit code.
   218  		exitStatus.ExitCode = 1
   219  
   220  		if exiterr, ok := exit.(*exec.ExitError); ok {
   221  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   222  				exitStatus.ExitCode = status.ExitStatus()
   223  			}
   224  		}
   225  	}
   226  
   227  	if c.exitFile != nil {
   228  		enc := json.NewEncoder(c.exitFile)
   229  		enc.Encode(exitStatus)
   230  		c.exitFile.Close()
   231  	}
   232  
   233  	return exitStatus.ExitCode
   234  }