blob: 1c47e15f32271e4abdd72c6b5a534ef43e2e2c65 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package impl
// TODO -- Ideally the code in this file would be integrated with the instance reaping,
// so we avoid having two process polling loops. This code is currently separate because
// the actions taken when the app dies (or is caught lying about its pid) prior to being
// considered running are fairly different from what's currently done by the reaper in
// handling deaths that occur after the app started successfully.
import (
"encoding/binary"
"fmt"
"os"
"os/exec"
"syscall"
"time"
"v.io/v23/context"
"v.io/v23/verror"
"v.io/x/lib/vlog"
vexec "v.io/x/ref/lib/exec"
"v.io/x/ref/services/device/internal/suid"
)
// appWatcher watches the pid of a running app until either the pid exits or stop()
// is called
type appWatcher struct {
pid int // Pid to watch
callback func() // Called if the pid exits or if stop() is invoked
stopper chan struct{} // Used to stop the appWatcher
}
func newAppWatcher(pidToWatch int, callOnPidExit func()) *appWatcher {
return &appWatcher{
pid: pidToWatch,
callback: callOnPidExit,
stopper: make(chan struct{}, 1),
}
}
func (a *appWatcher) stop() {
close(a.stopper)
}
func (a *appWatcher) watchAppPid() {
defer a.callback()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := syscall.Kill(a.pid, 0); err != nil && err != syscall.EPERM {
vlog.Errorf("App died in startup: pid=%d: %v", a.pid, err)
return
} else {
vlog.VI(2).Infof("App pid %d is alive", a.pid)
}
case <-a.stopper:
vlog.Errorf("AppWatcher was stopped")
return
}
}
// Not reached.
}
// appHandshaker is a utility to do the app handshake for a newly started app while
// reacting quickly if the app crashes. appHandshaker reads two pids from the app (one
// from the helper that forked the app, and the other from the app itself). If the app
// appears to be lying about its own pid, it will kill the app.
type appHandshaker struct {
helperRead, helperWrite *os.File
ctx *context.T
}
func (a *appHandshaker) cleanup() {
if a.helperRead != nil {
a.helperRead.Close()
a.helperRead = nil
}
if a.helperWrite != nil {
a.helperWrite.Close()
a.helperWrite = nil
}
}
// prepareToStart sets up the pipe used to talk to the helper. It must be called before
// the app is started so that the app will inherit the file descriptor
func (a *appHandshaker) prepareToStart(ctx *context.T, cmd *exec.Cmd) error {
if suid.PipeToParentFD != (len(cmd.ExtraFiles) + vexec.FileOffset) {
return verror.New(ErrOperationFailed, ctx,
fmt.Sprintf("FD expected by helper (%v) was not available (%v) (%v)",
suid.PipeToParentFD, len(cmd.ExtraFiles), vexec.FileOffset))
}
a.ctx = ctx
var err error
a.helperRead, a.helperWrite, err = os.Pipe()
if err != nil {
vlog.Errorf("Failed to create pipe: %v", err)
return err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, a.helperWrite)
return nil
}
// doAppHandshake executes the startup handshake for the app. Upon success, it returns the
// pid and appCycle manager name for the started app.
//
// handle should have been set up to use a helper for the app and handle.Start()
// and handle.Wait() should already have been called (so we know the helper is done)
func (a *appHandshaker) doHandshake(handle *vexec.ParentHandle, listener callbackListener) (int, string, error) {
// Close our copy of helperWrite to make helperRead return EOF once the
// helper's copy of helperWrite is closed.
a.helperWrite.Close()
a.helperWrite = nil
// Get the app pid from the helper. This won't block as the helper is done
var pid32 int32
if err := binary.Read(a.helperRead, binary.LittleEndian, &pid32); err != nil {
vlog.Errorf("Error reading app pid from child: %v", err)
return 0, "", verror.New(ErrOperationFailed, a.ctx, fmt.Sprintf("failed to read pid from helper: %v", err))
}
pidFromHelper := int(pid32)
vlog.VI(1).Infof("read app pid %v from child", pidFromHelper)
// Watch the app pid in case it exits.
pidExitedChan := make(chan struct{}, 1)
watcher := newAppWatcher(pidFromHelper, func() {
listener.stop()
close(pidExitedChan)
})
go watcher.watchAppPid()
defer watcher.stop()
// Wait for the child to say it's ready and provide its own pid via the init handshake
childReadyErrChan := make(chan error, 1)
go func() {
if err := handle.WaitForReady(childReadyTimeout); err != nil {
childReadyErrChan <- verror.New(ErrOperationFailed, a.ctx, fmt.Sprintf("WaitForReady(%v) failed: %v", childReadyTimeout, err))
}
childReadyErrChan <- nil
}()
// Wait until we get the pid from the app, but return early if
// the watcher notices that the app failed
pidFromChild := 0
select {
case <-pidExitedChan:
return 0, "", verror.New(ErrOperationFailed, a.ctx,
fmt.Sprintf("App exited (pid %d)", pidFromHelper))
case err := <-childReadyErrChan:
if err != nil {
return 0, "", err
}
// Note: handle.Pid() is the pid of the helper, rather than that
// of the app that the helper then forked. ChildPid is the pid
// received via the app startup handshake
pidFromChild = handle.ChildPid()
}
if pidFromHelper != pidFromChild {
// Something nasty is going on (the child may be lying).
suidHelper.terminatePid(pidFromHelper, nil, nil)
return 0, "", verror.New(ErrOperationFailed, a.ctx,
fmt.Sprintf("Child pids do not match! (%d != %d)", pidFromHelper, pidFromChild))
}
// The appWatcher will stop the listener if the pid dies while waiting below
childName, err := listener.waitForValue(childReadyTimeout)
if err != nil {
suidHelper.terminatePid(pidFromHelper, nil, nil)
return 0, "", verror.New(ErrOperationFailed, a.ctx,
fmt.Sprintf("Waiting for child name: %v", err))
}
return pidFromHelper, childName, nil
}