mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* 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>
176 lines
4.8 KiB
Go
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
|
|
}
|