Refactor pmctl into new portmaster-start
This commit is contained in:
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
|
||||
74
cmds/portmaster-start/build
Executable file
74
cmds/portmaster-start/build
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
# get build data
|
||||
if [[ "$BUILD_COMMIT" == "" ]]; then
|
||||
BUILD_COMMIT=$(git describe --all --long --abbrev=99 --dirty 2>/dev/null)
|
||||
fi
|
||||
if [[ "$BUILD_USER" == "" ]]; then
|
||||
BUILD_USER=$(id -un)
|
||||
fi
|
||||
if [[ "$BUILD_HOST" == "" ]]; then
|
||||
BUILD_HOST=$(hostname -f)
|
||||
fi
|
||||
if [[ "$BUILD_DATE" == "" ]]; then
|
||||
BUILD_DATE=$(date +%d.%m.%Y)
|
||||
fi
|
||||
if [[ "$BUILD_SOURCE" == "" ]]; then
|
||||
BUILD_SOURCE=$(git remote -v | grep origin | cut -f2 | cut -d" " -f1 | head -n 1)
|
||||
fi
|
||||
if [[ "$BUILD_SOURCE" == "" ]]; then
|
||||
BUILD_SOURCE=$(git remote -v | cut -f2 | cut -d" " -f1 | head -n 1)
|
||||
fi
|
||||
BUILD_BUILDOPTIONS=$(echo $* | sed "s/ /§/g")
|
||||
|
||||
# check
|
||||
if [[ "$BUILD_COMMIT" == "" ]]; then
|
||||
echo "could not automatically determine BUILD_COMMIT, please supply manually as environment variable."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$BUILD_USER" == "" ]]; then
|
||||
echo "could not automatically determine BUILD_USER, please supply manually as environment variable."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$BUILD_HOST" == "" ]]; then
|
||||
echo "could not automatically determine BUILD_HOST, please supply manually as environment variable."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$BUILD_DATE" == "" ]]; then
|
||||
echo "could not automatically determine BUILD_DATE, please supply manually as environment variable."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$BUILD_SOURCE" == "" ]]; then
|
||||
echo "could not automatically determine BUILD_SOURCE, please supply manually as environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# build tools
|
||||
EXTRA_LD_FLAGS=""
|
||||
if [[ $GOOS == "windows" ]]; then
|
||||
# checks
|
||||
if [[ $CC_FOR_windows_amd64 == "" ]]; then
|
||||
echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $CXX_FOR_windows_amd64 == "" ]]; then
|
||||
echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
|
||||
exit 1
|
||||
fi
|
||||
# compilers
|
||||
export CC=$CC_FOR_windows_amd64
|
||||
export CXX=$CXX_FOR_windows_amd64
|
||||
# custom
|
||||
export CGO_ENABLED=1
|
||||
EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available)
|
||||
# generate resource.syso for windows metadata / icon
|
||||
go generate
|
||||
fi
|
||||
|
||||
echo "Please notice, that this build script includes metadata into the build."
|
||||
echo "This information is useful for debugging and license compliance."
|
||||
echo "Run the compiled binary with the -version flag to see the information included."
|
||||
|
||||
# build
|
||||
BUILD_PATH="github.com/safing/portbase/info"
|
||||
go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*
|
||||
12
cmds/portmaster-start/console_default.go
Normal file
12
cmds/portmaster-start/console_default.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func attachToParentConsole() (attached bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func hideWindow(cmd *exec.Cmd) {
|
||||
}
|
||||
150
cmds/portmaster-start/console_windows.go
Normal file
150
cmds/portmaster-start/console_windows.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
// Parts of this file are FORKED
|
||||
// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go
|
||||
// on 16.07.2019
|
||||
// with Apache-2.0 license
|
||||
// authored by https://github.com/apenwarr
|
||||
|
||||
// docs/sources:
|
||||
// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld
|
||||
// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procAttachConsole = kernel32.NewProc("AttachConsole")
|
||||
)
|
||||
|
||||
// Windows console output is a mess.
|
||||
//
|
||||
// If you compile as "-H windows", then if you launch your program without
|
||||
// a console, Windows forcibly creates one to use as your stdin/stdout, which
|
||||
// is silly for a GUI app, so we can't do that.
|
||||
//
|
||||
// If you compile as "-H windowsgui", then it doesn't create a console for
|
||||
// your app... but also doesn't provide a working stdin/stdout/stderr even if
|
||||
// you *did* launch from the console. However, you can use AttachConsole()
|
||||
// to get a handle to your parent process's console, if any, and then
|
||||
// os.NewFile() to turn that handle into a fd usable as stdout/stderr.
|
||||
//
|
||||
// However, then you have the problem that if you redirect stdout or stderr
|
||||
// from the shell, you end up ignoring the redirection by forcing it to the
|
||||
// console.
|
||||
//
|
||||
// To fix *that*, we have to detect whether there was a pre-existing stdout
|
||||
// or not. We can check GetStdHandle(), which returns 0 for "should be
|
||||
// console" and nonzero for "already pointing at a file."
|
||||
//
|
||||
// Be careful though! As soon as you run AttachConsole(), it resets *all*
|
||||
// the GetStdHandle() handles to point them at the console instead, thus
|
||||
// throwing away the original file redirects. So we have to GetStdHandle()
|
||||
// *before* AttachConsole().
|
||||
//
|
||||
// For some reason, powershell redirections provide a valid file handle, but
|
||||
// writing to that handle doesn't write to the file. I haven't found a way
|
||||
// to work around that. (Windows 10.0.17763.379)
|
||||
//
|
||||
// Net result is as follows.
|
||||
// Before:
|
||||
// SHELL NON-REDIRECTED REDIRECTED
|
||||
// explorer.exe no console n/a
|
||||
// cmd.exe broken works
|
||||
// powershell broken broken
|
||||
// WSL bash broken works
|
||||
// After
|
||||
// SHELL NON-REDIRECTED REDIRECTED
|
||||
// explorer.exe no console n/a
|
||||
// cmd.exe works works
|
||||
// powershell works broken
|
||||
// WSL bash works works
|
||||
//
|
||||
// We don't seem to make anything worse, at least.
|
||||
func attachToParentConsole() (attached bool, err error) {
|
||||
// get std handles before we attempt to attach to parent console
|
||||
stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||
stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||
stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||
|
||||
// attempt to attach to parent console
|
||||
err = procAttachConsole.Find()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
r1, _, _ := procAttachConsole.Call(windowsAttachParentProcess)
|
||||
if r1 == 0 {
|
||||
// possible errors:
|
||||
// ERROR_ACCESS_DENIED: already attached to console
|
||||
// ERROR_INVALID_HANDLE: process does not have console
|
||||
// ERROR_INVALID_PARAMETER: process does not exist
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// get std handles after we attached to console
|
||||
var invalid syscall.Handle
|
||||
con := invalid
|
||||
|
||||
if stdin == invalid {
|
||||
stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||
}
|
||||
if stdout == invalid {
|
||||
stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||
con = stdout
|
||||
}
|
||||
if stderr == invalid {
|
||||
stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||
con = stderr
|
||||
}
|
||||
|
||||
// correct output mode
|
||||
if con != invalid {
|
||||
// Make sure the console is configured to convert
|
||||
// \n to \r\n, like Go programs expect.
|
||||
h := windows.Handle(con)
|
||||
var st uint32
|
||||
err := windows.GetConsoleMode(h, &st)
|
||||
if err != nil {
|
||||
log.Printf("failed to get console mode: %s\n", err)
|
||||
} else {
|
||||
err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
if err != nil {
|
||||
log.Printf("failed to set console mode: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix std handles to correct values (ie. redirects)
|
||||
if stdin != invalid {
|
||||
os.Stdin = os.NewFile(uintptr(stdin), "stdin")
|
||||
log.Printf("fixed os.Stdin after attaching to parent console\n")
|
||||
}
|
||||
if stdout != invalid {
|
||||
os.Stdout = os.NewFile(uintptr(stdout), "stdout")
|
||||
log.Printf("fixed os.Stdout after attaching to parent console\n")
|
||||
}
|
||||
if stderr != invalid {
|
||||
os.Stderr = os.NewFile(uintptr(stderr), "stderr")
|
||||
log.Printf("fixed os.Stderr after attaching to parent console\n")
|
||||
}
|
||||
|
||||
log.Println("attached to parent console")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func hideWindow(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
}
|
||||
}
|
||||
182
cmds/portmaster-start/install_windows.go
Normal file
182
cmds/portmaster-start/install_windows.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
// Based on the official Go examples from
|
||||
// https://github.com/golang/sys/blob/master/windows/svc/example
|
||||
// by The Go Authors.
|
||||
// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const exeSuffix = ".exe"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(installCmd)
|
||||
installCmd.AddCommand(installService)
|
||||
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
uninstallCmd.AddCommand(uninstallService)
|
||||
}
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install system integrations",
|
||||
}
|
||||
|
||||
var uninstallCmd = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall system integrations",
|
||||
}
|
||||
|
||||
var installService = &cobra.Command{
|
||||
Use: "core-service",
|
||||
Short: "Install Portmaster Core Windows Service",
|
||||
RunE: installWindowsService,
|
||||
}
|
||||
|
||||
var uninstallService = &cobra.Command{
|
||||
Use: "core-service",
|
||||
Short: "Uninstall Portmaster Core Windows Service",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// non-nil dummy to override db flag requirement
|
||||
return nil
|
||||
},
|
||||
RunE: uninstallWindowsService,
|
||||
}
|
||||
|
||||
func getAbsBinaryPath() (string, error) {
|
||||
p, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func getServiceExecCommand(exePath string, escape bool) []string {
|
||||
return []string{
|
||||
maybeEscape(exePath, escape),
|
||||
"core-service",
|
||||
"--data",
|
||||
maybeEscape(dataRoot.Path, escape),
|
||||
"--input-signals",
|
||||
}
|
||||
}
|
||||
|
||||
func maybeEscape(s string, escape bool) string {
|
||||
if escape {
|
||||
return windows.EscapeArg(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getServiceConfig(exePath string) mgr.Config {
|
||||
return mgr.Config{
|
||||
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
||||
StartType: mgr.StartAutomatic,
|
||||
ErrorControl: mgr.ErrorNormal,
|
||||
BinaryPathName: strings.Join(getServiceExecCommand(exePath, true), " "),
|
||||
DisplayName: "Portmaster Core",
|
||||
Description: "Portmaster Application Firewall - Core Service",
|
||||
}
|
||||
}
|
||||
|
||||
func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uint32) {
|
||||
return []mgr.RecoveryAction{
|
||||
{
|
||||
Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
|
||||
Delay: 1 * time.Minute, // the time to wait before performing the specified action
|
||||
},
|
||||
}, 86400
|
||||
}
|
||||
|
||||
func installWindowsService(cmd *cobra.Command, args []string) error {
|
||||
// get exe path
|
||||
exePath, err := getAbsBinaryPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get exe path: %s", err)
|
||||
}
|
||||
|
||||
// connect to Windows service manager
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to service manager: %s", err)
|
||||
}
|
||||
defer m.Disconnect() //nolint:errcheck // TODO
|
||||
|
||||
// open service
|
||||
created := false
|
||||
s, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
// create service
|
||||
cmd := getServiceExecCommand(exePath, false)
|
||||
s, err = m.CreateService(serviceName, cmd[0], getServiceConfig(exePath), cmd[1:]...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create service: %s", err)
|
||||
}
|
||||
defer s.Close()
|
||||
created = true
|
||||
} else {
|
||||
// update service
|
||||
err = s.UpdateConfig(getServiceConfig(exePath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update service: %s", err)
|
||||
}
|
||||
defer s.Close()
|
||||
}
|
||||
|
||||
// update recovery actions
|
||||
err = s.SetRecoveryActions(getRecoveryActions())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update recovery actions: %s", err)
|
||||
}
|
||||
|
||||
if created {
|
||||
log.Printf("created service %s\n", serviceName)
|
||||
} else {
|
||||
log.Printf("updated service %s\n", serviceName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallWindowsService(cmd *cobra.Command, args []string) error {
|
||||
// connect to Windows service manager
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %s is not installed", serviceName)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
_, err = s.Control(svc.Stop)
|
||||
if err != nil {
|
||||
log.Printf("failed to stop service: %s\n", err)
|
||||
}
|
||||
|
||||
// delete service
|
||||
err = s.Delete()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete service: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("uninstalled service %s\n", serviceName)
|
||||
return nil
|
||||
}
|
||||
64
cmds/portmaster-start/lock.go
Normal file
64
cmds/portmaster-start/lock.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
)
|
||||
|
||||
func checkAndCreateInstanceLock(name string) (pid int32, err error) {
|
||||
lockFilePath := filepath.Join(dataRoot.Path, fmt.Sprintf("%s-lock.pid", name))
|
||||
|
||||
// read current pid file
|
||||
data, err := ioutil.ReadFile(lockFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// create new lock
|
||||
return 0, createInstanceLock(lockFilePath)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// file exists!
|
||||
parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err)
|
||||
return 0, createInstanceLock(lockFilePath)
|
||||
}
|
||||
|
||||
// check if process exists
|
||||
p, err := processInfo.NewProcess(int32(parsedPid))
|
||||
if err == nil {
|
||||
return p.Pid, nil
|
||||
}
|
||||
|
||||
// else create new lock
|
||||
return 0, createInstanceLock(lockFilePath)
|
||||
}
|
||||
|
||||
func createInstanceLock(lockFilePath string) error {
|
||||
// check data root dir
|
||||
err := dataRoot.Ensure()
|
||||
if err != nil {
|
||||
log.Printf("failed to check data root dir: %s\n", err)
|
||||
}
|
||||
|
||||
// create lock file
|
||||
err = ioutil.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteInstanceLock(name string) error {
|
||||
lockFilePath := filepath.Join(dataRoot.Path, fmt.Sprintf("%s-lock.pid", name))
|
||||
return os.Remove(lockFilePath)
|
||||
}
|
||||
137
cmds/portmaster-start/logs.go
Normal file
137
cmds/portmaster-start/logs.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/container"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func initializeLogFile(logFilePath string, identifier string, version string) *os.File {
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0444)
|
||||
if err != nil {
|
||||
log.Printf("failed to create log file %s: %s\n", logFilePath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// create header, so that the portmaster can view log files as a database
|
||||
meta := record.Meta{}
|
||||
meta.Update()
|
||||
meta.SetAbsoluteExpiry(time.Now().Add(720 * time.Hour).Unix()) // one month
|
||||
|
||||
// manually marshal
|
||||
// version
|
||||
c := container.New([]byte{1})
|
||||
// meta
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
c.AppendAsBlock(metaSection)
|
||||
// log file data type (string) and newline for better manual viewing
|
||||
c.Append([]byte("S\n"))
|
||||
c.Append([]byte(fmt.Sprintf("executing %s version %s on %s %s\n", identifier, version, runtime.GOOS, runtime.GOARCH)))
|
||||
|
||||
_, err = logFile.Write(c.CompileData())
|
||||
if err != nil {
|
||||
log.Printf("failed to write header for log file %s: %s\n", logFilePath, err)
|
||||
finalizeLogFile(logFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
return logFile
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// check file size
|
||||
stat, err := os.Stat(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 getLogFile(options *Options, version, ext string) *os.File {
|
||||
// check logging dir
|
||||
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%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
|
||||
func logControlError(cErr error) {
|
||||
// check if error present
|
||||
if cErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
errorFile := getPmStartLogFile(".error.log")
|
||||
if errorFile == nil {
|
||||
return
|
||||
}
|
||||
defer errorFile.Close()
|
||||
|
||||
fmt.Fprintln(errorFile, cErr.Error())
|
||||
}
|
||||
|
||||
//nolint:deadcode,unused // TODO
|
||||
func logControlStack() {
|
||||
fp := getPmStartLogFile(".stack.log")
|
||||
if fp == nil {
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
// write error and close
|
||||
_ = pprof.Lookup("goroutine").WriteTo(fp, 2)
|
||||
}
|
||||
|
||||
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
||||
func runAndLogControlError(wrappedFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
err := wrappedFunc(cmd, args)
|
||||
if err != nil {
|
||||
logControlError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
102
cmds/portmaster-start/pack
Executable file
102
cmds/portmaster-start/pack
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
||||
cd "$baseDir"
|
||||
|
||||
COL_OFF="\033[00m"
|
||||
COL_BOLD="\033[01;01m"
|
||||
COL_RED="\033[31m"
|
||||
|
||||
destDirPart1="../dist"
|
||||
destDirPart2="start"
|
||||
|
||||
function check {
|
||||
# output
|
||||
output="portmaster-start"
|
||||
# get version
|
||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
||||
# build versioned file name
|
||||
filename="portmaster-start_v${version//./-}"
|
||||
# platform
|
||||
platform="${GOOS}_${GOARCH}"
|
||||
if [[ $GOOS == "windows" ]]; then
|
||||
filename="${filename}.exe"
|
||||
output="${output}.exe"
|
||||
fi
|
||||
# build destination path
|
||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
||||
|
||||
# check if file exists
|
||||
if [[ -f $destPath ]]; then
|
||||
echo "[start] $platform $version already built"
|
||||
else
|
||||
echo -e "${COL_BOLD}[start] $platform $version${COL_OFF}"
|
||||
fi
|
||||
}
|
||||
|
||||
function build {
|
||||
# output
|
||||
output="portmaster-start"
|
||||
# get version
|
||||
version=$(grep "info.Set" main.go | cut -d'"' -f4)
|
||||
# build versioned file name
|
||||
filename="portmaster-start_v${version//./-}"
|
||||
# platform
|
||||
platform="${GOOS}_${GOARCH}"
|
||||
if [[ $GOOS == "windows" ]]; then
|
||||
filename="${filename}.exe"
|
||||
output="${output}.exe"
|
||||
fi
|
||||
# build destination path
|
||||
destPath=${destDirPart1}/${platform}/${destDirPart2}/$filename
|
||||
|
||||
# check if file exists
|
||||
if [[ -f $destPath ]]; then
|
||||
echo "[start] $platform already built in version $version, skipping..."
|
||||
return
|
||||
fi
|
||||
|
||||
# build
|
||||
./build
|
||||
if [[ $? -ne 0 ]]; then
|
||||
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}[start] $platform: successfully built.${COL_OFF}"
|
||||
}
|
||||
|
||||
function check_all {
|
||||
GOOS=linux GOARCH=amd64 check
|
||||
GOOS=windows GOARCH=amd64 check
|
||||
GOOS=darwin GOARCH=amd64 check
|
||||
}
|
||||
|
||||
function build_all {
|
||||
GOOS=linux GOARCH=amd64 build
|
||||
GOOS=windows GOARCH=amd64 build
|
||||
GOOS=darwin GOARCH=amd64 build
|
||||
}
|
||||
|
||||
case $1 in
|
||||
"check" )
|
||||
check_all
|
||||
;;
|
||||
"build" )
|
||||
build_all
|
||||
;;
|
||||
* )
|
||||
echo ""
|
||||
echo "build list:"
|
||||
echo ""
|
||||
check_all
|
||||
echo ""
|
||||
read -p "press [Enter] to start building" x
|
||||
echo ""
|
||||
build_all
|
||||
echo ""
|
||||
echo "finished building."
|
||||
echo ""
|
||||
;;
|
||||
esac
|
||||
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)
|
||||
}
|
||||
140
cmds/portmaster-start/service_windows.go
Normal file
140
cmds/portmaster-start/service_windows.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
// Based on the official Go examples from
|
||||
// https://github.com/golang/sys/blob/master/windows/svc/example
|
||||
// by The Go Authors.
|
||||
// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/debug"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
var (
|
||||
runCoreService = &cobra.Command{
|
||||
Use: "core-service",
|
||||
Short: "Run the Portmaster Core as a Windows Service",
|
||||
RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error {
|
||||
return runService(cmd, &Options{
|
||||
Identifier: "core/portmaster-core",
|
||||
AllowDownload: true,
|
||||
AllowHidingWindow: false,
|
||||
NoOutput: true,
|
||||
}, args)
|
||||
}),
|
||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||
UnknownFlags: true,
|
||||
},
|
||||
}
|
||||
|
||||
// wait groups
|
||||
runWg sync.WaitGroup
|
||||
finishWg sync.WaitGroup
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCoreService)
|
||||
}
|
||||
|
||||
const serviceName = "PortmasterCore"
|
||||
|
||||
type windowsService struct{}
|
||||
|
||||
func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
service:
|
||||
for {
|
||||
select {
|
||||
case <-startupComplete:
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
case <-shuttingDown:
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
break service
|
||||
case c := <-changeRequests:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
initiateShutdown(nil)
|
||||
default:
|
||||
log.Printf("unexpected control request: #%d\n", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// define return values
|
||||
if getShutdownError() != nil {
|
||||
ssec = true // this error is specific to this service (ie. custom)
|
||||
errno = 1 // generic error, check logs / windows events
|
||||
}
|
||||
|
||||
// wait until everything else is finished
|
||||
finishWg.Wait()
|
||||
// send stopped status
|
||||
changes <- svc.Status{State: svc.Stopped}
|
||||
// wait a little for the status to reach Windows
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
return ssec, errno
|
||||
}
|
||||
|
||||
func runService(cmd *cobra.Command, opts *Options, cmdArgs []string) error {
|
||||
// check if we are running interactively
|
||||
isDebug, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine if running interactively: %s", err)
|
||||
}
|
||||
// select service run type
|
||||
svcRun := svc.Run
|
||||
if isDebug {
|
||||
log.Printf("WARNING: running interactively, switching to debug execution (no real service).\n")
|
||||
svcRun = debug.Run
|
||||
}
|
||||
|
||||
// open eventlog
|
||||
elog, err := eventlog.Open(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open eventlog: %s", err)
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
runWg.Add(2)
|
||||
finishWg.Add(1)
|
||||
|
||||
// run service client
|
||||
go func() {
|
||||
sErr := svcRun(serviceName, &windowsService{})
|
||||
initiateShutdown(sErr)
|
||||
runWg.Done()
|
||||
}()
|
||||
|
||||
// run service
|
||||
go func() {
|
||||
// run slightly delayed
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
err := run(cmd, opts, getExecArgs(opts, cmdArgs))
|
||||
initiateShutdown(err)
|
||||
finishWg.Done()
|
||||
runWg.Done()
|
||||
}()
|
||||
|
||||
runWg.Wait()
|
||||
|
||||
err = getShutdownError()
|
||||
if err != nil {
|
||||
log.Printf("%s service experienced an error: %s\n", serviceName, err)
|
||||
_ = elog.Error(1, fmt.Sprintf("%s experienced an error: %s", serviceName, err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
43
cmds/portmaster-start/shutdown.go
Normal file
43
cmds/portmaster-start/shutdown.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
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)
|
||||
//nolint:deadcode,unused // false positive on linux, currently used by windows only
|
||||
shutdownError error // protected by shutdownLock
|
||||
shutdownLock sync.Mutex
|
||||
)
|
||||
|
||||
func initiateShutdown(err error) {
|
||||
shutdownLock.Lock()
|
||||
defer shutdownLock.Unlock()
|
||||
|
||||
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()
|
||||
defer shutdownLock.Unlock()
|
||||
|
||||
return shutdownError
|
||||
}
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
54
cmds/portmaster-start/update.go
Normal file
54
cmds/portmaster-start/update.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updatesCmd)
|
||||
}
|
||||
|
||||
var updatesCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Run a manual update process",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return downloadUpdates()
|
||||
},
|
||||
}
|
||||
|
||||
func downloadUpdates() error {
|
||||
// mark required updates
|
||||
if onWindows {
|
||||
registry.MandatoryUpdates = []string{
|
||||
platform("core/portmaster-core.exe"),
|
||||
platform("start/portmaster-start.exe"),
|
||||
platform("app/portmaster-app.exe"),
|
||||
platform("notifier/portmaster-notifier.exe"),
|
||||
platform("notifier/portmaster-snoretoast.exe"),
|
||||
}
|
||||
} else {
|
||||
registry.MandatoryUpdates = []string{
|
||||
platform("core/portmaster-core"),
|
||||
platform("start/portmaster-start"),
|
||||
platform("app/portmaster-app"),
|
||||
platform("notifier/portmaster-notifier"),
|
||||
}
|
||||
}
|
||||
|
||||
// ok, now we want logging.
|
||||
err := log.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to start logging: %s\n", err)
|
||||
}
|
||||
|
||||
return registry.DownloadUpdates(context.TODO())
|
||||
}
|
||||
|
||||
func platform(identifier string) string {
|
||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user