github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/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 }