blob: 1619ec6206a9e100077df938e8a4c3cc421bf8cc [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wrapper
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"runtime/debug"
"syscall"
"github.com/golang/glog"
)
// Invocation encapsulates the parameters and execution logic for wrapping a command.
type Invocation struct {
Command []string
SignalPolicy SignalPolicy
// Env provides additional environment variables for the child process.
Env []string
// BaseEnv provides the base environment for the child process.
BaseEnv []string
// Sigs is a channel for receiving OS signals to be forwarded.
Sigs <-chan os.Signal
// Stdout and Stderr are the streams to which the wrapped command's output
// will be written.
Stdout io.Writer
Stderr io.Writer
}
// PopulateSystemDefaults fills missing fields with OS standard globals.
// This is a helper for CLI applications that want to use standard OS streams.
func (i *Invocation) PopulateSystemDefaults() {
if i.Stdout == nil {
i.Stdout = os.Stdout
}
if i.Stderr == nil {
i.Stderr = os.Stderr
}
if i.BaseEnv == nil {
i.BaseEnv = os.Environ()
}
if i.SignalPolicy == "" {
i.SignalPolicy = PolicyRelay
}
}
// Run executes the command according to the specified policy.
// It handles process isolation, signal forwarding, and exit code propagation.
// Callers should ensure that Stdout, Stderr, and BaseEnv are set, or call
// PopulateSystemDefaults() first.
func (i *Invocation) Run(ctx context.Context) int {
if len(i.Command) == 0 {
return 0
}
cmd := exec.CommandContext(ctx, i.Command[0], i.Command[1:]...)
cmd.Stdout = i.Stdout
cmd.Stderr = i.Stderr
cmd.Env = append(append([]string{}, i.BaseEnv...), i.Env...)
// Process isolation: run the command in its own process group unless passive mode is used.
if i.SignalPolicy != PolicyPassive {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
if err := cmd.Start(); err != nil {
glog.Errorf("Failed to start wrapped command: %v", err)
return 1
}
// Forward signals to the wrapped command.
done := make(chan struct{})
go func() {
// The recover() block guards against unrecovered panics in this background
// goroutine, which in Go results in an immediate and silent process
// termination with exit code 2. This ensures that any runtime failures
// (e.g., unexpected signal states or nil pointer dereferences) are
// captured in the logs with a full stack trace before the process exits.
defer func() {
if r := recover(); r != nil {
glog.Errorf("[wrapper] FATAL PANIC in signal forwarder: %v\nStack trace:\n%s", r, debug.Stack())
glog.Flush()
os.Exit(3) // Distinct code for internal panic
}
}()
for {
select {
case sig, ok := <-i.Sigs:
if !ok {
return
}
if i.SignalPolicy == PolicyPassive {
continue
}
s, ok := sig.(syscall.Signal)
if !ok {
glog.V(1).Infof("Received non-syscall signal %v, skipping forwarding", sig)
continue
}
if cmd.Process == nil {
glog.V(1).Infof("Process not initialized, cannot forward signal %v", sig)
continue
}
target := cmd.Process.Pid
if i.SignalPolicy == PolicyRelayGroup {
target = -target
}
fmt.Fprintf(os.Stderr, "[wrapper] Forwarding signal %v to target %v\n", sig, target)
if err := syscall.Kill(target, s); err != nil {
// It's possible the process already exited between reading from
// the channel and calling kill.
glog.V(1).Infof("Failed to forward signal %v to target %v: %v", sig, target, err)
}
case <-ctx.Done():
glog.V(1).Infof("[wrapper] Context done, stopping signal forwarder")
return
case <-done:
return
}
}
}()
var exitCode int
if err := cmd.Wait(); err != nil {
glog.Infof("Wrapped command finished with error: %v", err)
if exitError, ok := err.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
} else {
// e.g. if the command was terminated by a signal.
exitCode = -1
}
} else {
glog.Infof("Wrapped command finished successfully.")
}
close(done)
return exitCode
}