github.com/ChicK00o/awgo@v0.29.4/background.go (about)

     1  // Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net>
     2  // MIT Licence - http://opensource.org/licenses/MIT
     3  
     4  package aw
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"syscall"
    15  
    16  	"github.com/ChicK00o/awgo/util"
    17  )
    18  
    19  // ErrJobExists is the error returned by RunInBackground if a job with
    20  // the given name is already running.
    21  type ErrJobExists struct {
    22  	Name string // Name of the job
    23  	Pid  int    // PID of the running job
    24  }
    25  
    26  // Error implements error interface.
    27  func (err ErrJobExists) Error() string {
    28  	return fmt.Sprintf(`job "%s" already running with PID %d`, err.Name, err.Pid)
    29  }
    30  
    31  // Is returns true if target is of type ErrJobExists.
    32  func (err ErrJobExists) Is(target error) bool {
    33  	_, ok := target.(ErrJobExists)
    34  	return ok
    35  }
    36  
    37  // IsJobExists returns true if error is of type or wraps ErrJobExists.
    38  func IsJobExists(err error) bool {
    39  	return errors.Is(err, ErrJobExists{})
    40  }
    41  
    42  // RunInBackground executes cmd in the background. It returns an
    43  // ErrJobExists error if a job of the same name is already running.
    44  func (wf *Workflow) RunInBackground(jobName string, cmd *exec.Cmd) error {
    45  	if wf.IsRunning(jobName) {
    46  		pid, _ := wf.getPid(jobName)
    47  		return ErrJobExists{jobName, pid}
    48  	}
    49  
    50  	if cmd.SysProcAttr == nil {
    51  		cmd.SysProcAttr = &syscall.SysProcAttr{}
    52  	}
    53  	// Prevent process from being killed when parent is
    54  	cmd.SysProcAttr.Setpgid = true
    55  	if err := cmd.Start(); err != nil {
    56  		return fmt.Errorf("execute command %v: %w", cmd, err)
    57  	}
    58  
    59  	return wf.savePid(jobName, cmd.Process.Pid)
    60  }
    61  
    62  // Kill stops a background job.
    63  func (wf *Workflow) Kill(jobName string) error {
    64  	pid, err := wf.getPid(jobName)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	p := wf.pidFile(jobName)
    69  	err = syscall.Kill(pid, syscall.SIGTERM)
    70  	os.Remove(p)
    71  	return err
    72  }
    73  
    74  // IsRunning returns true if a job with name jobName is currently running.
    75  func (wf *Workflow) IsRunning(jobName string) bool {
    76  	pid, err := wf.getPid(jobName)
    77  	if err != nil {
    78  		return false
    79  	}
    80  	if err = syscall.Kill(pid, 0); err != nil {
    81  		// Delete stale PID file
    82  		os.Remove(wf.pidFile(jobName))
    83  		return false
    84  	}
    85  	return true
    86  }
    87  
    88  // Save PID to a job-specific file.
    89  func (wf *Workflow) savePid(jobName string, pid int) error {
    90  	return ioutil.WriteFile(wf.pidFile(jobName), []byte(strconv.Itoa(pid)), 0600)
    91  }
    92  
    93  // Return PID for job.
    94  func (wf *Workflow) getPid(jobName string) (int, error) {
    95  	data, err := ioutil.ReadFile(wf.pidFile(jobName))
    96  	if err != nil {
    97  		return 0, err
    98  	}
    99  	pid, err := strconv.Atoi(string(data))
   100  	if err != nil {
   101  		return 0, err
   102  	}
   103  	return pid, nil
   104  }
   105  
   106  // Path to PID file for job.
   107  func (wf *Workflow) pidFile(jobName string) string {
   108  	dir := util.MustExist(filepath.Join(wf.awCacheDir(), "jobs"))
   109  	return filepath.Join(dir, jobName+".pid")
   110  }