diff --git a/cmds/portmaster-start/.gitignore b/cmds/portmaster-start/.gitignore new file mode 100644 index 00000000..e3db7ed6 --- /dev/null +++ b/cmds/portmaster-start/.gitignore @@ -0,0 +1,6 @@ +# binaries +portmaster-start +portmaster-start.exe + +# test dir +test diff --git a/pmctl/build b/cmds/portmaster-start/build similarity index 100% rename from pmctl/build rename to cmds/portmaster-start/build diff --git a/pmctl/console_default.go b/cmds/portmaster-start/console_default.go similarity index 100% rename from pmctl/console_default.go rename to cmds/portmaster-start/console_default.go diff --git a/pmctl/console_windows.go b/cmds/portmaster-start/console_windows.go similarity index 100% rename from pmctl/console_windows.go rename to cmds/portmaster-start/console_windows.go diff --git a/pmctl/install_windows.go b/cmds/portmaster-start/install_windows.go similarity index 86% rename from pmctl/install_windows.go rename to cmds/portmaster-start/install_windows.go index 3446c493..c0728ad8 100644 --- a/pmctl/install_windows.go +++ b/cmds/portmaster-start/install_windows.go @@ -19,9 +19,7 @@ import ( "golang.org/x/sys/windows/svc/mgr" ) -const ( - exeSuffix = ".exe" -) +const exeSuffix = ".exe" func init() { rootCmd.AddCommand(installCmd) @@ -57,39 +55,18 @@ var uninstallService = &cobra.Command{ RunE: uninstallWindowsService, } -func getExePath() (string, error) { - // get own filepath - prog := os.Args[0] - p, err := filepath.Abs(prog) +func getAbsBinaryPath() (string, error) { + p, err := filepath.Abs(os.Args[0]) if err != nil { return "", err } - // check if the path is valid - fi, err := os.Stat(p) - if err == nil { - if !fi.Mode().IsDir() { - return p, nil - } - err = fmt.Errorf("%s is directory", p) - } - // check if we have a .exe extension, add and check if not - if filepath.Ext(p) == "" { - p += exeSuffix - fi, err = os.Stat(p) - if err == nil { - if !fi.Mode().IsDir() { - return p, nil - } - err = fmt.Errorf("%s is directory", p) - } - } - return "", err + + return p, nil } func getServiceExecCommand(exePath string, escape bool) []string { return []string{ maybeEscape(exePath, escape), - "run", "core-service", "--data", maybeEscape(dataRoot.Path, escape), @@ -126,7 +103,7 @@ func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uin func installWindowsService(cmd *cobra.Command, args []string) error { // get exe path - exePath, err := getExePath() + exePath, err := getAbsBinaryPath() if err != nil { return fmt.Errorf("failed to get exe path: %s", err) } @@ -180,7 +157,7 @@ func uninstallWindowsService(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer m.Disconnect() //nolint:errcheck // TODO + defer m.Disconnect() //nolint:errcheck // we don't care if we failed to disconnect from the service manager, we're quitting anyway. // open service s, err := m.OpenService(serviceName) diff --git a/pmctl/lock.go b/cmds/portmaster-start/lock.go similarity index 84% rename from pmctl/lock.go rename to cmds/portmaster-start/lock.go index 8117109c..7d7af1bc 100644 --- a/pmctl/lock.go +++ b/cmds/portmaster-start/lock.go @@ -35,13 +35,7 @@ func checkAndCreateInstanceLock(name string) (pid int32, err error) { // check if process exists p, err := processInfo.NewProcess(int32(parsedPid)) if err == nil { - // TODO: remove this workaround as soon as NewProcess really returns an error on windows when the process does not exist - // Issue: https://github.com/shirou/gopsutil/issues/729 - _, err = p.Name() - if err == nil { - // process exists - return p.Pid, nil - } + return p.Pid, nil } // else create new lock diff --git a/pmctl/logs.go b/cmds/portmaster-start/logs.go similarity index 59% rename from pmctl/logs.go rename to cmds/portmaster-start/logs.go index 4f2c623b..2a13e097 100644 --- a/pmctl/logs.go +++ b/cmds/portmaster-start/logs.go @@ -35,7 +35,7 @@ func initializeLogFile(logFilePath string, identifier string, version string) *o metaSection, err := dsd.Dump(meta, dsd.JSON) if err != nil { log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile, logFilePath) + finalizeLogFile(logFile) return nil } c.AppendAsBlock(metaSection) @@ -46,14 +46,16 @@ func initializeLogFile(logFilePath string, identifier string, version string) *o _, err = logFile.Write(c.CompileData()) if err != nil { log.Printf("failed to write header for log file %s: %s\n", logFilePath, err) - finalizeLogFile(logFile, logFilePath) + finalizeLogFile(logFile) return nil } return logFile } -func finalizeLogFile(logFile *os.File, logFilePath string) { +func finalizeLogFile(logFile *os.File) { + logFilePath := logFile.Name() + err := logFile.Close() if err != nil { log.Printf("failed to close log file %s: %s\n", logFilePath, err) @@ -61,28 +63,38 @@ func finalizeLogFile(logFile *os.File, logFilePath string) { // check file size stat, err := os.Stat(logFilePath) - if err == nil { - // delete if file is smaller than - if stat.Size() < 200 { // header + info is about 150 bytes - err := os.Remove(logFilePath) - if err != nil { - log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err) - } - } + if err != nil { + return + } + + // delete if file is smaller than + if stat.Size() >= 200 { // header + info is about 150 bytes + return + } + + if err := os.Remove(logFilePath); err != nil { + log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err) } } -func initControlLogFile() *os.File { +func getLogFile(options *Options, version, ext string) *os.File { // check logging dir - logFileBasePath := filepath.Join(logsRoot.Path, "control") + logFileBasePath := filepath.Join(logsRoot.Path, options.ShortIdentifier) err := logsRoot.EnsureAbsPath(logFileBasePath) if err != nil { log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err) } // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.log", time.Now().UTC().Format("2006-01-02-15-04-05"))) - return initializeLogFile(logFilePath, "control/portmaster-control", info.Version()) + logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s%s", time.Now().UTC().Format("2006-01-02-15-04-05"), ext)) + return initializeLogFile(logFilePath, options.Identifier, version) +} + +func getPmStartLogFile(ext string) *os.File { + return getLogFile(&Options{ + ShortIdentifier: "start", + Identifier: "start/portmaster-start", + }, info.Version(), ext) } //nolint:deadcode,unused // false positive on linux, currently used by windows only @@ -92,44 +104,25 @@ func logControlError(cErr error) { return } - // check logging dir - logFileBasePath := filepath.Join(logsRoot.Path, "control") - err := logsRoot.EnsureAbsPath(logFileBasePath) - if err != nil { - log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err) - } - - // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.error.log", time.Now().UTC().Format("2006-01-02-15-04-05"))) - errorFile := initializeLogFile(logFilePath, "control/portmaster-control", info.Version()) + errorFile := getPmStartLogFile(".error.log") if errorFile == nil { return } + defer errorFile.Close() - // write error and close fmt.Fprintln(errorFile, cErr.Error()) - errorFile.Close() } //nolint:deadcode,unused // TODO func logControlStack() { - // check logging dir - logFileBasePath := filepath.Join(logsRoot.Path, "control") - err := logsRoot.EnsureAbsPath(logFileBasePath) - if err != nil { - log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err) - } - - // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.stack.log", time.Now().UTC().Format("2006-01-02-15-04-05"))) - errorFile := initializeLogFile(logFilePath, "control/portmaster-control", info.Version()) - if errorFile == nil { + fp := getPmStartLogFile(".stack.log") + if fp == nil { return } + defer fp.Close() // write error and close - _ = pprof.Lookup("goroutine").WriteTo(errorFile, 2) - errorFile.Close() + _ = pprof.Lookup("goroutine").WriteTo(fp, 2) } //nolint:deadcode,unused // false positive on linux, currently used by windows only diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go new file mode 100644 index 00000000..52ef057a --- /dev/null +++ b/cmds/portmaster-start/main.go @@ -0,0 +1,221 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/safing/portbase/dataroot" + "github.com/safing/portbase/info" + portlog "github.com/safing/portbase/log" + "github.com/safing/portbase/updater" + "github.com/safing/portbase/utils" + + "github.com/spf13/cobra" +) + +var ( + dataDir string + maxRetries int + dataRoot *utils.DirStructure + logsRoot *utils.DirStructure + + // create registry + registry = &updater.ResourceRegistry{ + Name: "updates", + UpdateURLs: []string{ + "https://updates.safing.io", + }, + Beta: false, + DevMode: false, + Online: true, // is disabled later based on command + } + + rootCmd = &cobra.Command{ + Use: "portmaster-start", + Short: "Start Portmaster components", + PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { + + if err := configureDataRoot(); err != nil { + return err + } + + if err := configureLogging(); err != nil { + return err + } + + return nil + }, + SilenceUsage: true, + } +) + +func init() { + // Let cobra ignore if we are running as "GUI" or not + cobra.MousetrapHelpText = "" + + flags := rootCmd.PersistentFlags() + { + flags.StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.") + flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component") + flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdid.") + _ = rootCmd.MarkPersistentFlagDirname("data") + _ = flags.MarkHidden("input-signals") + } +} + +func main() { + cobra.OnInitialize(initCobra) + + // set meta info + info.Set("Portmaster Start", "0.3.5", "AGPLv3", false) + + // for debugging + // log.Start() + // log.SetLogLevel(log.TraceLevel) + // go func() { + // time.Sleep(3 * time.Second) + // pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) + // os.Exit(1) + // }() + + // catch interrupt for clean shutdown + signalCh := make(chan os.Signal, 2) + signal.Notify( + signalCh, + os.Interrupt, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + // start root command + go func() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } + os.Exit(0) + }() + + // for debugging windows service (no stdout/err) + // go func() { + // time.Sleep(10 * time.Second) + // // initiateShutdown(nil) + // // logControlStack() + // }() + + // wait for signals + for sig := range signalCh { + if childIsRunning.IsSet() { + log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig) + continue + } + + log.Printf("got %s signal, exiting... (not executing anything)\n", sig) + os.Exit(0) + } +} + +func initCobra() { + // check if we are running in a console (try to attach to parent console if available) + var err error + runningInConsole, err = attachToParentConsole() + if err != nil { + log.Fatalf("failed to attach to parent console: %s\n", err) + } + + // check if meta info is ok + err = info.CheckVersion() + if err != nil { + log.Fatalf("compile error: please compile using the provided build script") + } + + // set up logging + log.SetFlags(log.Ldate | log.Ltime | log.LUTC) + log.SetPrefix("[control] ") + log.SetOutput(os.Stdout) + + // not using portbase logger + portlog.SetLogLevel(portlog.CriticalLevel) +} + +func configureDataRoot() error { + // The data directory is not + // check for environment variable + // PORTMASTER_DATA + if dataDir == "" { + dataDir = os.Getenv("PORTMASTER_DATA") + } + + // check data dir + if dataDir == "" { + return errors.New("please set the data directory using --data=/path/to/data/dir") + } + + // remove redundant escape characters and quotes + dataDir = strings.Trim(dataDir, `\"`) + // initialize dataroot + err := dataroot.Initialize(dataDir, 0755) + if err != nil { + return fmt.Errorf("failed to initialize data root: %s", err) + } + dataRoot = dataroot.Root() + + // initialize registry + err = registry.Initialize(dataRoot.ChildDir("updates", 0755)) + if err != nil { + return err + } + + registry.AddIndex(updater.Index{ + Path: "stable.json", + Stable: true, + Beta: false, + }) + + // TODO: enable loading beta versions + // registry.AddIndex(updater.Index{ + // Path: "beta.json", + // Stable: false, + // Beta: true, + // }) + + updateRegistryIndex() + return nil +} + +func configureLogging() error { + // set up logs root + logsRoot = dataRoot.ChildDir("logs", 0777) + err := logsRoot.Ensure() + if err != nil { + return fmt.Errorf("failed to initialize logs root: %s", err) + } + + // warn about CTRL-C on windows + if runningInConsole && onWindows { + log.Println("WARNING: portmaster-start is marked as a GUI application in order to get rid of the console window.") + log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.") + } + return nil +} + +func updateRegistryIndex() { + err := registry.LoadIndexes(context.Background()) + if err != nil { + log.Printf("WARNING: error loading indexes: %s\n", err) + } + + err = registry.ScanStorage("") + if err != nil { + log.Printf("WARNING: error during storage scan: %s\n", err) + } + + registry.SelectVersions() +} diff --git a/pmctl/pack b/cmds/portmaster-start/pack similarity index 76% rename from pmctl/pack rename to cmds/portmaster-start/pack index 1e83e602..79fdf784 100755 --- a/pmctl/pack +++ b/cmds/portmaster-start/pack @@ -8,15 +8,15 @@ COL_BOLD="\033[01;01m" COL_RED="\033[31m" destDirPart1="../dist" -destDirPart2="control" +destDirPart2="start" function check { # output - output="pmctl" + output="portmaster-start" # get version version=$(grep "info.Set" main.go | cut -d'"' -f4) # build versioned file name - filename="portmaster-control_v${version//./-}" + filename="portmaster-start_v${version//./-}" # platform platform="${GOOS}_${GOARCH}" if [[ $GOOS == "windows" ]]; then @@ -28,19 +28,19 @@ function check { # check if file exists if [[ -f $destPath ]]; then - echo "[control] $platform $version already built" + echo "[start] $platform $version already built" else - echo -e "${COL_BOLD}[control] $platform $version${COL_OFF}" + echo -e "${COL_BOLD}[start] $platform $version${COL_OFF}" fi } function build { # output - output="pmctl" + output="portmaster-start" # get version version=$(grep "info.Set" main.go | cut -d'"' -f4) # build versioned file name - filename="portmaster-control_v${version//./-}" + filename="portmaster-start_v${version//./-}" # platform platform="${GOOS}_${GOARCH}" if [[ $GOOS == "windows" ]]; then @@ -52,19 +52,19 @@ function build { # check if file exists if [[ -f $destPath ]]; then - echo "[control] $platform already built in version $version, skipping..." + echo "[start] $platform already built in version $version, skipping..." return fi # build ./build if [[ $? -ne 0 ]]; then - echo -e "\n${COL_BOLD}[control] $platform: ${COL_RED}BUILD FAILED.${COL_OFF}" + echo -e "\n${COL_BOLD}[start] $platform: ${COL_RED}BUILD FAILED.${COL_OFF}" exit 1 fi mkdir -p $(dirname $destPath) cp $output $destPath - echo -e "\n${COL_BOLD}[control] $platform: successfully built.${COL_OFF}" + echo -e "\n${COL_BOLD}[start] $platform: successfully built.${COL_OFF}" } function check_all { diff --git a/cmds/portmaster-start/run.go b/cmds/portmaster-start/run.go new file mode 100644 index 00000000..f01574d2 --- /dev/null +++ b/cmds/portmaster-start/run.go @@ -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) +} diff --git a/pmctl/service_windows.go b/cmds/portmaster-start/service_windows.go similarity index 94% rename from pmctl/service_windows.go rename to cmds/portmaster-start/service_windows.go index 8b02815d..d22dba18 100644 --- a/pmctl/service_windows.go +++ b/cmds/portmaster-start/service_windows.go @@ -27,7 +27,7 @@ var ( AllowDownload: true, AllowHidingWindow: false, NoOutput: true, - }) + }, args) }), FParseErrWhitelist: cobra.FParseErrWhitelist{ // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags @@ -41,7 +41,7 @@ var ( ) func init() { - runCmd.AddCommand(runCoreService) + rootCmd.AddCommand(runCoreService) } const serviceName = "PortmasterCore" @@ -88,7 +88,7 @@ service: return ssec, errno } -func runService(cmd *cobra.Command, opts *Options) error { +func runService(cmd *cobra.Command, opts *Options, cmdArgs []string) error { // check if we are running interactively isDebug, err := svc.IsAnInteractiveSession() if err != nil { @@ -122,7 +122,8 @@ func runService(cmd *cobra.Command, opts *Options) error { go func() { // run slightly delayed time.Sleep(250 * time.Millisecond) - _ = handleRun(cmd, opts) // error handled by shutdown routine + err := run(cmd, opts, getExecArgs(opts, cmdArgs)) + initiateShutdown(err) finishWg.Done() runWg.Done() }() diff --git a/cmds/portmaster-start/show.go b/cmds/portmaster-start/show.go new file mode 100644 index 00000000..0e3696c2 --- /dev/null +++ b/cmds/portmaster-start/show.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(showCmd) + // sub-commands of show are registered using registerComponent +} + +var showCmd = &cobra.Command{ + Use: "show", + PersistentPreRunE: func(*cobra.Command, []string) error { + // all show sub-commands need the data-root but no logging. + return configureDataRoot() + }, + Short: "Show the command to run a Portmaster component yourself", +} + +func show(cmd *cobra.Command, opts *Options, cmdArgs []string) error { + // get original arguments + args := getExecArgs(opts, cmdArgs) + + // adapt identifier + if onWindows { + opts.Identifier += ".exe" + } + + file, err := registry.GetFile(platform(opts.Identifier)) + if err != nil { + return fmt.Errorf("could not get component: %s", err) + } + + fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " ")) + + return nil +} diff --git a/pmctl/service.go b/cmds/portmaster-start/shutdown.go similarity index 50% rename from pmctl/service.go rename to cmds/portmaster-start/shutdown.go index cec7fa34..20ffd2de 100644 --- a/pmctl/service.go +++ b/cmds/portmaster-start/shutdown.go @@ -5,11 +5,10 @@ import ( ) var ( - startupComplete = make(chan struct{}) // signal that the start procedure completed (is never closed, just signaled once) - shuttingDown = make(chan struct{}) // signal that we are shutting down (will be closed, may not be closed directly, use initiateShutdown) - shutdownInitiated = false // not to be used directly + startupComplete = make(chan struct{}) // signal that the start procedure completed (is never closed, just signaled once) + shuttingDown = make(chan struct{}) // signal that we are shutting down (will be closed, may not be closed directly, use initiateShutdown) //nolint:deadcode,unused // false positive on linux, currently used by windows only - shutdownError error // may not be read or written to directly + shutdownError error // protected by shutdownLock shutdownLock sync.Mutex ) @@ -17,13 +16,24 @@ func initiateShutdown(err error) { shutdownLock.Lock() defer shutdownLock.Unlock() - if !shutdownInitiated { - shutdownInitiated = true + select { + case <-shuttingDown: + return + default: shutdownError = err close(shuttingDown) } } +func isShutdown() bool { + select { + case <-shuttingDown: + return true + default: + return false + } +} + //nolint:deadcode,unused // false positive on linux, currently used by windows only func getShutdownError() error { shutdownLock.Lock() diff --git a/cmds/portmaster-start/snoretoast_windows.go b/cmds/portmaster-start/snoretoast_windows.go new file mode 100644 index 00000000..a9f12630 --- /dev/null +++ b/cmds/portmaster-start/snoretoast_windows.go @@ -0,0 +1,14 @@ +package main + +func init() { + registerComponent([]Options{ + { + Name: "Portmaster SnoreToast Notifier", + ShortIdentifier: "notifier-snoretoast", // would otherwise conflict with notifier. + Identifier: "notifier/portmaster-snoretoast", + AllowDownload: false, + AllowHidingWindow: true, + SuppressArgs: true, + }, + }) +} diff --git a/pmctl/update.go b/cmds/portmaster-start/update.go similarity index 92% rename from pmctl/update.go rename to cmds/portmaster-start/update.go index 205247a1..45369af5 100644 --- a/pmctl/update.go +++ b/cmds/portmaster-start/update.go @@ -26,7 +26,7 @@ func downloadUpdates() error { if onWindows { registry.MandatoryUpdates = []string{ platform("core/portmaster-core.exe"), - platform("control/portmaster-control.exe"), + platform("start/portmaster-start.exe"), platform("app/portmaster-app.exe"), platform("notifier/portmaster-notifier.exe"), platform("notifier/portmaster-snoretoast.exe"), @@ -34,7 +34,7 @@ func downloadUpdates() error { } else { registry.MandatoryUpdates = []string{ platform("core/portmaster-core"), - platform("control/portmaster-control"), + platform("start/portmaster-start"), platform("app/portmaster-app"), platform("notifier/portmaster-notifier"), } diff --git a/cmds/portmaster-start/version.go b/cmds/portmaster-start/version.go new file mode 100644 index 00000000..2f80c77c --- /dev/null +++ b/cmds/portmaster-start/version.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "sort" + "strings" + "text/tabwriter" + + "github.com/safing/portbase/info" + "github.com/spf13/cobra" +) + +var showShortVersion bool +var showAllVersions bool +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Display various portmaster versions", + Args: cobra.NoArgs, + PersistentPreRunE: func(*cobra.Command, []string) error { + if showAllVersions { + // if we are going to show all component versions + // we need the dataroot to be configured. + if err := configureDataRoot(); err != nil { + return err + } + } + + return nil + }, + RunE: func(*cobra.Command, []string) error { + if !showAllVersions { + if showShortVersion { + fmt.Println(info.Version()) + } + + fmt.Println(info.FullVersion()) + return nil + } + + fmt.Printf("portmaster-start %s\n\n", info.Version()) + fmt.Printf("Components:\n") + + all := registry.Export() + keys := make([]string, 0, len(all)) + for identifier := range all { + keys = append(keys, identifier) + } + sort.Strings(keys) + + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + for _, identifier := range keys { + res := all[identifier] + + if showShortVersion { + // in "short" mode, skip all resources that are irrelevant on that platform + if !strings.HasPrefix(identifier, "all") && !strings.HasPrefix(identifier, runtime.GOOS) { + continue + } + } + + fmt.Fprintf(tw, " %s\t%s\n", identifier, res.SelectedVersion.VersionNumber) + } + tw.Flush() + + return nil + }, +} + +func init() { + flags := versionCmd.Flags() + { + flags.BoolVar(&showShortVersion, "short", false, "Print only the verison number.") + flags.BoolVar(&showAllVersions, "all", false, "Dump versions for all components.") + } + + rootCmd.AddCommand(versionCmd) +} diff --git a/pmctl/.gitignore b/pmctl/.gitignore deleted file mode 100644 index cd5f05f4..00000000 --- a/pmctl/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# binaries -pmctl -pmctl.exe - -# test dir -test diff --git a/pmctl/main.go b/pmctl/main.go deleted file mode 100644 index 3271661a..00000000 --- a/pmctl/main.go +++ /dev/null @@ -1,227 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "log" - "os" - "os/signal" - "strings" - "syscall" - - "github.com/safing/portbase/dataroot" - "github.com/safing/portbase/info" - portlog "github.com/safing/portbase/log" - "github.com/safing/portbase/updater" - "github.com/safing/portbase/utils" - - "github.com/spf13/cobra" -) - -var ( - dataDir string - databaseDir string - dataRoot *utils.DirStructure - logsRoot *utils.DirStructure - - showShortVersion bool - showFullVersion bool - - // create registry - registry = &updater.ResourceRegistry{ - Name: "updates", - UpdateURLs: []string{ - "https://updates.safing.io", - }, - Beta: false, - DevMode: false, - Online: true, // is disabled later based on command - } - - rootCmd = &cobra.Command{ - Use: "portmaster-control", - Short: "Controller for all portmaster components", - PersistentPreRunE: cmdSetup, - RunE: func(cmd *cobra.Command, args []string) error { - if showShortVersion { - fmt.Println(info.Version()) - return nil - } - if showFullVersion { - fmt.Println(info.FullVersion()) - return nil - } - return cmd.Help() - }, - SilenceUsage: true, - } -) - -func init() { - // Let cobra ignore if we are running as "GUI" or not - cobra.MousetrapHelpText = "" - - rootCmd.PersistentFlags().StringVar(&dataDir, "data", "", "Configures the data directory. Alternatively, this can also be set via the environment variable PORTMASTER_DATA.") - rootCmd.PersistentFlags().StringVar(&databaseDir, "db", "", "Alias to --data (deprecated)") - _ = rootCmd.MarkPersistentFlagDirname("data") - _ = rootCmd.MarkPersistentFlagDirname("db") - rootCmd.Flags().BoolVar(&showFullVersion, "version", false, "Print version of portmaster-control.") - rootCmd.Flags().BoolVar(&showShortVersion, "ver", false, "Print version number only") -} - -func main() { - // set meta info - info.Set("Portmaster Control", "0.3.5", "AGPLv3", false) - - // for debugging - // log.Start() - // log.SetLogLevel(log.TraceLevel) - // go func() { - // time.Sleep(3 * time.Second) - // pprof.Lookup("goroutine").WriteTo(os.Stdout, 2) - // os.Exit(1) - // }() - - // catch interrupt for clean shutdown - signalCh := make(chan os.Signal, 2) - signal.Notify( - signalCh, - os.Interrupt, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // start root command - go func() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } - os.Exit(0) - }() - - // for debugging windows service (no stdout/err) - // go func() { - // time.Sleep(10 * time.Second) - // // initiateShutdown(nil) - // // logControlStack() - // }() - - // wait for signals - for sig := range signalCh { - if childIsRunning.IsSet() { - log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig) - } else { - log.Printf("got %s signal, exiting... (not executing anything)\n", sig) - os.Exit(0) - } - } -} - -func cmdSetup(cmd *cobra.Command, args []string) (err error) { - // check if we are running in a console (try to attach to parent console if available) - runningInConsole, err = attachToParentConsole() - if err != nil { - log.Printf("failed to attach to parent console: %s\n", err) - os.Exit(1) - } - - // check if meta info is ok - err = info.CheckVersion() - if err != nil { - fmt.Println("compile error: please compile using the provided build script") - os.Exit(1) - } - - // set up logging - log.SetFlags(log.Ldate | log.Ltime | log.LUTC) - log.SetPrefix("[control] ") - log.SetOutput(os.Stdout) - - // not using portbase logger - portlog.SetLogLevel(portlog.CriticalLevel) - - // data directory - if !showShortVersion && !showFullVersion { - // set data root - // backwards compatibility - if dataDir == "" { - dataDir = databaseDir - } - - // check for environment variable - // PORTMASTER_DATA - if dataDir == "" { - dataDir = os.Getenv("PORTMASTER_DATA") - } - - // check data dir - if dataDir == "" { - return errors.New("please set the data directory using --data=/path/to/data/dir") - } - - // remove redundant escape characters and quotes - dataDir = strings.Trim(dataDir, `\"`) - // initialize dataroot - err = dataroot.Initialize(dataDir, 0755) - if err != nil { - return fmt.Errorf("failed to initialize data root: %s", err) - } - dataRoot = dataroot.Root() - - // initialize registry - err := registry.Initialize(dataRoot.ChildDir("updates", 0755)) - if err != nil { - return err - } - - registry.AddIndex(updater.Index{ - Path: "stable.json", - Stable: true, - Beta: false, - }) - - // TODO: enable loading beta versions - // registry.AddIndex(updater.Index{ - // Path: "beta.json", - // Stable: false, - // Beta: true, - // }) - - updateRegistryIndex() - } - - // logs and warning - if !showShortVersion && !showFullVersion && !strings.Contains(cmd.CommandPath(), " show ") { - // set up logs root - logsRoot = dataRoot.ChildDir("logs", 0777) - err = logsRoot.Ensure() - if err != nil { - return fmt.Errorf("failed to initialize logs root: %s", err) - } - - // warn about CTRL-C on windows - if runningInConsole && onWindows { - log.Println("WARNING: portmaster-control is marked as a GUI application in order to get rid of the console window.") - log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.") - } - } - - return nil -} - -func updateRegistryIndex() { - err := registry.LoadIndexes(context.TODO()) - if err != nil { - log.Printf("WARNING: error loading indexes: %s\n", err) - } - - err = registry.ScanStorage("") - if err != nil { - log.Printf("WARNING: error during storage scan: %s\n", err) - } - - registry.SelectVersions() -} diff --git a/pmctl/run.go b/pmctl/run.go deleted file mode 100644 index eb5fc367..00000000 --- a/pmctl/run.go +++ /dev/null @@ -1,406 +0,0 @@ -package main - -import ( - "fmt" - "io" - "log" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "github.com/spf13/cobra" - "github.com/tevino/abool" -) - -const ( - restartCode = 23 -) - -var ( - runningInConsole bool - onWindows = runtime.GOOS == "windows" - - childIsRunning = abool.NewBool(false) -) - -// Options for starting component -type Options struct { - 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() { - rootCmd.AddCommand(runCmd) - runCmd.AddCommand(runCore) - runCmd.AddCommand(runApp) - runCmd.AddCommand(runNotifier) -} - -var runCmd = &cobra.Command{ - Use: "run", - Short: "Run a Portmaster component in the foreground", -} - -var runCore = &cobra.Command{ - Use: "core", - Short: "Run the Portmaster Core", - RunE: func(cmd *cobra.Command, args []string) error { - return handleRun(cmd, &Options{ - Identifier: "core/portmaster-core", - AllowDownload: true, - AllowHidingWindow: true, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -var runApp = &cobra.Command{ - Use: "app", - Short: "Run the Portmaster App", - RunE: func(cmd *cobra.Command, args []string) error { - return handleRun(cmd, &Options{ - Identifier: "app/portmaster-app", - AllowDownload: false, - AllowHidingWindow: false, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -var runNotifier = &cobra.Command{ - Use: "notifier", - Short: "Run the Portmaster Notifier", - RunE: func(cmd *cobra.Command, args []string) error { - return handleRun(cmd, &Options{ - Identifier: "notifier/portmaster-notifier", - AllowDownload: false, - AllowHidingWindow: true, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -func handleRun(cmd *cobra.Command, opts *Options) (err error) { - err = run(cmd, opts) - initiateShutdown(err) - return -} - -func run(cmd *cobra.Command, opts *Options) (err error) { //nolint:gocognit - - // set download option - registry.Online = opts.AllowDownload - - // parse identifier - opts.ShortIdentifier = path.Dir(opts.Identifier) - - // check for concurrent error (eg. service) - shutdownLock.Lock() - alreadyDead := shutdownInitiated - shutdownLock.Unlock() - if alreadyDead { - return - } - - // 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{}{} - }() - - // get original arguments - var args []string - if len(os.Args) < 4 { - return cmd.Help() - } - args = os.Args[3:] - if opts.SuppressArgs { - args = nil - } - - // adapt identifier - if onWindows { - opts.Identifier += ".exe" - } - - // setup logging - // init log file - logFile := initControlLogFile() - 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)) - } - } - - // run - tries := 0 - for { - // normal execution - tryAgain := false - tryAgain, err = execute(opts, args) - switch { - case tryAgain && err != nil: - // temporary? execution error - log.Printf("execution of %s failed: %s\n", opts.Identifier, err) - tries++ - if tries >= 5 { - log.Println("error seems to be permanent, giving up...") - return err - } - // resilience - time.Sleep(time.Duration(tries) * 2 * time.Second) - if tries >= 2 { - // try updating - updateRegistryIndex() - } - log.Println("trying again...") - case tryAgain && err == nil: - // reset error count - tries = 0 - // upgrade - log.Println("restarting by request...") - // update index - log.Println("checking versions...") - updateRegistryIndex() - case !tryAgain && err != nil: - // fatal error - return err - case !tryAgain && err == nil: - // clean exit - log.Printf("%s completed successfully\n", opts.Identifier) - return nil - } - } -} - -// nolint:gocyclo,gocognit // TODO: simplify -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: %s", err) - } - - // check permission - if !onWindows { - info, err := os.Stat(file.Path()) - if err != nil { - return true, fmt.Errorf("failed to get file info on %s: %s", file.Path(), err) - } - if info.Mode() != 0755 { - err := os.Chmod(file.Path(), 0755) - if err != nil { - return true, fmt.Errorf("failed to set exec permissions on %s: %s", file.Path(), err) - } - } - } - - log.Printf("starting %s %s\n", file.Path(), strings.Join(args, " ")) - - // log files - var logFile, errorFile *os.File - logFileBasePath := filepath.Join(logsRoot.Path, opts.ShortIdentifier) - err = logsRoot.EnsureAbsPath(logFileBasePath) - if err != nil { - log.Printf("failed to check/create log file dir %s: %s\n", logFileBasePath, err) - } else { - // open log file - logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.log", time.Now().UTC().Format("2006-01-02-15-04-05"))) - logFile = initializeLogFile(logFilePath, opts.Identifier, file.Version()) - if logFile != nil { - defer finalizeLogFile(logFile, logFilePath) - } - // open error log file - errorFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.error.log", time.Now().UTC().Format("2006-01-02-15-04-05"))) - errorFile = initializeLogFile(errorFilePath, opts.Identifier, file.Version()) - if errorFile != nil { - defer finalizeLogFile(errorFile, errorFilePath) - } - } - - // 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) - } - - // check if input signals are enabled - inputSignalsEnabled := false - for _, arg := range args { - if strings.HasSuffix(arg, "-input-signals") { - inputSignalsEnabled = true - break - } - } - - // consume stdout/stderr - stdout, err := exc.StdoutPipe() - if err != nil { - return true, fmt.Errorf("failed to connect stdout: %s", err) - } - stderr, err := exc.StderrPipe() - if err != nil { - return true, fmt.Errorf("failed to connect stderr: %s", err) - } - var stdin io.WriteCloser - if inputSignalsEnabled { - stdin, err = exc.StdinPipe() - if err != nil { - return true, fmt.Errorf("failed to connect stdin: %s", err) - } - } - - // start - err = exc.Start() - if err != nil { - return true, fmt.Errorf("failed to start %s: %s", opts.Identifier, err) - } - childIsRunning.Set() - - // start output writers - var wg sync.WaitGroup - wg.Add(2) - go func() { - var logFileError error - if logFile == nil { - _, logFileError = io.Copy(os.Stdout, stdout) - } else { - if opts.NoOutput { - _, logFileError = io.Copy(logFile, stdout) - } else { - _, logFileError = io.Copy(io.MultiWriter(os.Stdout, logFile), stdout) - } - } - if logFileError != nil { - log.Printf("failed write logs: %s\n", logFileError) - } - wg.Done() - }() - go func() { - var errorFileError error - if logFile == nil { - _, errorFileError = io.Copy(os.Stderr, stderr) - } else { - if opts.NoOutput { - _, errorFileError = io.Copy(errorFile, stderr) - } else { - _, errorFileError = io.Copy(io.MultiWriter(os.Stderr, errorFile), stderr) - } - } - if errorFileError != nil { - log.Printf("failed write error logs: %s\n", errorFileError) - } - wg.Done() - }() - - // wait for completion - finished := make(chan error) - go func() { - // wait for output writers to complete - wg.Wait() - // wait for process to return - finished <- exc.Wait() - // update status - childIsRunning.UnSet() - // notify manager - close(finished) - }() - - // state change listeners - for { - select { - case <-shuttingDown: - // signal process shutdown - if inputSignalsEnabled { - // for windows - _, err = stdin.Write([]byte("SIGINT\n")) - } else { - err = exc.Process.Signal(os.Interrupt) - } - if 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: %s", 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: - if err != nil { - exErr, ok := err.(*exec.ExitError) - if ok { - switch exErr.ProcessState.ExitCode() { - case 0: - // clean exit - return false, fmt.Errorf("clean exit, but with error: %s", err) - case 1: - // error exit - return true, fmt.Errorf("error during execution: %s", err) - case restartCode: - // restart request - log.Printf("restarting %s\n", opts.Identifier) - return true, nil - default: - return true, fmt.Errorf("unexpected error during execution: %s", err) - } - } else { - return true, fmt.Errorf("unexpected error type during execution: %s", err) - } - } - // clean exit - return false, nil - } - } -} diff --git a/pmctl/show.go b/pmctl/show.go deleted file mode 100644 index ab89d13d..00000000 --- a/pmctl/show.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(showCmd) - showCmd.AddCommand(showCore) - showCmd.AddCommand(showApp) - showCmd.AddCommand(showNotifier) -} - -var showCmd = &cobra.Command{ - Use: "show", - Short: "Show the command to run a Portmaster component yourself", -} - -var showCore = &cobra.Command{ - Use: "core", - Short: "Show command to run the Portmaster Core", - RunE: func(cmd *cobra.Command, args []string) error { - return show(cmd, &Options{ - Identifier: "core/portmaster-core", - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -var showApp = &cobra.Command{ - Use: "app", - Short: "Show command to run the Portmaster App", - RunE: func(cmd *cobra.Command, args []string) error { - return show(cmd, &Options{ - Identifier: "app/portmaster-app", - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -var showNotifier = &cobra.Command{ - Use: "notifier", - Short: "Show command to run the Portmaster Notifier", - RunE: func(cmd *cobra.Command, args []string) error { - return show(cmd, &Options{ - Identifier: "notifier/portmaster-notifier", - SuppressArgs: true, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -func show(cmd *cobra.Command, opts *Options) error { - // get original arguments - var args []string - if len(os.Args) < 4 { - return cmd.Help() - } - args = os.Args[3:] - if opts.SuppressArgs { - args = nil - } - - // adapt identifier - if onWindows { - opts.Identifier += ".exe" - } - - file, err := registry.GetFile(platform(opts.Identifier)) - if err != nil { - return fmt.Errorf("could not get component: %s", err) - } - - fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " ")) - - return nil -} diff --git a/pmctl/snoretoast_windows.go b/pmctl/snoretoast_windows.go deleted file mode 100644 index 75421253..00000000 --- a/pmctl/snoretoast_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import "github.com/spf13/cobra" - -func init() { - showCmd.AddCommand(showSnoreToast) - runCmd.AddCommand(runSnoreToast) -} - -var showSnoreToast = &cobra.Command{ - Use: "notifier-snoretoast", - Short: "Show command to run the Notifier component SnoreToast", - RunE: func(cmd *cobra.Command, args []string) error { - return show(cmd, &Options{ - Identifier: "notifier/portmaster-snoretoast", - SuppressArgs: true, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -} - -var runSnoreToast = &cobra.Command{ - Use: "notifier-snoretoast", - Short: "Run the Notifier component SnoreToast", - RunE: func(cmd *cobra.Command, args []string) error { - return handleRun(cmd, &Options{ - Identifier: "notifier/portmaster-snoretoast", - AllowDownload: false, - AllowHidingWindow: true, - SuppressArgs: true, - }) - }, - FParseErrWhitelist: cobra.FParseErrWhitelist{ - // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags - UnknownFlags: true, - }, -}