mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
Refactor pmctl into new portmaster-start
This commit is contained in:
parent
7b12384b63
commit
58dad190a1
21 changed files with 819 additions and 868 deletions
6
cmds/portmaster-start/.gitignore
vendored
Normal file
6
cmds/portmaster-start/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# binaries
|
||||||
|
portmaster-start
|
||||||
|
portmaster-start.exe
|
||||||
|
|
||||||
|
# test dir
|
||||||
|
test
|
|
@ -19,9 +19,7 @@ import (
|
||||||
"golang.org/x/sys/windows/svc/mgr"
|
"golang.org/x/sys/windows/svc/mgr"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const exeSuffix = ".exe"
|
||||||
exeSuffix = ".exe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(installCmd)
|
rootCmd.AddCommand(installCmd)
|
||||||
|
@ -57,39 +55,18 @@ var uninstallService = &cobra.Command{
|
||||||
RunE: uninstallWindowsService,
|
RunE: uninstallWindowsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExePath() (string, error) {
|
func getAbsBinaryPath() (string, error) {
|
||||||
// get own filepath
|
p, err := filepath.Abs(os.Args[0])
|
||||||
prog := os.Args[0]
|
|
||||||
p, err := filepath.Abs(prog)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// check if the path is valid
|
|
||||||
fi, err := os.Stat(p)
|
return p, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServiceExecCommand(exePath string, escape bool) []string {
|
func getServiceExecCommand(exePath string, escape bool) []string {
|
||||||
return []string{
|
return []string{
|
||||||
maybeEscape(exePath, escape),
|
maybeEscape(exePath, escape),
|
||||||
"run",
|
|
||||||
"core-service",
|
"core-service",
|
||||||
"--data",
|
"--data",
|
||||||
maybeEscape(dataRoot.Path, escape),
|
maybeEscape(dataRoot.Path, escape),
|
||||||
|
@ -126,7 +103,7 @@ func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uin
|
||||||
|
|
||||||
func installWindowsService(cmd *cobra.Command, args []string) error {
|
func installWindowsService(cmd *cobra.Command, args []string) error {
|
||||||
// get exe path
|
// get exe path
|
||||||
exePath, err := getExePath()
|
exePath, err := getAbsBinaryPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get exe path: %s", err)
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
// open service
|
||||||
s, err := m.OpenService(serviceName)
|
s, err := m.OpenService(serviceName)
|
|
@ -35,13 +35,7 @@ func checkAndCreateInstanceLock(name string) (pid int32, err error) {
|
||||||
// check if process exists
|
// check if process exists
|
||||||
p, err := processInfo.NewProcess(int32(parsedPid))
|
p, err := processInfo.NewProcess(int32(parsedPid))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// TODO: remove this workaround as soon as NewProcess really returns an error on windows when the process does not exist
|
return p.Pid, nil
|
||||||
// Issue: https://github.com/shirou/gopsutil/issues/729
|
|
||||||
_, err = p.Name()
|
|
||||||
if err == nil {
|
|
||||||
// process exists
|
|
||||||
return p.Pid, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// else create new lock
|
// else create new lock
|
|
@ -35,7 +35,7 @@ func initializeLogFile(logFilePath string, identifier string, version string) *o
|
||||||
metaSection, err := dsd.Dump(meta, dsd.JSON)
|
metaSection, err := dsd.Dump(meta, dsd.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err)
|
log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err)
|
||||||
finalizeLogFile(logFile, logFilePath)
|
finalizeLogFile(logFile)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.AppendAsBlock(metaSection)
|
c.AppendAsBlock(metaSection)
|
||||||
|
@ -46,14 +46,16 @@ func initializeLogFile(logFilePath string, identifier string, version string) *o
|
||||||
_, err = logFile.Write(c.CompileData())
|
_, err = logFile.Write(c.CompileData())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to write header for log file %s: %s\n", logFilePath, err)
|
log.Printf("failed to write header for log file %s: %s\n", logFilePath, err)
|
||||||
finalizeLogFile(logFile, logFilePath)
|
finalizeLogFile(logFile)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return logFile
|
return logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func finalizeLogFile(logFile *os.File, logFilePath string) {
|
func finalizeLogFile(logFile *os.File) {
|
||||||
|
logFilePath := logFile.Name()
|
||||||
|
|
||||||
err := logFile.Close()
|
err := logFile.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to close log file %s: %s\n", logFilePath, err)
|
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
|
// check file size
|
||||||
stat, err := os.Stat(logFilePath)
|
stat, err := os.Stat(logFilePath)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
// delete if file is smaller than
|
return
|
||||||
if stat.Size() < 200 { // header + info is about 150 bytes
|
}
|
||||||
err := os.Remove(logFilePath)
|
|
||||||
if err != nil {
|
// delete if file is smaller than
|
||||||
log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err)
|
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
|
// check logging dir
|
||||||
logFileBasePath := filepath.Join(logsRoot.Path, "control")
|
logFileBasePath := filepath.Join(logsRoot.Path, options.ShortIdentifier)
|
||||||
err := logsRoot.EnsureAbsPath(logFileBasePath)
|
err := logsRoot.EnsureAbsPath(logFileBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
|
log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// open log file
|
// open log file
|
||||||
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.log", time.Now().UTC().Format("2006-01-02-15-04-05")))
|
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s%s", time.Now().UTC().Format("2006-01-02-15-04-05"), ext))
|
||||||
return initializeLogFile(logFilePath, "control/portmaster-control", info.Version())
|
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
|
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
||||||
|
@ -92,44 +104,25 @@ func logControlError(cErr error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check logging dir
|
errorFile := getPmStartLogFile(".error.log")
|
||||||
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())
|
|
||||||
if errorFile == nil {
|
if errorFile == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer errorFile.Close()
|
||||||
|
|
||||||
// write error and close
|
|
||||||
fmt.Fprintln(errorFile, cErr.Error())
|
fmt.Fprintln(errorFile, cErr.Error())
|
||||||
errorFile.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:deadcode,unused // TODO
|
//nolint:deadcode,unused // TODO
|
||||||
func logControlStack() {
|
func logControlStack() {
|
||||||
// check logging dir
|
fp := getPmStartLogFile(".stack.log")
|
||||||
logFileBasePath := filepath.Join(logsRoot.Path, "control")
|
if fp == nil {
|
||||||
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 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
// write error and close
|
// write error and close
|
||||||
_ = pprof.Lookup("goroutine").WriteTo(errorFile, 2)
|
_ = pprof.Lookup("goroutine").WriteTo(fp, 2)
|
||||||
errorFile.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
221
cmds/portmaster-start/main.go
Normal file
221
cmds/portmaster-start/main.go
Normal file
|
@ -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()
|
||||||
|
}
|
|
@ -8,15 +8,15 @@ COL_BOLD="\033[01;01m"
|
||||||
COL_RED="\033[31m"
|
COL_RED="\033[31m"
|
||||||
|
|
||||||
destDirPart1="../dist"
|
destDirPart1="../dist"
|
||||||
destDirPart2="control"
|
destDirPart2="start"
|
||||||
|
|
||||||
function check {
|
function check {
|
||||||
# output
|
# output
|
||||||
output="pmctl"
|
output="portmaster-start"
|
||||||
# get version
|
# get version
|
||||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
||||||
# build versioned file name
|
# build versioned file name
|
||||||
filename="portmaster-control_v${version//./-}"
|
filename="portmaster-start_v${version//./-}"
|
||||||
# platform
|
# platform
|
||||||
platform="${GOOS}_${GOARCH}"
|
platform="${GOOS}_${GOARCH}"
|
||||||
if [[ $GOOS == "windows" ]]; then
|
if [[ $GOOS == "windows" ]]; then
|
||||||
|
@ -28,19 +28,19 @@ function check {
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[control] $platform $version already built"
|
echo "[start] $platform $version already built"
|
||||||
else
|
else
|
||||||
echo -e "${COL_BOLD}[control] $platform $version${COL_OFF}"
|
echo -e "${COL_BOLD}[start] $platform $version${COL_OFF}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
# output
|
# output
|
||||||
output="pmctl"
|
output="portmaster-start"
|
||||||
# get version
|
# get version
|
||||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
||||||
# build versioned file name
|
# build versioned file name
|
||||||
filename="portmaster-control_v${version//./-}"
|
filename="portmaster-start_v${version//./-}"
|
||||||
# platform
|
# platform
|
||||||
platform="${GOOS}_${GOARCH}"
|
platform="${GOOS}_${GOARCH}"
|
||||||
if [[ $GOOS == "windows" ]]; then
|
if [[ $GOOS == "windows" ]]; then
|
||||||
|
@ -52,19 +52,19 @@ function build {
|
||||||
|
|
||||||
# check if file exists
|
# check if file exists
|
||||||
if [[ -f $destPath ]]; then
|
if [[ -f $destPath ]]; then
|
||||||
echo "[control] $platform already built in version $version, skipping..."
|
echo "[start] $platform already built in version $version, skipping..."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# build
|
# build
|
||||||
./build
|
./build
|
||||||
if [[ $? -ne 0 ]]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p $(dirname $destPath)
|
mkdir -p $(dirname $destPath)
|
||||||
cp $output $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 {
|
function check_all {
|
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)
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ var (
|
||||||
AllowDownload: true,
|
AllowDownload: true,
|
||||||
AllowHidingWindow: false,
|
AllowHidingWindow: false,
|
||||||
NoOutput: true,
|
NoOutput: true,
|
||||||
})
|
}, args)
|
||||||
}),
|
}),
|
||||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
@ -41,7 +41,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.AddCommand(runCoreService)
|
rootCmd.AddCommand(runCoreService)
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceName = "PortmasterCore"
|
const serviceName = "PortmasterCore"
|
||||||
|
@ -88,7 +88,7 @@ service:
|
||||||
return ssec, errno
|
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
|
// check if we are running interactively
|
||||||
isDebug, err := svc.IsAnInteractiveSession()
|
isDebug, err := svc.IsAnInteractiveSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,7 +122,8 @@ func runService(cmd *cobra.Command, opts *Options) error {
|
||||||
go func() {
|
go func() {
|
||||||
// run slightly delayed
|
// run slightly delayed
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
_ = handleRun(cmd, opts) // error handled by shutdown routine
|
err := run(cmd, opts, getExecArgs(opts, cmdArgs))
|
||||||
|
initiateShutdown(err)
|
||||||
finishWg.Done()
|
finishWg.Done()
|
||||||
runWg.Done()
|
runWg.Done()
|
||||||
}()
|
}()
|
41
cmds/portmaster-start/show.go
Normal file
41
cmds/portmaster-start/show.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -5,11 +5,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
startupComplete = make(chan struct{}) // signal that the start procedure completed (is never closed, just signaled once)
|
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)
|
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
|
|
||||||
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
//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
|
shutdownLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,13 +16,24 @@ func initiateShutdown(err error) {
|
||||||
shutdownLock.Lock()
|
shutdownLock.Lock()
|
||||||
defer shutdownLock.Unlock()
|
defer shutdownLock.Unlock()
|
||||||
|
|
||||||
if !shutdownInitiated {
|
select {
|
||||||
shutdownInitiated = true
|
case <-shuttingDown:
|
||||||
|
return
|
||||||
|
default:
|
||||||
shutdownError = err
|
shutdownError = err
|
||||||
close(shuttingDown)
|
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
|
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
||||||
func getShutdownError() error {
|
func getShutdownError() error {
|
||||||
shutdownLock.Lock()
|
shutdownLock.Lock()
|
14
cmds/portmaster-start/snoretoast_windows.go
Normal file
14
cmds/portmaster-start/snoretoast_windows.go
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ func downloadUpdates() error {
|
||||||
if onWindows {
|
if onWindows {
|
||||||
registry.MandatoryUpdates = []string{
|
registry.MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core.exe"),
|
platform("core/portmaster-core.exe"),
|
||||||
platform("control/portmaster-control.exe"),
|
platform("start/portmaster-start.exe"),
|
||||||
platform("app/portmaster-app.exe"),
|
platform("app/portmaster-app.exe"),
|
||||||
platform("notifier/portmaster-notifier.exe"),
|
platform("notifier/portmaster-notifier.exe"),
|
||||||
platform("notifier/portmaster-snoretoast.exe"),
|
platform("notifier/portmaster-snoretoast.exe"),
|
||||||
|
@ -34,7 +34,7 @@ func downloadUpdates() error {
|
||||||
} else {
|
} else {
|
||||||
registry.MandatoryUpdates = []string{
|
registry.MandatoryUpdates = []string{
|
||||||
platform("core/portmaster-core"),
|
platform("core/portmaster-core"),
|
||||||
platform("control/portmaster-control"),
|
platform("start/portmaster-start"),
|
||||||
platform("app/portmaster-app"),
|
platform("app/portmaster-app"),
|
||||||
platform("notifier/portmaster-notifier"),
|
platform("notifier/portmaster-notifier"),
|
||||||
}
|
}
|
79
cmds/portmaster-start/version.go
Normal file
79
cmds/portmaster-start/version.go
Normal file
|
@ -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)
|
||||||
|
}
|
6
pmctl/.gitignore
vendored
6
pmctl/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
||||||
# binaries
|
|
||||||
pmctl
|
|
||||||
pmctl.exe
|
|
||||||
|
|
||||||
# test dir
|
|
||||||
test
|
|
227
pmctl/main.go
227
pmctl/main.go
|
@ -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()
|
|
||||||
}
|
|
406
pmctl/run.go
406
pmctl/run.go
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue