Refactor pmctl into new portmaster-start
This commit is contained in:
384
cmds/portmaster-start/run.go
Normal file
384
cmds/portmaster-start/run.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
const (
|
||||
// RestartExitCode is the exit code that any service started by portmaster-start
|
||||
// can return in order to trigger a restart after a clean shutdown.
|
||||
RestartExitCode = 23
|
||||
)
|
||||
|
||||
var (
|
||||
runningInConsole bool
|
||||
onWindows = runtime.GOOS == "windows"
|
||||
stdinSignals bool
|
||||
childIsRunning = abool.NewBool(false)
|
||||
)
|
||||
|
||||
// Options for starting component
|
||||
type Options struct {
|
||||
Name string
|
||||
Identifier string // component identifier
|
||||
ShortIdentifier string // populated automatically
|
||||
SuppressArgs bool // do not use any args
|
||||
AllowDownload bool // allow download of component if it is not yet available
|
||||
AllowHidingWindow bool // allow hiding the window of the subprocess
|
||||
NoOutput bool // do not use stdout/err if logging to file is available (did not fail to open log file)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerComponent([]Options{
|
||||
{
|
||||
Name: "Portmaster Core",
|
||||
Identifier: "core/portmaster-core",
|
||||
AllowDownload: true,
|
||||
AllowHidingWindow: true,
|
||||
},
|
||||
{
|
||||
Name: "Portmaster App",
|
||||
Identifier: "app/portmaster-app",
|
||||
AllowDownload: false,
|
||||
AllowHidingWindow: false,
|
||||
},
|
||||
{
|
||||
Name: "Portmaster Notifier",
|
||||
Identifier: "notifier/portmaster-notifier",
|
||||
AllowDownload: false,
|
||||
AllowHidingWindow: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func registerComponent(opts []Options) {
|
||||
for idx := range opts {
|
||||
opt := &opts[idx] // we need a copy
|
||||
if opt.ShortIdentifier == "" {
|
||||
opt.ShortIdentifier = path.Dir(opt.Identifier)
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: opt.ShortIdentifier,
|
||||
Short: "Run the " + opt.Name,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := run(cmd, opt, args)
|
||||
initiateShutdown(err)
|
||||
return err
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
showCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: opt.ShortIdentifier,
|
||||
Short: "Show command to execute the " + opt.Name,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return show(cmd, opt, args)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getExecArgs(opts *Options, cmdArgs []string) []string {
|
||||
if opts.SuppressArgs {
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{"--data", dataDir}
|
||||
if stdinSignals {
|
||||
args = append(args, "-input-signals")
|
||||
}
|
||||
args = append(args, cmdArgs...)
|
||||
return args
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, opts *Options, cmdArgs []string) (err error) {
|
||||
// set download option
|
||||
registry.Online = opts.AllowDownload
|
||||
|
||||
if isShutdown() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get original arguments
|
||||
// additional parameters can be specified using -- --some-parameter
|
||||
args := getExecArgs(opts, cmdArgs)
|
||||
|
||||
// check for duplicate instances
|
||||
if opts.ShortIdentifier == "core" {
|
||||
pid, _ := checkAndCreateInstanceLock(opts.ShortIdentifier)
|
||||
if pid != 0 {
|
||||
return fmt.Errorf("another instance of Portmaster Core is already running: PID %d", pid)
|
||||
}
|
||||
defer func() {
|
||||
err := deleteInstanceLock(opts.ShortIdentifier)
|
||||
if err != nil {
|
||||
log.Printf("failed to delete instance lock: %s\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// notify service after some time
|
||||
go func() {
|
||||
// assume that after 3 seconds service has finished starting
|
||||
time.Sleep(3 * time.Second)
|
||||
startupComplete <- struct{}{}
|
||||
}()
|
||||
|
||||
// adapt identifier
|
||||
if onWindows {
|
||||
opts.Identifier += ".exe"
|
||||
}
|
||||
|
||||
// setup logging
|
||||
// init log file
|
||||
logFile := getPmStartLogFile(".log")
|
||||
if logFile != nil {
|
||||
// don't close logFile, will be closed by system
|
||||
if opts.NoOutput {
|
||||
log.Println("disabling log output to stdout... bye!")
|
||||
log.SetOutput(logFile)
|
||||
} else {
|
||||
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
|
||||
}
|
||||
}
|
||||
|
||||
return runAndRestart(opts, args)
|
||||
}
|
||||
|
||||
func runAndRestart(opts *Options, args []string) error {
|
||||
tries := 0
|
||||
for {
|
||||
tryAgain, err := execute(opts, args)
|
||||
if err != nil {
|
||||
log.Printf("%s failed with: %s\n", opts.Identifier, err)
|
||||
tries++
|
||||
if tries >= maxRetries {
|
||||
log.Printf("encountered %d consecutive errors, giving up ...", tries)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
tries = 0
|
||||
log.Printf("%s exited without error", opts.Identifier)
|
||||
}
|
||||
|
||||
if !tryAgain {
|
||||
return err
|
||||
}
|
||||
|
||||
// if a restart was requested `tries` is set to 0 so
|
||||
// this becomes a no-op.
|
||||
time.Sleep(time.Duration(2*tries) * time.Second)
|
||||
|
||||
if tries >= 2 || err == nil {
|
||||
// if we are constantly failing or a restart was requested
|
||||
// try to update the resources.
|
||||
log.Printf("updating registry index")
|
||||
updateRegistryIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixExecPerm(path string) error {
|
||||
if onWindows {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat %s: %w", path, err)
|
||||
}
|
||||
|
||||
if info.Mode() == 0755 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Chmod(path, 0755); err != nil {
|
||||
return fmt.Errorf("failed to chmod %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyLogs(opts *Options, consoleSink io.Writer, version, ext string, logSource io.Reader, notifier chan<- struct{}) {
|
||||
defer func() { notifier <- struct{}{} }()
|
||||
|
||||
sink := consoleSink
|
||||
|
||||
fileSink := getLogFile(opts, version, ext)
|
||||
if fileSink != nil {
|
||||
defer finalizeLogFile(fileSink)
|
||||
if opts.NoOutput {
|
||||
sink = fileSink
|
||||
} else {
|
||||
sink = io.MultiWriter(consoleSink, fileSink)
|
||||
}
|
||||
}
|
||||
|
||||
if bytes, err := io.Copy(sink, logSource); err != nil {
|
||||
log.Printf("%s: writting logs failed after %d bytes: %s", fileSink.Name(), bytes, err)
|
||||
}
|
||||
}
|
||||
|
||||
func persistOutputStreams(opts *Options, version string, cmd *exec.Cmd) (chan struct{}, error) {
|
||||
var (
|
||||
done = make(chan struct{})
|
||||
copyNotifier = make(chan struct{}, 2)
|
||||
)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect stdout: %w", err)
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect stderr: %w", err)
|
||||
}
|
||||
|
||||
go copyLogs(opts, os.Stdout, version, ".log", stdout, copyNotifier)
|
||||
go copyLogs(opts, os.Stderr, version, ".error.log", stderr, copyNotifier)
|
||||
|
||||
go func() {
|
||||
<-copyNotifier
|
||||
<-copyNotifier
|
||||
close(copyNotifier)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
return done, nil
|
||||
}
|
||||
|
||||
func execute(opts *Options, args []string) (cont bool, err error) {
|
||||
file, err := registry.GetFile(platform(opts.Identifier))
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("could not get component: %w", err)
|
||||
}
|
||||
|
||||
// check permission
|
||||
if err := fixExecPerm(file.Path()); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
log.Printf("starting %s %s\n", file.Path(), strings.Join(args, " "))
|
||||
|
||||
// create command
|
||||
exc := exec.Command(file.Path(), args...) //nolint:gosec // everything is okay
|
||||
|
||||
if !runningInConsole && opts.AllowHidingWindow {
|
||||
// Windows only:
|
||||
// only hide (all) windows of program if we are not running in console and windows may be hidden
|
||||
hideWindow(exc)
|
||||
}
|
||||
|
||||
outputsWritten, err := persistOutputStreams(opts, file.Version(), exc)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
interrupt, err := getProcessSignalFunc(exc)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
err = exc.Start()
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("failed to start %s: %w", opts.Identifier, err)
|
||||
}
|
||||
childIsRunning.Set()
|
||||
|
||||
// wait for completion
|
||||
finished := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(finished)
|
||||
|
||||
<-outputsWritten
|
||||
// wait for process to return
|
||||
finished <- exc.Wait()
|
||||
// update status
|
||||
childIsRunning.UnSet()
|
||||
}()
|
||||
|
||||
// state change listeners
|
||||
select {
|
||||
case <-shuttingDown:
|
||||
if err := interrupt(); err != nil {
|
||||
log.Printf("failed to signal %s to shutdown: %s\n", opts.Identifier, err)
|
||||
err = exc.Process.Kill()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to kill %s: %w", opts.Identifier, err)
|
||||
}
|
||||
return false, fmt.Errorf("killed %s", opts.Identifier)
|
||||
}
|
||||
|
||||
// wait until shut down
|
||||
select {
|
||||
case <-finished:
|
||||
case <-time.After(11 * time.Second): // portmaster core prints stack if not able to shutdown in 10 seconds
|
||||
// kill
|
||||
err = exc.Process.Kill()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to kill %s: %s", opts.Identifier, err)
|
||||
}
|
||||
return false, fmt.Errorf("killed %s", opts.Identifier)
|
||||
}
|
||||
return false, nil
|
||||
|
||||
case err := <-finished:
|
||||
return parseExitError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getProcessSignalFunc(cmd *exec.Cmd) (func() error, error) {
|
||||
if stdinSignals {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect stdin: %w", err)
|
||||
}
|
||||
|
||||
return func() error {
|
||||
_, err := fmt.Fprintln(stdin, "SIGINT")
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
|
||||
return func() error {
|
||||
return cmd.Process.Signal(os.Interrupt)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseExitError(err error) (restart bool, errWithCtx error) {
|
||||
if err == nil {
|
||||
// clean and coordinated exit
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if exErr, ok := err.(*exec.ExitError); ok {
|
||||
switch exErr.ProcessState.ExitCode() {
|
||||
case 0:
|
||||
return false, fmt.Errorf("clean exit with error: %w", err)
|
||||
case 1:
|
||||
return true, fmt.Errorf("error during execution: %w", err)
|
||||
case RestartExitCode:
|
||||
return true, nil
|
||||
default:
|
||||
return true, fmt.Errorf("unknown exit code %w", exErr)
|
||||
}
|
||||
}
|
||||
|
||||
return true, fmt.Errorf("unexpected error type: %w", err)
|
||||
}
|
||||
Reference in New Issue
Block a user