Exemple #1
0
// 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()
	}
}
Exemple #2
0
// 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()
	}
}
Exemple #3
0
// 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()
	}
}
Exemple #4
0
// 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
}
Exemple #5
0
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
}
Exemple #6
0
// 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
}
Exemple #7
0
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
}