mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
|
@ -35,14 +35,8 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// else create new lock
|
||||
return 0, createInstanceLock(lockFilePath)
|
|
@ -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 {
|
||||
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
|
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"
|
||||
|
||||
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 {
|
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,
|
||||
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()
|
||||
}()
|
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
|
||||
}
|
|
@ -7,9 +7,8 @@ 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
|
||||
//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()
|
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 {
|
||||
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"),
|
||||
}
|
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