Files
snapd/osutil/exec.go
Arseniy Aharonov fcc3ba8aea osutil: refactoring the code exporting mocking APIs to other packages
* osutil: Refactoring the code exporting mocking APIs to other packages

File 'mockable.go' is used to mock certain data obtained by 'osutil'
from the system (e.g. 'mountinfo'). The file also contained some
variables that belong to production code itself. A cleanup
refactoring was applied to this file and 'mountinfo_linux.go':
- Package variables moved from 'mockable.go' to logic that owns it
- Test-related logic moved from 'mountinfo_linux.go' to 'mockable.go'
- United and refactored logic designed to mock 'mountinfo'

Signed-off-by: Arseniy Aharonov <arseniy.aharonov@canonical.com>

* osutil: Fixes and cleanups after the code review

Signed-off-by: Arseniy Aharonov <arseniy.aharonov@canonical.com>

* osutil: make mustNotBeTestBinary function non-exportable

To avoid unwonted usage of this method in different places in
the code it was requested to make this function local to package
osutil only.

Signed-off-by: Arseniy Aharonov <arseniy.aharonov@canonical.com>

* osutil: refactoring mountinfo code following PR review

My original refactoring of the mountinfo logic introduced
an inconsistency into the code, so that os-agnostic code
contain Linux-related logic. This inconsistencey was
eliminated.

Signed-off-by: Arseniy Aharonov <arseniy.aharonov@canonical.com>
2022-01-25 17:44:18 +01:00

176 lines
4.8 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* 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, see <http://www.gnu.org/licenses/>.
*
*/
package osutil
import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
"time"
"gopkg.in/tomb.v2"
"github.com/snapcore/snapd/strutil"
)
var (
syscallKill = syscall.Kill
syscallGetpgid = syscall.Getpgid
)
var cmdWaitTimeout = 5 * time.Second
// KillProcessGroup kills the process group associated with the given command.
//
// If the command hasn't had Setpgid set in its SysProcAttr, you'll probably end
// up killing yourself.
func KillProcessGroup(cmd *exec.Cmd) error {
pgid, err := syscallGetpgid(cmd.Process.Pid)
if err != nil {
return err
}
if pgid == 1 {
return fmt.Errorf("cannot kill pgid 1")
}
return syscallKill(-pgid, syscall.SIGKILL)
}
// RunAndWait runs a command for the given argv with the given environ added to
// os.Environ, killing it if it reaches timeout, or if the tomb is dying.
func RunAndWait(argv []string, env []string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) {
if len(argv) == 0 {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-empty argv")
}
if timeout <= 0 {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs positive timeout")
}
if tomb == nil {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-nil tomb")
}
command := exec.Command(argv[0], argv[1:]...)
// setup a process group for the command so that we can kill parent
// and children on e.g. timeout
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
command.Env = append(os.Environ(), env...)
// Make sure we can obtain stdout and stderror. Same buffer so they're
// combined.
buffer := strutil.NewLimitedBuffer(100, 10*1024)
command.Stdout = buffer
command.Stderr = buffer
// Actually run the command.
if err := command.Start(); err != nil {
return nil, err
}
// add timeout handling
killTimerCh := time.After(timeout)
commandCompleted := make(chan struct{})
var commandError error
go func() {
// Wait for hook to complete
commandError = command.Wait()
close(commandCompleted)
}()
var abortOrTimeoutError error
select {
case <-commandCompleted:
// Command completed; it may or may not have been successful.
return buffer.Bytes(), commandError
case <-tomb.Dying():
// Hook was aborted, process will get killed below
abortOrTimeoutError = fmt.Errorf("aborted")
case <-killTimerCh:
// Max timeout reached, process will get killed below
abortOrTimeoutError = fmt.Errorf("exceeded maximum runtime of %s", timeout)
}
// select above exited which means that aborted or killTimeout
// was reached. Kill the command and wait for command.Wait()
// to clean it up (but limit the wait with the cmdWaitTimer)
if err := KillProcessGroup(command); err != nil {
return nil, fmt.Errorf("cannot abort: %s", err)
}
select {
case <-time.After(cmdWaitTimeout):
// cmdWaitTimeout was reached, i.e. command.Wait() did not
// finish in a reasonable amount of time, we can not use
// buffer in this case so return without it.
return nil, fmt.Errorf("%v, but did not stop", abortOrTimeoutError)
case <-commandCompleted:
// cmd.Wait came back from waiting the killed process
break
}
fmt.Fprintf(buffer, "\n<%s>", abortOrTimeoutError)
return buffer.Bytes(), abortOrTimeoutError
}
type waitingReader struct {
reader io.Reader
cmd *exec.Cmd
}
func (r *waitingReader) Close() error {
if r.cmd.Process != nil {
r.cmd.Process.Kill()
}
return r.cmd.Wait()
}
func (r *waitingReader) Read(b []byte) (int, error) {
n, err := r.reader.Read(b)
if n == 0 && err == io.EOF {
err = r.Close()
if err == nil {
return 0, io.EOF
}
return 0, err
}
return n, err
}
// StreamCommand runs a the named program with the given arguments,
// streaming its standard output over the returned io.ReadCloser.
//
// The program will run until EOF is reached (at which point the
// ReadCloser is closed), or until the ReadCloser is explicitly closed.
func StreamCommand(name string, args ...string) (io.ReadCloser, error) {
cmd := exec.Command(name, args...)
pipe, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return nil, err
}
return &waitingReader{reader: pipe, cmd: cmd}, nil
}