apparmor.d/pkg/paths/process.go
2024-10-05 23:03:41 +01:00

227 lines
7.9 KiB
Go

//
// This file is part of PathsHelper library.
//
// Copyright 2023 Arduino AG (http://www.arduino.cc/)
//
// PathsHelper library is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
//
// As a special exception, you may use this file as part of a free software
// library without restriction. Specifically, if other files instantiate
// templates or use macros or inline functions from this file, or you compile
// this file and link it with other files to produce an executable, this
// file does not by itself cause the resulting executable to be covered by
// the GNU General Public License. This exception does not however
// invalidate any other reasons why the executable file might be covered by
// the GNU General Public License.
//
package paths
import (
"bytes"
"context"
"errors"
"io"
"os"
"os/exec"
)
// Process is representation of an external process run
type Process struct {
cmd *exec.Cmd
}
// NewProcess creates a command with the provided command line arguments
// and environment variables (that will be added to the parent os.Environ).
// The argument args[0] is the path to the executable, the remainder are the
// arguments to the command.
func NewProcess(extraEnv []string, args ...string) (*Process, error) {
if len(args) == 0 {
return nil, errors.New("no executable specified")
}
p := &Process{
cmd: exec.Command(args[0], args[1:]...),
}
p.cmd.Env = append(os.Environ(), extraEnv...)
tellCommandNotToSpawnShell(p.cmd) // windows specific
tellCommandToStartOnNewProcessGroup(p.cmd) // linux specific
// This is required because some tools detects if the program is running
// from terminal by looking at the stdin/out bindings.
// https://github.com/arduino/arduino-cli/issues/844
p.cmd.Stdin = nullReaderInstance
return p, nil
}
// TellCommandNotToSpawnShell avoids that the specified Cmd display a small
// command prompt while runnning on Windows. It has no effects on other OS.
func (p *Process) TellCommandNotToSpawnShell() {
tellCommandNotToSpawnShell(p.cmd)
}
// NewProcessFromPath creates a command from the provided executable path,
// additional environment vars (in addition to the system default ones)
// and command line arguments.
func NewProcessFromPath(extraEnv []string, executable *Path, args ...string) (*Process, error) {
processArgs := []string{executable.String()}
processArgs = append(processArgs, args...)
return NewProcess(extraEnv, processArgs...)
}
// RedirectStdoutTo will redirect the process' stdout to the specified
// writer. Any previous redirection will be overwritten.
func (p *Process) RedirectStdoutTo(out io.Writer) {
p.cmd.Stdout = out
}
// RedirectStderrTo will redirect the process' stdout to the specified
// writer. Any previous redirection will be overwritten.
func (p *Process) RedirectStderrTo(out io.Writer) {
p.cmd.Stderr = out
}
// StdinPipe returns a pipe that will be connected to the command's standard
// input when the command starts. The pipe will be closed automatically after
// Wait sees the command exit. A caller need only call Close to force the pipe
// to close sooner. For example, if the command being run will not exit until
// standard input is closed, the caller must close the pipe.
func (p *Process) StdinPipe() (io.WriteCloser, error) {
if p.cmd.Stdin == nullReaderInstance {
p.cmd.Stdin = nil
}
return p.cmd.StdinPipe()
}
// StdoutPipe returns a pipe that will be connected to the command's standard
// output when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// don't need to close the pipe themselves. It is thus incorrect to call Wait
// before all reads from the pipe have completed.
// For the same reason, it is incorrect to call Run when using StdoutPipe.
func (p *Process) StdoutPipe() (io.ReadCloser, error) {
return p.cmd.StdoutPipe()
}
// StderrPipe returns a pipe that will be connected to the command's standard
// error when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// don't need to close the pipe themselves. It is thus incorrect to call Wait
// before all reads from the pipe have completed.
// For the same reason, it is incorrect to use Run when using StderrPipe.
func (p *Process) StderrPipe() (io.ReadCloser, error) {
return p.cmd.StderrPipe()
}
// Start will start the underliyng process.
func (p *Process) Start() error {
return p.cmd.Start()
}
// Wait waits for the command to exit and waits for any copying to stdin or copying
// from stdout or stderr to complete.
func (p *Process) Wait() error {
// TODO: make some helpers to retrieve exit codes out of *ExitError.
return p.cmd.Wait()
}
// Signal sends a signal to the Process. Sending Interrupt on Windows is not implemented.
func (p *Process) Signal(sig os.Signal) error {
return p.cmd.Process.Signal(sig)
}
// Kill causes the Process to exit immediately. Kill does not wait until the Process has
// actually exited. This only kills the Process itself, not any other processes it may
// have started.
func (p *Process) Kill() error {
return kill(p.cmd)
}
// SetDir sets the working directory of the command. If Dir is the empty string, Run
// runs the command in the calling process's current directory.
func (p *Process) SetDir(dir string) {
p.cmd.Dir = dir
}
// GetDir gets the working directory of the command.
func (p *Process) GetDir() string {
return p.cmd.Dir
}
// SetDirFromPath sets the working directory of the command. If path is nil, Run
// runs the command in the calling process's current directory.
func (p *Process) SetDirFromPath(path *Path) {
if path == nil {
p.cmd.Dir = ""
} else {
p.cmd.Dir = path.String()
}
}
// Run starts the specified command and waits for it to complete.
func (p *Process) Run() error {
return p.cmd.Run()
}
// SetEnvironment set the environment for the running process. Each entry is of the form "key=value".
// System default environments will be wiped out.
func (p *Process) SetEnvironment(values []string) {
p.cmd.Env = append([]string{}, values...)
}
// RunWithinContext starts the specified command and waits for it to complete. If the given context
// is canceled before the normal process termination, the process is killed.
func (p *Process) RunWithinContext(ctx context.Context) error {
if err := p.Start(); err != nil {
return err
}
completed := make(chan struct{})
defer close(completed)
go func() {
select {
case <-ctx.Done():
p.Kill()
case <-completed:
}
}()
return p.Wait()
}
// RunAndCaptureOutput starts the specified command and waits for it to complete. If the given context
// is canceled before the normal process termination, the process is killed. The standard output and
// standard error of the process are captured and returned at process termination.
func (p *Process) RunAndCaptureOutput(ctx context.Context) ([]byte, []byte, error) {
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
p.RedirectStdoutTo(stdout)
p.RedirectStderrTo(stderr)
err := p.RunWithinContext(ctx)
return stdout.Bytes(), stderr.Bytes(), err
}
// GetArgs returns the command arguments
func (p *Process) GetArgs() []string {
return p.cmd.Args
}
// nullReaderInstance is an io.Reader that will always return EOF
var nullReaderInstance = &nullReader{}
type nullReader struct{}
func (r *nullReader) Read(buff []byte) (int, error) {
return 0, io.EOF
}