// handleSessionExit processes the result from the session command, records it in persistent // maner and determines if the Executor should exit func (t *tether) handleSessionExit(session *SessionConfig) { defer trace.End(trace.Begin("handling exit of session " + session.ID)) session.Lock() defer session.Unlock() if session.wait != nil { session.wait.Wait() } // reader must be closed before calling wait, and this must interrupt the underlying Read // or Wait will not return. session.Reader.Close() session.Cmd.Wait() // close down the outputs session.Outwriter.Close() session.Errwriter.Close() // close the signaling channel (it is nil for detached sessions) and set it to nil (for restart) if session.ClearToLaunch != nil { close(session.ClearToLaunch) session.ClearToLaunch = nil } // Remove associated PID file cmdname := path.Base(session.Cmd.Path) _ = os.Remove(fmt.Sprintf("%s.pid", path.Join(PIDFileDir(), cmdname))) // set the stop time session.StopTime = time.Now().UTC().Unix() // this returns an arbitrary closure for invocation after the session status update f := t.ops.HandleSessionExit(t.config, session) // FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not // currently sure how to expose it neatly via a utility function extraconfig.EncodeWithPrefix(t.sink, session, fmt.Sprintf("guestinfo.vice..sessions|%s", session.ID)) if f != nil { f() } }
// handleSessionExit processes the result from the session command, records it in persistent // maner and determines if the Executor should exit func (t *tether) handleSessionExit(session *SessionConfig) { defer trace.End(trace.Begin("handling exit of session " + session.ID)) // close down the IO session.Reader.Close() // live.outwriter.Close() // live.errwriter.Close() // flush session log output // record exit status // FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not // currently sure how to expose it neatly via a utility function extraconfig.EncodeWithPrefix(t.sink, session.ExitStatus, fmt.Sprintf("guestinfo..sessions|%s.status", session.ID)) log.Infof("%s exit code: %d", session.ID, session.ExitStatus) if t.ops.HandleSessionExit(t.config, session) { t.Stop() } }
// handleSessionExit processes the result from the session command, records it in persistent // maner and determines if the Executor should exit func (t *tether) handleSessionExit(session *SessionConfig) { defer trace.End(trace.Begin("handling exit of session " + session.ID)) // stdio must be closed before calling wait or Wait hangs indefinitely session.Reader.Close() session.Cmd.Wait() // close down the outputs session.Outwriter.Close() session.Errwriter.Close() // Log a death record trimming records if need be logs := session.Diagnostics.ExitLogs logCount := len(logs) if logCount >= MaxDeathRecords { logs = logs[logCount-MaxDeathRecords+1:] } session.Diagnostics.ExitLogs = append(logs, executor.ExitLog{ Time: time.Now(), ExitStatus: session.ExitStatus, // We don't have any message for now }) // this returns an arbitrary closure for invocation after the session status update f := t.ops.HandleSessionExit(t.config, session) log.Infof("%s exit code: %d", session.ID, session.ExitStatus) // record exit status // FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not // currently sure how to expose it neatly via a utility function extraconfig.EncodeWithPrefix(t.sink, session, fmt.Sprintf("guestinfo..sessions|%s", session.ID)) if f != nil { f() } }
// launch will launch the command defined in the session. // This will return an error if the session fails to launch func (t *tether) launch(session *SessionConfig) error { defer trace.End(trace.Begin("launching session " + session.ID)) // encode the result whether success or error defer func() { extraconfig.EncodeWithPrefix(t.sink, session, fmt.Sprintf("guestinfo.vice..sessions|%s", session.ID)) }() session.Lock() defer session.Unlock() // Special case here because UID/GID lookup need to be done // on the appliance... if len(session.User) > 0 { session.Cmd.SysProcAttr = getUserSysProcAttr(session.User) } session.Cmd.Env = t.ops.ProcessEnv(session.Cmd.Env) session.Cmd.Stdout = session.Outwriter session.Cmd.Stderr = session.Errwriter session.Cmd.Stdin = session.Reader resolved, err := lookPath(session.Cmd.Path, session.Cmd.Env, session.Cmd.Dir) if err != nil { log.Errorf("Path lookup failed for %s: %s", session.Cmd.Path, err) session.Started = err.Error() return err } log.Debugf("Resolved %s to %s", session.Cmd.Path, resolved) session.Cmd.Path = resolved // block until we have a connection if session.RunBlock && session.ClearToLaunch != nil { log.Debugf("Waiting clear signal to launch %s", session.ID) select { case <-t.ctx.Done(): log.Warnf("Waiting to launch %s canceled, bailing out", session.ID) return nil case <-session.ClearToLaunch: log.Debugf("Received the clear signal to launch %s", session.ID) } } pid := 0 // Use the mutex to make creating a child and adding the child pid into the // childPidTable appear atomic to the reaper function. Use a anonymous function // so we can defer unlocking locally // logging is done after the function to keep the locked time as low as possible err = func() error { t.config.pidMutex.Lock() defer t.config.pidMutex.Unlock() if !session.Tty { err = session.Cmd.Start() } else { err = establishPty(session) } if err != nil { return err } pid = session.Cmd.Process.Pid t.config.pids[pid] = session return nil }() if err != nil { detail := fmt.Sprintf("failed to start container process: %s", err) log.Error(detail) // Set the Started key to the undecorated error message session.Started = err.Error() return errors.New(detail) } // Set the Started key to "true" - this indicates a successful launch session.Started = "true" // Write the PID to the associated PID file cmdname := path.Base(session.Cmd.Path) err = ioutil.WriteFile(fmt.Sprintf("%s.pid", path.Join(PIDFileDir(), cmdname)), []byte(fmt.Sprintf("%d", pid)), 0644) if err != nil { log.Errorf("Unable to write PID file for %s: %s", cmdname, err) } log.Debugf("Launched command with pid %d", pid) return nil }
func (t *tether) processSessions() error { type results struct { id string path string err error } // so that we can launch multiple sessions in parallel var wg sync.WaitGroup // to collect the errors back from them resultsCh := make(chan results, len(t.config.Sessions)) // process the sessions and launch if needed for id, session := range t.config.Sessions { session.Lock() log.Debugf("Processing config for session %s", id) var proc = session.Cmd.Process // check if session is alive and well if proc != nil && proc.Signal(syscall.Signal(0)) == nil { log.Debugf("Process for session %s is already running (pid: %d)", id, proc.Pid) session.Unlock() continue } // check if session has never been started or is configured for restart if proc == nil || session.Restart { if proc == nil { log.Infof("Launching process for session %s", id) } else { session.Diagnostics.ResurrectionCount++ // FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not // currently sure how to expose it neatly via a utility function extraconfig.EncodeWithPrefix(t.sink, session, fmt.Sprintf("guestinfo.vice..sessions|%s", id)) log.Warnf("Re-launching process for session %s (count: %d)", id, session.Diagnostics.ResurrectionCount) session.Cmd = *restartableCmd(&session.Cmd) } session.Unlock() wg.Add(1) go func(session *SessionConfig) { defer wg.Done() resultsCh <- results{ id: session.ID, path: session.Cmd.Path, err: t.launch(session), } }(session) } } wg.Wait() // close the channel close(resultsCh) // iterate over the results for result := range resultsCh { if result.err != nil { detail := fmt.Errorf("failed to launch %s for %s: %s", result.path, result.id, result.err) return detail } } return nil }
// launch will launch the command defined in the session. // This will return an error if the session fails to launch func (t *tether) launch(session *SessionConfig) error { defer trace.End(trace.Begin("launching session " + session.ID)) // encode the result whether success or error defer func() { extraconfig.EncodeWithPrefix(t.sink, session.Started, fmt.Sprintf("guestinfo..sessions|%s.started", session.ID)) }() logwriter, err := t.ops.SessionLog(session) if err != nil { detail := fmt.Sprintf("failed to get log writer for session: %s", err) log.Error(detail) session.Started = detail return errors.New(detail) } // we store these outside of the session.Cmd struct so that there's consistent // handling between tty & non-tty paths session.Outwriter = logwriter session.Errwriter = logwriter session.Reader = dio.MultiReader() session.Cmd.Env = t.ops.ProcessEnv(session.Cmd.Env) session.Cmd.Stdout = session.Outwriter session.Cmd.Stderr = session.Errwriter session.Cmd.Stdin = session.Reader resolved, err := lookPath(session.Cmd.Path, session.Cmd.Env, session.Cmd.Dir) if err != nil { log.Errorf("Path lookup failed for %s: %s", session.Cmd.Path, err) session.Started = err.Error() return err } log.Debugf("Resolved %s to %s", session.Cmd.Path, resolved) session.Cmd.Path = resolved // Use the mutex to make creating a child and adding the child pid into the // childPidTable appear atomic to the reaper function. Use a anonymous function // so we can defer unlocking locally // logging is done after the function to keep the locked time as low as possible err = func() error { t.config.pidMutex.Lock() defer t.config.pidMutex.Unlock() log.Infof("Launching command %#v", session.Cmd.Args) if !session.Tty { err = session.Cmd.Start() } else { err = establishPty(session) } if err != nil { return err } // ChildReaper will use this channel to inform us the wait status of the child. t.config.pids[session.Cmd.Process.Pid] = session return nil }() if err != nil { detail := fmt.Sprintf("failed to start container process: %s", err) log.Error(detail) // Set the Started key to the undecorated error message session.Started = err.Error() return errors.New(detail) } // Set the Started key to "true" - this indicates a successful launch session.Started = "true" log.Debugf("Launched command with pid %d", session.Cmd.Process.Pid) return nil }
func (t *tether) Start() error { defer trace.End(trace.Begin("main tether loop")) t.setup() defer t.cleanup() // initial entry, so seed this t.reload <- true for _ = range t.reload { log.Info("Loading main configuration") // load the config - this modifies the structure values in place extraconfig.Decode(t.src, t.config) logConfig(t.config) short := t.config.ID if len(short) > shortLen { short = short[:shortLen] } if err := t.ops.SetHostname(short, t.config.Name); err != nil { detail := fmt.Sprintf("failed to set hostname: %s", err) log.Error(detail) // we don't attempt to recover from this - it's a fundemental misconfiguration // so just exit return errors.New(detail) } // process the networks then publish any dynamic data for _, v := range t.config.Networks { if err := t.ops.Apply(v); err != nil { detail := fmt.Sprintf("failed to apply network endpoint config: %s", err) log.Error(detail) return errors.New(detail) } } extraconfig.Encode(t.sink, t.config) //process the filesystem mounts - this is performed after networks to allow for network mounts for k, v := range t.config.Mounts { if v.Source.Scheme != "label" { detail := fmt.Sprintf("unsupported volume mount type for %s: %s", k, v.Source.Scheme) log.Error(detail) return errors.New(detail) } // this could block indefinitely while waiting for a volume to present t.ops.MountLabel(context.Background(), v.Source.Path, v.Path) } // process the sessions and launch if needed for id, session := range t.config.Sessions { log.Debugf("Processing config for session %s", session.ID) var proc = session.Cmd.Process // check if session is alive and well if proc != nil && proc.Signal(syscall.Signal(0)) == nil { log.Debugf("Process for session %s is already running (pid: %d)", session.ID, proc.Pid) continue } // check if session has never been started or is configured for restart if proc == nil || session.Restart { if proc == nil { log.Infof("Launching process for session %s", session.ID) } else { session.Diagnostics.ResurrectionCount++ // FIXME: we cannot have this embedded knowledge of the extraconfig encoding pattern, but not // currently sure how to expose it neatly via a utility function extraconfig.EncodeWithPrefix(t.sink, session, fmt.Sprintf("guestinfo..sessions|%s", session.ID)) log.Warnf("Re-launching process for session %s (count: %d)", session.ID, session.Diagnostics.ResurrectionCount) session.Cmd = *restartableCmd(&session.Cmd) } err := t.launch(session) if err != nil { detail := fmt.Sprintf("failed to launch %s for %s: %s", session.Cmd.Path, id, err) log.Error(detail) // TODO: check if failure to launch this is fatal to everything in this containerVM // for now failure to launch at all is terminal return errors.New(detail) } continue } log.Warnf("Process for session %s has exited (%d) and is not configured for restart", session.ID, session.ExitStatus) } for name, ext := range t.extensions { log.Info("Passing config to " + name) err := ext.Reload(t.config) if err != nil { log.Errorf("Failed to cleanly reload config for extension %s: %s", name, err) return err } } } return nil }