mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
[WIP] Fix unit tests
This commit is contained in:
parent
a8517cd65f
commit
a874ec9412
48 changed files with 264 additions and 4088 deletions
|
@ -18,13 +18,12 @@ import (
|
||||||
"github.com/safing/portmaster/base/metrics"
|
"github.com/safing/portmaster/base/metrics"
|
||||||
"github.com/safing/portmaster/service/mgr"
|
"github.com/safing/portmaster/service/mgr"
|
||||||
"github.com/safing/portmaster/service/updates"
|
"github.com/safing/portmaster/service/updates"
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
"github.com/safing/portmaster/spn"
|
"github.com/safing/portmaster/spn"
|
||||||
"github.com/safing/portmaster/spn/conf"
|
"github.com/safing/portmaster/spn/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade")
|
// flag.BoolVar(&updates.RebootOnRestart, "reboot-on-restart", false, "reboot server on auto-upgrade")
|
||||||
}
|
}
|
||||||
|
|
||||||
var sigUSR1 = syscall.Signal(0xa)
|
var sigUSR1 = syscall.Signal(0xa)
|
||||||
|
@ -40,7 +39,7 @@ func main() {
|
||||||
|
|
||||||
// Configure user agent and updates.
|
// Configure user agent and updates.
|
||||||
updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
||||||
helper.IntelOnly()
|
// helper.IntelOnly()
|
||||||
|
|
||||||
// Set SPN public hub mode.
|
// Set SPN public hub mode.
|
||||||
conf.EnablePublicHub(true)
|
conf.EnablePublicHub(true)
|
||||||
|
|
34
cmds/notifier/.gitignore
vendored
34
cmds/notifier/.gitignore
vendored
|
@ -1,34 +0,0 @@
|
||||||
# Compiled binaries
|
|
||||||
notifier
|
|
||||||
notifier.exe
|
|
||||||
|
|
||||||
# Go vendor
|
|
||||||
vendor
|
|
||||||
|
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
*.prof
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
|
@ -1,5 +0,0 @@
|
||||||
### Development Dependencies
|
|
||||||
|
|
||||||
sudo apt install libgtk-3-dev libayatana-appindicator3-dev libwebkitgtk-3.0-dev libgl1-mesa-dev libglu1-mesa-dev libnotify-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
|
||||||
|
|
||||||
sudo pacman -S libappindicator-gtk3
|
|
|
@ -1,63 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
apiBaseURL = "http://127.0.0.1:817/api/v1/"
|
|
||||||
apiShutdownEndpoint = "core/shutdown"
|
|
||||||
)
|
|
||||||
|
|
||||||
var httpAPIClient *http.Client
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Make cookie jar.
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("http-api: failed to create cookie jar: %s", err)
|
|
||||||
jar = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create client.
|
|
||||||
httpAPIClient = &http.Client{
|
|
||||||
Jar: jar,
|
|
||||||
Timeout: 3 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpAPIAction(endpoint string) (response string, err error) {
|
|
||||||
// Make action request.
|
|
||||||
resp, err := httpAPIClient.Post(apiBaseURL+endpoint, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("request failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the response body.
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
respData, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read data: %w", err)
|
|
||||||
}
|
|
||||||
response = strings.TrimSpace(string(respData))
|
|
||||||
|
|
||||||
// Check if the request was successful on the server.
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
||||||
return response, fmt.Errorf("server failed with %s: %s", resp.Status, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerShutdown triggers a shutdown via the APi.
|
|
||||||
func TriggerShutdown() error {
|
|
||||||
_, err := httpAPIAction(apiShutdownEndpoint)
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
icons "github.com/safing/portmaster/assets"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
appIconEnsureOnce sync.Once
|
|
||||||
appIconPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
func ensureAppIcon() (location string, err error) {
|
|
||||||
appIconEnsureOnce.Do(func() {
|
|
||||||
if appIconPath == "" {
|
|
||||||
appIconPath = filepath.Join(dataDir, "exec", "portmaster.png")
|
|
||||||
}
|
|
||||||
err = os.WriteFile(appIconPath, icons.PNG, 0o0644) // nolint:gosec
|
|
||||||
})
|
|
||||||
|
|
||||||
return appIconPath, err
|
|
||||||
}
|
|
|
@ -1,287 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"runtime/pprof"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api/client"
|
|
||||||
"github.com/safing/portmaster/base/dataroot"
|
|
||||||
"github.com/safing/portmaster/base/info"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/portmaster/base/updater"
|
|
||||||
"github.com/safing/portmaster/base/utils"
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dataDir string
|
|
||||||
printStackOnExit bool
|
|
||||||
showVersion bool
|
|
||||||
|
|
||||||
apiClient = client.NewClient("127.0.0.1:817")
|
|
||||||
connected = abool.New()
|
|
||||||
shuttingDown = abool.New()
|
|
||||||
restarting = abool.New()
|
|
||||||
|
|
||||||
mainCtx, cancelMainCtx = context.WithCancel(context.Background())
|
|
||||||
mainWg = &sync.WaitGroup{}
|
|
||||||
|
|
||||||
dataRoot *utils.DirStructure
|
|
||||||
// Create registry.
|
|
||||||
registry = &updater.ResourceRegistry{
|
|
||||||
Name: "updates",
|
|
||||||
UpdateURLs: []string{
|
|
||||||
"https://updates.safing.io",
|
|
||||||
},
|
|
||||||
DevMode: false,
|
|
||||||
Online: false, // disable download of resources (this is job for the core).
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const query = "query "
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.StringVar(&dataDir, "data", "", "set data directory")
|
|
||||||
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
|
|
||||||
flag.BoolVar(&showVersion, "version", false, "show version and exit")
|
|
||||||
|
|
||||||
runtime.GOMAXPROCS(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// parse flags
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// set meta info
|
|
||||||
info.Set("Portmaster Notifier", "0.3.6", "GPLv3")
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print help
|
|
||||||
// if modules.HelpFlag {
|
|
||||||
// flag.Usage()
|
|
||||||
// os.Exit(0)
|
|
||||||
// }
|
|
||||||
|
|
||||||
if showVersion {
|
|
||||||
fmt.Println(info.FullVersion())
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// auto detect
|
|
||||||
if dataDir == "" {
|
|
||||||
dataDir = detectDataDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check data dir
|
|
||||||
if dataDir == "" {
|
|
||||||
fmt.Fprintln(os.Stderr, "please set the data directory using --data=/path/to/data/dir")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// switch to safe exec dir
|
|
||||||
err = os.Chdir(filepath.Join(dataDir, "exec"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "warning: failed to switch to safe exec dir: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start log writer
|
|
||||||
err = log.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to start logging: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// load registry
|
|
||||||
err = configureRegistry(true)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to load registry: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect to API
|
|
||||||
go apiClient.StayConnected()
|
|
||||||
go apiStatusMonitor()
|
|
||||||
|
|
||||||
// start subsystems
|
|
||||||
go tray()
|
|
||||||
go subsystemsClient()
|
|
||||||
go spnStatusClient()
|
|
||||||
go notifClient()
|
|
||||||
go startShutdownEventListener()
|
|
||||||
|
|
||||||
// Shutdown
|
|
||||||
// catch interrupt for clean shutdown
|
|
||||||
signalCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(
|
|
||||||
signalCh,
|
|
||||||
os.Interrupt,
|
|
||||||
syscall.SIGHUP,
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
syscall.SIGQUIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
// wait for shutdown
|
|
||||||
select {
|
|
||||||
case <-signalCh:
|
|
||||||
fmt.Println(" <INTERRUPT>")
|
|
||||||
log.Warning("program was interrupted, shutting down")
|
|
||||||
case <-mainCtx.Done():
|
|
||||||
log.Warning("program is shutting down")
|
|
||||||
}
|
|
||||||
|
|
||||||
if printStackOnExit {
|
|
||||||
fmt.Println("=== PRINTING STACK ===")
|
|
||||||
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
|
|
||||||
fmt.Println("=== END STACK ===")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
|
|
||||||
_ = pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
|
|
||||||
os.Exit(1)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// clear all notifications
|
|
||||||
clearNotifications()
|
|
||||||
|
|
||||||
// shutdown
|
|
||||||
cancelMainCtx()
|
|
||||||
mainWg.Wait()
|
|
||||||
|
|
||||||
apiClient.Shutdown()
|
|
||||||
exitTray()
|
|
||||||
log.Shutdown()
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiStatusMonitor() {
|
|
||||||
for {
|
|
||||||
// Wait for connection.
|
|
||||||
<-apiClient.Online()
|
|
||||||
connected.Set()
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
// Wait for lost connection.
|
|
||||||
<-apiClient.Offline()
|
|
||||||
connected.UnSet()
|
|
||||||
triggerTrayUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectDataDir() string {
|
|
||||||
// get path of executable
|
|
||||||
binPath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// get directory
|
|
||||||
binDir := filepath.Dir(binPath)
|
|
||||||
// check if we in the updates directory
|
|
||||||
identifierDir := filepath.Join("updates", runtime.GOOS+"_"+runtime.GOARCH, "notifier")
|
|
||||||
// check if there is a match and return data dir
|
|
||||||
if strings.HasSuffix(binDir, identifierDir) {
|
|
||||||
return filepath.Clean(strings.TrimSuffix(binDir, identifierDir))
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func configureRegistry(mustLoadIndex bool) error {
|
|
||||||
// If dataDir is not set, check the environment variable.
|
|
||||||
if dataDir == "" {
|
|
||||||
dataDir = os.Getenv("PORTMASTER_DATA")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's still empty, try to auto-detect it.
|
|
||||||
if dataDir == "" {
|
|
||||||
dataDir = detectInstallationDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, if it's still empty, the user must provide it.
|
|
||||||
if dataDir == "" {
|
|
||||||
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove left over quotes.
|
|
||||||
dataDir = strings.Trim(dataDir, `\"`)
|
|
||||||
// Initialize data root.
|
|
||||||
err := dataroot.Initialize(dataDir, 0o0755)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize data root: %w", err)
|
|
||||||
}
|
|
||||||
dataRoot = dataroot.Root()
|
|
||||||
|
|
||||||
// Initialize registry.
|
|
||||||
err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateRegistryIndex(mustLoadIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectInstallationDir() string {
|
|
||||||
exePath, err := filepath.Abs(os.Args[0])
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := filepath.Dir(exePath) // parent should be "...\updates\windows_amd64\notifier"
|
|
||||||
stableJSONFile := filepath.Join(parent, "..", "..", "stable.json") // "...\updates\stable.json"
|
|
||||||
stat, err := os.Stat(stableJSONFile)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if stat.IsDir() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateRegistryIndex(mustLoadIndex bool) error {
|
|
||||||
// Set indexes based on the release channel.
|
|
||||||
warning := helper.SetIndexes(registry, "", false, false, false)
|
|
||||||
if warning != nil {
|
|
||||||
log.Warningf("%q", warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load indexes from disk or network, if needed and desired.
|
|
||||||
err := registry.LoadIndexes(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("error loading indexes %q", warning)
|
|
||||||
if mustLoadIndex {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load versions from disk to know which others we have and which are available.
|
|
||||||
err = registry.ScanStorage("")
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("error during storage scan: %q\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.SelectVersions()
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
pbnotify "github.com/safing/portmaster/base/notifications"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Notification represents a notification that is to be delivered to the user.
|
|
||||||
type Notification struct {
|
|
||||||
pbnotify.Notification
|
|
||||||
|
|
||||||
// systemID holds the ID returned by the dbus interface on Linux or by WinToast library on Windows.
|
|
||||||
systemID NotificationID
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSupportedAction returns whether the action is supported on this system.
|
|
||||||
func IsSupportedAction(a pbnotify.Action) bool {
|
|
||||||
switch a.Type {
|
|
||||||
case pbnotify.ActionTypeNone:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectAction sends an action back to the portmaster.
|
|
||||||
func (n *Notification) SelectAction(action string) {
|
|
||||||
upd := &pbnotify.Notification{
|
|
||||||
EventID: n.EventID,
|
|
||||||
SelectedActionID: action,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = apiClient.Update(fmt.Sprintf("%s%s", dbNotifBasePath, upd.EventID), upd, nil)
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api/client"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
pbnotify "github.com/safing/portmaster/base/notifications"
|
|
||||||
"github.com/safing/structures/dsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbNotifBasePath = "notifications:all/"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
notifications = make(map[string]*Notification)
|
|
||||||
notificationsLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func notifClient() {
|
|
||||||
notifOp := apiClient.Qsub(fmt.Sprintf("query %s where ShowOnSystem is true", dbNotifBasePath), handleNotification)
|
|
||||||
notifOp.EnableResuscitation()
|
|
||||||
|
|
||||||
// start the action listener and block
|
|
||||||
// until it's closed.
|
|
||||||
actionListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNotification(m *client.Message) {
|
|
||||||
notificationsLock.Lock()
|
|
||||||
defer notificationsLock.Unlock()
|
|
||||||
|
|
||||||
log.Tracef("received %s msg: %s", m.Type, m.Key)
|
|
||||||
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgError:
|
|
||||||
case client.MsgDone:
|
|
||||||
case client.MsgSuccess:
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
|
|
||||||
n := &Notification{}
|
|
||||||
_, err := dsd.Load(m.RawValue, &n.Notification)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to parse new notification: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy existing system values
|
|
||||||
existing, ok := notifications[n.EventID]
|
|
||||||
if ok {
|
|
||||||
existing.Lock()
|
|
||||||
n.systemID = existing.systemID
|
|
||||||
existing.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// save
|
|
||||||
notifications[n.EventID] = n
|
|
||||||
|
|
||||||
// Handle notification.
|
|
||||||
switch {
|
|
||||||
case existing != nil:
|
|
||||||
// Cancel existing notification if not active, else ignore.
|
|
||||||
if n.State != pbnotify.Active {
|
|
||||||
existing.Cancel()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case n.State == pbnotify.Active:
|
|
||||||
// Show new notifications that are active.
|
|
||||||
n.Show()
|
|
||||||
default:
|
|
||||||
// Ignore new notifications that are not active.
|
|
||||||
}
|
|
||||||
|
|
||||||
case client.MsgDelete:
|
|
||||||
|
|
||||||
n, ok := notifications[strings.TrimPrefix(m.Key, dbNotifBasePath)]
|
|
||||||
if ok {
|
|
||||||
n.Cancel()
|
|
||||||
delete(notifications, n.EventID)
|
|
||||||
}
|
|
||||||
|
|
||||||
case client.MsgWarning:
|
|
||||||
case client.MsgOffline:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearNotifications() {
|
|
||||||
notificationsLock.Lock()
|
|
||||||
defer notificationsLock.Unlock()
|
|
||||||
|
|
||||||
for _, n := range notifications {
|
|
||||||
n.Cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for goroutines that cancel notifications.
|
|
||||||
// TODO: Revamp to use a waitgroup.
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
notify "github.com/dhaavi/go-notify"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationID uint32
|
|
||||||
|
|
||||||
var (
|
|
||||||
capabilities notify.Capabilities
|
|
||||||
notifsByID sync.Map
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var err error
|
|
||||||
capabilities, err = notify.GetCapabilities()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to get notification system capabilities: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleActions(ctx context.Context, actions chan notify.Signal) {
|
|
||||||
mainWg.Add(1)
|
|
||||||
defer mainWg.Done()
|
|
||||||
|
|
||||||
listenForNotifications:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case sig := <-actions:
|
|
||||||
if sig.Name != "org.freedesktop.Notifications.ActionInvoked" {
|
|
||||||
// we don't care for anything else (dismissed, closed)
|
|
||||||
continue listenForNotifications
|
|
||||||
}
|
|
||||||
|
|
||||||
// get notification by system ID
|
|
||||||
n, ok := notifsByID.LoadAndDelete(NotificationID(sig.ID))
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
continue listenForNotifications
|
|
||||||
}
|
|
||||||
|
|
||||||
notification, ok := n.(*Notification)
|
|
||||||
if !ok {
|
|
||||||
log.Errorf("received invalid notification type %T", n)
|
|
||||||
|
|
||||||
continue listenForNotifications
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("notify: received signal: %+v", sig)
|
|
||||||
if sig.ActionKey != "" {
|
|
||||||
// send action
|
|
||||||
if ok {
|
|
||||||
notification.Lock()
|
|
||||||
notification.SelectAction(sig.ActionKey)
|
|
||||||
notification.Unlock()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Tracef("notify: notification clicked: %+v", sig)
|
|
||||||
// Global action invoked, start the app
|
|
||||||
launchApp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionListener() {
|
|
||||||
actions := make(chan notify.Signal, 100)
|
|
||||||
|
|
||||||
go handleActions(mainCtx, actions)
|
|
||||||
|
|
||||||
err := notify.SignalNotify(mainCtx, actions)
|
|
||||||
if err != nil && errors.Is(err, context.Canceled) {
|
|
||||||
log.Errorf("notify: signal listener failed: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show shows the notification.
|
|
||||||
func (n *Notification) Show() {
|
|
||||||
sysN := notify.NewNotification("Portmaster", n.Message)
|
|
||||||
// see https://developer.gnome.org/notification-spec/
|
|
||||||
|
|
||||||
// The optional name of the application sending the notification.
|
|
||||||
// Can be blank.
|
|
||||||
sysN.AppName = "Portmaster"
|
|
||||||
|
|
||||||
// The optional notification ID that this notification replaces.
|
|
||||||
sysN.ReplacesID = uint32(n.systemID)
|
|
||||||
|
|
||||||
// The optional program icon of the calling application.
|
|
||||||
// sysN.AppIcon string
|
|
||||||
|
|
||||||
// The summary text briefly describing the notification.
|
|
||||||
// Summary string (arg 1)
|
|
||||||
|
|
||||||
// The optional detailed body text.
|
|
||||||
// Body string (arg 2)
|
|
||||||
|
|
||||||
// The actions send a request message back to the notification client
|
|
||||||
// when invoked.
|
|
||||||
// sysN.Actions []string
|
|
||||||
if capabilities.Actions {
|
|
||||||
sysN.Actions = make([]string, 0, len(n.AvailableActions)*2)
|
|
||||||
for _, action := range n.AvailableActions {
|
|
||||||
if IsSupportedAction(*action) {
|
|
||||||
sysN.Actions = append(sysN.Actions, action.ID)
|
|
||||||
sysN.Actions = append(sysN.Actions, action.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Portmaster icon.
|
|
||||||
iconLocation, err := ensureAppIcon()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to write icon: %s", err)
|
|
||||||
}
|
|
||||||
sysN.AppIcon = iconLocation
|
|
||||||
|
|
||||||
// TODO: Use hints to display icon of affected app.
|
|
||||||
// Hints are a way to provide extra data to a notification server.
|
|
||||||
// sysN.Hints = make(map[string]interface{})
|
|
||||||
|
|
||||||
// The timeout time in milliseconds since the display of the
|
|
||||||
// notification at which the notification should automatically close.
|
|
||||||
// sysN.Timeout int32
|
|
||||||
|
|
||||||
newID, err := sysN.Show()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to show notification %s", n.EventID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notifsByID.Store(NotificationID(newID), n)
|
|
||||||
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
n.systemID = NotificationID(newID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel cancels the notification.
|
|
||||||
func (n *Notification) Cancel() {
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
|
|
||||||
// TODO: could a ID of 0 be valid?
|
|
||||||
if n.systemID != 0 {
|
|
||||||
err := notify.CloseNotification(uint32(n.systemID))
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to close notification %s/%d", n.EventID, n.systemID)
|
|
||||||
}
|
|
||||||
notifsByID.Delete(n.systemID)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,184 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/portmaster/cmds/notifier/wintoast"
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationID int64
|
|
||||||
|
|
||||||
const (
|
|
||||||
appName = "Portmaster"
|
|
||||||
appUserModelID = "io.safing.portmaster.2"
|
|
||||||
originalShortcutPath = "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Portmaster\\Portmaster.lnk"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SoundDefault = 0
|
|
||||||
SoundSilent = 1
|
|
||||||
SoundLoop = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SoundPathDefault = 0
|
|
||||||
// see notification_glue.h if you need more types
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
initOnce sync.Once
|
|
||||||
lib *wintoast.WinToast
|
|
||||||
notificationsByIDs sync.Map
|
|
||||||
)
|
|
||||||
|
|
||||||
func getLib() *wintoast.WinToast {
|
|
||||||
initOnce.Do(func() {
|
|
||||||
dllPath, err := getDllPath()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("notify: failed to get dll path: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Load dll and all the functions
|
|
||||||
newLib, err := wintoast.New(dllPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("notify: failed to load library: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize. This will create or update application shortcut. C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
|
|
||||||
// and it will be of the originalShortcutPath with no CLSID and different AUMI
|
|
||||||
err = newLib.Initialize(appName, appUserModelID, originalShortcutPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("notify: failed to load library: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// library was initialized successfully
|
|
||||||
lib = newLib
|
|
||||||
|
|
||||||
// Set callbacks
|
|
||||||
|
|
||||||
err = lib.SetCallbacks(notificationActivatedCallback, notificationDismissedCallback, notificationDismissedCallback)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to set callbacks: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return lib
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show shows the notification.
|
|
||||||
func (n *Notification) Show() {
|
|
||||||
// Lock notification
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
|
|
||||||
// Create new notification object
|
|
||||||
builder, err := getLib().NewNotification(n.Title, n.Message)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("notify: failed to create notification: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Make sure memory is freed when done
|
|
||||||
defer builder.Delete()
|
|
||||||
|
|
||||||
// if needed set notification icon
|
|
||||||
// _ = builder.SetImage(iconLocation)
|
|
||||||
|
|
||||||
// Leaving the default value for the sound
|
|
||||||
// _ = builder.SetSound(SoundDefault, SoundPathDefault)
|
|
||||||
|
|
||||||
// Set all the required actions.
|
|
||||||
for _, action := range n.AvailableActions {
|
|
||||||
err = builder.AddButton(action.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("notify: failed to add button: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show notification.
|
|
||||||
id, err := builder.Show()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("notify: failed to show notification: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
n.systemID = NotificationID(id)
|
|
||||||
|
|
||||||
// Link system id to the notification object
|
|
||||||
notificationsByIDs.Store(NotificationID(id), n)
|
|
||||||
|
|
||||||
log.Debugf("notify: showing notification %q: %d", n.Title, n.systemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel cancels the notification.
|
|
||||||
func (n *Notification) Cancel() {
|
|
||||||
// Lock notification
|
|
||||||
n.Lock()
|
|
||||||
defer n.Unlock()
|
|
||||||
|
|
||||||
// No need to check for errors. If it fails it is probably already dismissed
|
|
||||||
_ = getLib().HideNotification(int64(n.systemID))
|
|
||||||
|
|
||||||
notificationsByIDs.Delete(n.systemID)
|
|
||||||
log.Debugf("notify: notification canceled %q: %d", n.Title, n.systemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notificationActivatedCallback(id int64, actionIndex int32) {
|
|
||||||
if actionIndex == -1 {
|
|
||||||
// The user clicked on the notification (not a button), open the portmaster and delete
|
|
||||||
launchApp()
|
|
||||||
notificationsByIDs.Delete(NotificationID(id))
|
|
||||||
log.Debugf("notify: notification clicked %d", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// The user click one of the buttons
|
|
||||||
|
|
||||||
// Get notified object
|
|
||||||
n, ok := notificationsByIDs.LoadAndDelete(NotificationID(id))
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notification := n.(*Notification)
|
|
||||||
|
|
||||||
notification.Lock()
|
|
||||||
defer notification.Unlock()
|
|
||||||
|
|
||||||
// Set selected action
|
|
||||||
actionID := notification.AvailableActions[actionIndex].ID
|
|
||||||
notification.SelectAction(actionID)
|
|
||||||
|
|
||||||
log.Debugf("notify: notification button cliecked %d button id: %d", id, actionIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notificationDismissedCallback(id int64, reason int32) {
|
|
||||||
// Failure or user dismissed the notification
|
|
||||||
if reason == 0 {
|
|
||||||
notificationsByIDs.Delete(NotificationID(id))
|
|
||||||
log.Debugf("notify: notification dissmissed %d", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDllPath() (string, error) {
|
|
||||||
if dataDir == "" {
|
|
||||||
return "", fmt.Errorf("dataDir is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aks the registry for the dll path
|
|
||||||
identifier := helper.PlatformIdentifier("notifier/portmaster-wintoast.dll")
|
|
||||||
file, err := registry.GetFile(identifier)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return file.Path(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionListener() {
|
|
||||||
// initialize the library
|
|
||||||
_ = getLib()
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/safing/portmaster/base/api/client"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func startShutdownEventListener() {
|
|
||||||
shutdownNotifOp := apiClient.Sub("query runtime:modules/core/event/shutdown", handleShutdownEvent)
|
|
||||||
shutdownNotifOp.EnableResuscitation()
|
|
||||||
|
|
||||||
restartNotifOp := apiClient.Sub("query runtime:modules/core/event/restart", handleRestartEvent)
|
|
||||||
restartNotifOp.EnableResuscitation()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleShutdownEvent(m *client.Message) {
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
shuttingDown.Set()
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
log.Warningf("shutdown: received shutdown event, shutting down now")
|
|
||||||
|
|
||||||
// wait for the API client connection to die
|
|
||||||
<-apiClient.Offline()
|
|
||||||
shuttingDown.UnSet()
|
|
||||||
|
|
||||||
cancelMainCtx()
|
|
||||||
|
|
||||||
case client.MsgWarning, client.MsgError:
|
|
||||||
log.Errorf("shutdown: event subscription error: %s", string(m.RawValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRestartEvent(m *client.Message) {
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
restarting.Set()
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
log.Warningf("restart: received restart event")
|
|
||||||
|
|
||||||
// wait for the API client connection to die
|
|
||||||
<-apiClient.Offline()
|
|
||||||
restarting.UnSet()
|
|
||||||
triggerTrayUpdate()
|
|
||||||
case client.MsgWarning, client.MsgError:
|
|
||||||
log.Errorf("shutdown: event subscription error: %s", string(m.RawValue))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
||||||
index 498226a..446ba5e 100644
|
|
||||||
--- a/CMakeLists.txt
|
|
||||||
+++ b/CMakeLists.txt
|
|
||||||
@@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.4)
|
|
||||||
|
|
||||||
project(snoretoast VERSION 0.6.0)
|
|
||||||
# Always change the guid when the version is changed SNORETOAST_CALLBACK_GUID
|
|
||||||
-set(SNORETOAST_CALLBACK_GUID eb1fdd5b-8f70-4b5a-b230-998a2dc19303)
|
|
||||||
+#We keep it fixed!
|
|
||||||
+set(SNORETOAST_CALLBACK_GUID 7F00FB48-65D5-4BA8-A35B-F194DA7E1A51)
|
|
||||||
+
|
|
||||||
|
|
||||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/)
|
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api/client"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/structures/dsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
spnModuleKey = "config:spn/enable"
|
|
||||||
spnStatusKey = "runtime:spn/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
spnEnabled = abool.New()
|
|
||||||
|
|
||||||
spnStatusCache *SPNStatus
|
|
||||||
spnStatusCacheLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// SPNStatus holds SPN status information.
|
|
||||||
type SPNStatus struct {
|
|
||||||
Status string
|
|
||||||
HomeHubID string
|
|
||||||
HomeHubName string
|
|
||||||
ConnectedIP string
|
|
||||||
ConnectedTransport string
|
|
||||||
ConnectedSince *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSPNStatus returns the SPN status.
|
|
||||||
func GetSPNStatus() *SPNStatus {
|
|
||||||
spnStatusCacheLock.Lock()
|
|
||||||
defer spnStatusCacheLock.Unlock()
|
|
||||||
|
|
||||||
return spnStatusCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateSPNStatus(s *SPNStatus) {
|
|
||||||
spnStatusCacheLock.Lock()
|
|
||||||
defer spnStatusCacheLock.Unlock()
|
|
||||||
|
|
||||||
spnStatusCache = s
|
|
||||||
}
|
|
||||||
|
|
||||||
func spnStatusClient() {
|
|
||||||
moduleQueryOp := apiClient.Qsub(query+spnModuleKey, handleSPNModuleUpdate)
|
|
||||||
moduleQueryOp.EnableResuscitation()
|
|
||||||
|
|
||||||
statusQueryOp := apiClient.Qsub(query+spnStatusKey, handleSPNStatusUpdate)
|
|
||||||
statusQueryOp.EnableResuscitation()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSPNModuleUpdate(m *client.Message) {
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
var cfg struct {
|
|
||||||
Value bool `json:"Value"`
|
|
||||||
}
|
|
||||||
_, err := dsd.Load(m.RawValue, &cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("config: failed to parse config: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("config: received update to SPN module: enabled=%v", cfg.Value)
|
|
||||||
|
|
||||||
spnEnabled.SetTo(cfg.Value)
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSPNStatusUpdate(m *client.Message) {
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
newStatus := &SPNStatus{}
|
|
||||||
_, err := dsd.Load(m.RawValue, newStatus)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("config: failed to parse config: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("config: received update to SPN status: %+v", newStatus)
|
|
||||||
|
|
||||||
updateSPNStatus(newStatus)
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToggleSPN() {
|
|
||||||
var cfg struct {
|
|
||||||
Value bool `json:"Value"`
|
|
||||||
}
|
|
||||||
cfg.Value = !spnEnabled.IsSet()
|
|
||||||
|
|
||||||
apiClient.Update(spnModuleKey, &cfg, nil)
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api/client"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/structures/dsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
subsystemsKeySpace = "runtime:subsystems/"
|
|
||||||
|
|
||||||
// Module Failure Status Values
|
|
||||||
// FailureNone = 0 // unused
|
|
||||||
// FailureHint = 1 // unused.
|
|
||||||
FailureWarning = 2
|
|
||||||
FailureError = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
subsystems = make(map[string]*Subsystem)
|
|
||||||
subsystemsLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// Subsystem describes a subset of modules that represent a part of a
|
|
||||||
// service or program to the user. Subsystems can be (de-)activated causing
|
|
||||||
// all related modules to be brought down or up.
|
|
||||||
type Subsystem struct { //nolint:maligned // not worth the effort
|
|
||||||
// ID is a unique identifier for the subsystem.
|
|
||||||
ID string
|
|
||||||
|
|
||||||
// Name holds a human readable name of the subsystem.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Description may holds an optional description of
|
|
||||||
// the subsystem's purpose.
|
|
||||||
Description string
|
|
||||||
|
|
||||||
// Modules contains all modules that are related to the subsystem.
|
|
||||||
// Note that this slice also contains a reference to the subsystem
|
|
||||||
// module itself.
|
|
||||||
Modules []*ModuleStatus
|
|
||||||
|
|
||||||
// FailureStatus is the worst failure status that is currently
|
|
||||||
// set in one of the subsystem's dependencies.
|
|
||||||
FailureStatus uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModuleStatus describes the status of a module.
|
|
||||||
type ModuleStatus struct {
|
|
||||||
Name string
|
|
||||||
Enabled bool
|
|
||||||
Status uint8
|
|
||||||
FailureStatus uint8
|
|
||||||
FailureID string
|
|
||||||
FailureMsg string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFailure returns the worst of all subsystem failures.
|
|
||||||
func GetFailure() (failureStatus uint8, failureMsg string) {
|
|
||||||
subsystemsLock.Lock()
|
|
||||||
defer subsystemsLock.Unlock()
|
|
||||||
|
|
||||||
for _, subsystem := range subsystems {
|
|
||||||
for _, module := range subsystem.Modules {
|
|
||||||
if failureStatus < module.FailureStatus {
|
|
||||||
failureStatus = module.FailureStatus
|
|
||||||
failureMsg = module.FailureMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateSubsystem(s *Subsystem) {
|
|
||||||
subsystemsLock.Lock()
|
|
||||||
defer subsystemsLock.Unlock()
|
|
||||||
|
|
||||||
subsystems[s.ID] = s
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearSubsystems() {
|
|
||||||
subsystemsLock.Lock()
|
|
||||||
defer subsystemsLock.Unlock()
|
|
||||||
|
|
||||||
for key := range subsystems {
|
|
||||||
delete(subsystems, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func subsystemsClient() {
|
|
||||||
subsystemsOp := apiClient.Qsub("query "+subsystemsKeySpace, handleSubsystem)
|
|
||||||
subsystemsOp.EnableResuscitation()
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSubsystem(m *client.Message) {
|
|
||||||
switch m.Type {
|
|
||||||
case client.MsgError:
|
|
||||||
case client.MsgDone:
|
|
||||||
case client.MsgSuccess:
|
|
||||||
case client.MsgOk, client.MsgUpdate, client.MsgNew:
|
|
||||||
|
|
||||||
newSubsystem := &Subsystem{}
|
|
||||||
_, err := dsd.Load(m.RawValue, newSubsystem)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("subsystems: failed to parse new subsystem: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateSubsystem(newSubsystem)
|
|
||||||
triggerTrayUpdate()
|
|
||||||
|
|
||||||
case client.MsgDelete:
|
|
||||||
case client.MsgWarning:
|
|
||||||
case client.MsgOffline:
|
|
||||||
|
|
||||||
clearSubsystems()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,217 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fyne.io/systray"
|
|
||||||
|
|
||||||
icons "github.com/safing/portmaster/assets"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
shortenStatusMsgTo = 40
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
trayLock sync.Mutex
|
|
||||||
|
|
||||||
scaleColoredIconsTo int
|
|
||||||
|
|
||||||
activeIconID int = -1
|
|
||||||
activeStatusMsg = ""
|
|
||||||
activeSPNStatus = ""
|
|
||||||
activeSPNSwitch = ""
|
|
||||||
|
|
||||||
menuItemStatusMsg *systray.MenuItem
|
|
||||||
menuItemSPNStatus *systray.MenuItem
|
|
||||||
menuItemSPNSwitch *systray.MenuItem
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flag.IntVar(&scaleColoredIconsTo, "scale-icons", 32, "scale colored icons to given size in pixels")
|
|
||||||
|
|
||||||
// lock until ready
|
|
||||||
trayLock.Lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func tray() {
|
|
||||||
if scaleColoredIconsTo > 0 {
|
|
||||||
icons.ScaleColoredIconsTo(scaleColoredIconsTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
systray.Run(onReady, onExit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exitTray() {
|
|
||||||
systray.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func onReady() {
|
|
||||||
// unlock when ready
|
|
||||||
defer trayLock.Unlock()
|
|
||||||
|
|
||||||
// icon
|
|
||||||
systray.SetIcon(icons.ColoredIcons[icons.RedID])
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// systray.SetTitle("Portmaster Notifier") // Don't set title, as it may be displayed in full in the menu/tray bar. (Ubuntu)
|
|
||||||
systray.SetTooltip("Portmaster Notifier")
|
|
||||||
}
|
|
||||||
|
|
||||||
// menu: open app
|
|
||||||
if dataDir != "" {
|
|
||||||
menuItemOpenApp := systray.AddMenuItem("Open App", "")
|
|
||||||
go clickListener(menuItemOpenApp, launchApp)
|
|
||||||
systray.AddSeparator()
|
|
||||||
}
|
|
||||||
|
|
||||||
// menu: status
|
|
||||||
|
|
||||||
menuItemStatusMsg = systray.AddMenuItem("Loading...", "")
|
|
||||||
menuItemStatusMsg.Disable()
|
|
||||||
systray.AddSeparator()
|
|
||||||
|
|
||||||
// menu: SPN
|
|
||||||
|
|
||||||
menuItemSPNStatus = systray.AddMenuItem("Loading...", "")
|
|
||||||
menuItemSPNStatus.Disable()
|
|
||||||
menuItemSPNSwitch = systray.AddMenuItem("Loading...", "")
|
|
||||||
go clickListener(menuItemSPNSwitch, func() {
|
|
||||||
ToggleSPN()
|
|
||||||
})
|
|
||||||
systray.AddSeparator()
|
|
||||||
|
|
||||||
// menu: quit
|
|
||||||
systray.AddSeparator()
|
|
||||||
closeTray := systray.AddMenuItem("Close Tray Notifier", "")
|
|
||||||
go clickListener(closeTray, func() {
|
|
||||||
cancelMainCtx()
|
|
||||||
})
|
|
||||||
shutdownPortmaster := systray.AddMenuItem("Shut Down Portmaster", "")
|
|
||||||
go clickListener(shutdownPortmaster, func() {
|
|
||||||
_ = TriggerShutdown()
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
cancelMainCtx()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func onExit() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func triggerTrayUpdate() {
|
|
||||||
// TODO: Deduplicate triggers.
|
|
||||||
go updateTray()
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateTray update the state of the tray depending on the currently available information.
|
|
||||||
func updateTray() {
|
|
||||||
// Get current information.
|
|
||||||
spnStatus := GetSPNStatus()
|
|
||||||
failureID, failureMsg := GetFailure()
|
|
||||||
|
|
||||||
trayLock.Lock()
|
|
||||||
defer trayLock.Unlock()
|
|
||||||
|
|
||||||
// Select icon and status message to show.
|
|
||||||
newIconID := icons.GreenID
|
|
||||||
newStatusMsg := "Secure"
|
|
||||||
switch {
|
|
||||||
case shuttingDown.IsSet():
|
|
||||||
newIconID = icons.RedID
|
|
||||||
newStatusMsg = "Shutting Down Portmaster"
|
|
||||||
|
|
||||||
case restarting.IsSet():
|
|
||||||
newIconID = icons.YellowID
|
|
||||||
newStatusMsg = "Restarting Portmaster"
|
|
||||||
|
|
||||||
case !connected.IsSet():
|
|
||||||
newIconID = icons.RedID
|
|
||||||
newStatusMsg = "Waiting for Portmaster Core Service"
|
|
||||||
|
|
||||||
case failureID == FailureError:
|
|
||||||
newIconID = icons.RedID
|
|
||||||
newStatusMsg = failureMsg
|
|
||||||
|
|
||||||
case failureID == FailureWarning:
|
|
||||||
newIconID = icons.YellowID
|
|
||||||
newStatusMsg = failureMsg
|
|
||||||
|
|
||||||
case spnEnabled.IsSet():
|
|
||||||
newIconID = icons.BlueID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set icon if changed.
|
|
||||||
if newIconID != activeIconID {
|
|
||||||
activeIconID = newIconID
|
|
||||||
systray.SetIcon(icons.ColoredIcons[activeIconID])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set message if changed.
|
|
||||||
if newStatusMsg != activeStatusMsg {
|
|
||||||
activeStatusMsg = newStatusMsg
|
|
||||||
|
|
||||||
// Shorten message if too long.
|
|
||||||
shortenedMsg := activeStatusMsg
|
|
||||||
if len(shortenedMsg) > shortenStatusMsgTo && strings.Contains(shortenedMsg, ". ") {
|
|
||||||
shortenedMsg = strings.SplitN(shortenedMsg, ". ", 2)[0]
|
|
||||||
}
|
|
||||||
if len(shortenedMsg) > shortenStatusMsgTo {
|
|
||||||
shortenedMsg = shortenedMsg[:shortenStatusMsgTo] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItemStatusMsg.SetTitle("Status: " + shortenedMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set SPN status if changed.
|
|
||||||
if spnStatus != nil && activeSPNStatus != spnStatus.Status {
|
|
||||||
activeSPNStatus = spnStatus.Status
|
|
||||||
menuItemSPNStatus.SetTitle("SPN: " + strings.Title(activeSPNStatus)) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set SPN switch if changed.
|
|
||||||
newSPNSwitch := "Enable SPN"
|
|
||||||
if spnEnabled.IsSet() {
|
|
||||||
newSPNSwitch = "Disable SPN"
|
|
||||||
}
|
|
||||||
if activeSPNSwitch != newSPNSwitch {
|
|
||||||
activeSPNSwitch = newSPNSwitch
|
|
||||||
menuItemSPNSwitch.SetTitle(activeSPNSwitch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clickListener(item *systray.MenuItem, fn func()) {
|
|
||||||
for range item.ClickedCh {
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchApp() {
|
|
||||||
// build path to app
|
|
||||||
pmStartPath := filepath.Join(dataDir, "portmaster-start")
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
pmStartPath += ".exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
// start app
|
|
||||||
cmd := exec.Command(pmStartPath, "app", "--data", dataDir)
|
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("failed to start app: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use cmd.Wait() instead of cmd.Process.Release() to properly release its resources.
|
|
||||||
// See https://github.com/golang/go/issues/36534
|
|
||||||
go func() {
|
|
||||||
err := cmd.Wait()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("failed to wait/release app process: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package wintoast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationBuilder struct {
|
|
||||||
templatePointer uintptr
|
|
||||||
lib *WinToast
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNotification(lib *WinToast, title string, message string) (*NotificationBuilder, error) {
|
|
||||||
lib.Lock()
|
|
||||||
defer lib.Unlock()
|
|
||||||
|
|
||||||
titleUTF, _ := windows.UTF16PtrFromString(title)
|
|
||||||
messageUTF, _ := windows.UTF16PtrFromString(message)
|
|
||||||
titleP := unsafe.Pointer(titleUTF)
|
|
||||||
messageP := unsafe.Pointer(messageUTF)
|
|
||||||
|
|
||||||
ptr, _, err := lib.createNotification.Call(uintptr(titleP), uintptr(messageP))
|
|
||||||
if ptr == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NotificationBuilder{ptr, lib}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationBuilder) Delete() {
|
|
||||||
if n == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n.lib.Lock()
|
|
||||||
defer n.lib.Unlock()
|
|
||||||
|
|
||||||
_, _, _ = n.lib.deleteNotification.Call(n.templatePointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationBuilder) AddButton(text string) error {
|
|
||||||
n.lib.Lock()
|
|
||||||
defer n.lib.Unlock()
|
|
||||||
textUTF, _ := windows.UTF16PtrFromString(text)
|
|
||||||
textP := unsafe.Pointer(textUTF)
|
|
||||||
|
|
||||||
rc, _, err := n.lib.addButton.Call(n.templatePointer, uintptr(textP))
|
|
||||||
if rc != 1 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationBuilder) SetImage(iconPath string) error {
|
|
||||||
n.lib.Lock()
|
|
||||||
defer n.lib.Unlock()
|
|
||||||
pathUTF, _ := windows.UTF16PtrFromString(iconPath)
|
|
||||||
pathP := unsafe.Pointer(pathUTF)
|
|
||||||
|
|
||||||
rc, _, err := n.lib.setImage.Call(n.templatePointer, uintptr(pathP))
|
|
||||||
if rc != 1 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationBuilder) SetSound(option int, path int) error {
|
|
||||||
n.lib.Lock()
|
|
||||||
defer n.lib.Unlock()
|
|
||||||
|
|
||||||
rc, _, err := n.lib.setSound.Call(n.templatePointer, uintptr(option), uintptr(path))
|
|
||||||
if rc != 1 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationBuilder) Show() (int64, error) {
|
|
||||||
n.lib.Lock()
|
|
||||||
defer n.lib.Unlock()
|
|
||||||
|
|
||||||
id, _, err := n.lib.showNotification.Call(n.templatePointer)
|
|
||||||
if int64(id) == -1 {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
return int64(id), nil
|
|
||||||
}
|
|
|
@ -1,217 +0,0 @@
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package wintoast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WinNotify holds the DLL handle.
|
|
||||||
type WinToast struct {
|
|
||||||
sync.RWMutex
|
|
||||||
|
|
||||||
dll *windows.DLL
|
|
||||||
|
|
||||||
initialized *abool.AtomicBool
|
|
||||||
|
|
||||||
initialize *windows.Proc
|
|
||||||
isInitialized *windows.Proc
|
|
||||||
createNotification *windows.Proc
|
|
||||||
deleteNotification *windows.Proc
|
|
||||||
addButton *windows.Proc
|
|
||||||
setImage *windows.Proc
|
|
||||||
setSound *windows.Proc
|
|
||||||
showNotification *windows.Proc
|
|
||||||
hideNotification *windows.Proc
|
|
||||||
setActivatedCallback *windows.Proc
|
|
||||||
setDismissedCallback *windows.Proc
|
|
||||||
setFailedCallback *windows.Proc
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(dllPath string) (*WinToast, error) {
|
|
||||||
if dllPath == "" {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: path to dll not specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject := &WinToast{}
|
|
||||||
libraryObject.initialized = abool.New()
|
|
||||||
|
|
||||||
// load dll
|
|
||||||
var err error
|
|
||||||
libraryObject.dll, err = windows.LoadDLL(dllPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: failed to load notifier dll %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// load functions
|
|
||||||
libraryObject.initialize, err = libraryObject.dll.FindProc("PortmasterToastInitialize")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastInitialize not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.isInitialized, err = libraryObject.dll.FindProc("PortmasterToastIsInitialized")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastIsInitialized not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.createNotification, err = libraryObject.dll.FindProc("PortmasterToastCreateNotification")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastCreateNotification not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.deleteNotification, err = libraryObject.dll.FindProc("PortmasterToastDeleteNotification")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastDeleteNotification not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.addButton, err = libraryObject.dll.FindProc("PortmasterToastAddButton")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastAddButton not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.setImage, err = libraryObject.dll.FindProc("PortmasterToastSetImage")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastSetImage not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.setSound, err = libraryObject.dll.FindProc("PortmasterToastSetSound")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastSetSound not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.showNotification, err = libraryObject.dll.FindProc("PortmasterToastShow")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastShow not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.setActivatedCallback, err = libraryObject.dll.FindProc("PortmasterToastActivatedCallback")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterActivatedCallback not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.setDismissedCallback, err = libraryObject.dll.FindProc("PortmasterToastDismissedCallback")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastDismissedCallback not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.setFailedCallback, err = libraryObject.dll.FindProc("PortmasterToastFailedCallback")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastFailedCallback not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryObject.hideNotification, err = libraryObject.dll.FindProc("PortmasterToastHide")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("winnotifiy: PortmasterToastHide not found %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryObject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lib *WinToast) Initialize(appName, aumi, originalShortcutPath string) error {
|
|
||||||
if lib == nil {
|
|
||||||
return fmt.Errorf("wintoast: lib object was nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
lib.Lock()
|
|
||||||
defer lib.Unlock()
|
|
||||||
|
|
||||||
// Initialize all necessary string for the notification meta data
|
|
||||||
appNameUTF, _ := windows.UTF16PtrFromString(appName)
|
|
||||||
aumiUTF, _ := windows.UTF16PtrFromString(aumi)
|
|
||||||
linkUTF, _ := windows.UTF16PtrFromString(originalShortcutPath)
|
|
||||||
|
|
||||||
// They are needed as unsafe pointers
|
|
||||||
appNameP := unsafe.Pointer(appNameUTF)
|
|
||||||
aumiP := unsafe.Pointer(aumiUTF)
|
|
||||||
linkP := unsafe.Pointer(linkUTF)
|
|
||||||
|
|
||||||
// Initialize notifications
|
|
||||||
rc, _, err := lib.initialize.Call(uintptr(appNameP), uintptr(aumiP), uintptr(linkP))
|
|
||||||
if rc != 0 {
|
|
||||||
return fmt.Errorf("wintoast: failed to initialize library rc = %d, %w", rc, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if if the initialization was successfully
|
|
||||||
rc, _, _ = lib.isInitialized.Call()
|
|
||||||
if rc == 1 {
|
|
||||||
lib.initialized.Set()
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("wintoast: initialized flag was not set: rc = %d", rc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lib *WinToast) SetCallbacks(activated func(id int64, actionIndex int32), dismissed func(id int64, reason int32), failed func(id int64, reason int32)) error {
|
|
||||||
if lib == nil {
|
|
||||||
return fmt.Errorf("wintoast: lib object was nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if lib.initialized.IsNotSet() {
|
|
||||||
return fmt.Errorf("winnotifiy: library not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize notification activated callback
|
|
||||||
callback := windows.NewCallback(func(id int64, actionIndex int32) uint64 {
|
|
||||||
activated(id, actionIndex)
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
rc, _, err := lib.setActivatedCallback.Call(callback)
|
|
||||||
if rc != 1 {
|
|
||||||
return fmt.Errorf("winnotifiy: failed to initialize activated callback %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize notification dismissed callback
|
|
||||||
callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 {
|
|
||||||
dismissed(id, actionIndex)
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
rc, _, err = lib.setDismissedCallback.Call(callback)
|
|
||||||
if rc != 1 {
|
|
||||||
return fmt.Errorf("winnotifiy: failed to initialize dismissed callback %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize notification failed callback
|
|
||||||
callback = windows.NewCallback(func(id int64, actionIndex int32) uint64 {
|
|
||||||
failed(id, actionIndex)
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
rc, _, err = lib.setFailedCallback.Call(callback)
|
|
||||||
if rc != 1 {
|
|
||||||
return fmt.Errorf("winnotifiy: failed to initialize failed callback %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNotification starts a creation of new notification. NotificationBuilder.Delete should allays be called when done using the object or there will be memory leeks
|
|
||||||
func (lib *WinToast) NewNotification(title string, content string) (*NotificationBuilder, error) {
|
|
||||||
if lib == nil {
|
|
||||||
return nil, fmt.Errorf("wintoast: lib object was nil")
|
|
||||||
}
|
|
||||||
return newNotification(lib, title, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HideNotification hides notification
|
|
||||||
func (lib *WinToast) HideNotification(id int64) error {
|
|
||||||
if lib == nil {
|
|
||||||
return fmt.Errorf("wintoast: lib object was nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
lib.Lock()
|
|
||||||
defer lib.Unlock()
|
|
||||||
|
|
||||||
rc, _, _ := lib.hideNotification.Call(uintptr(id))
|
|
||||||
|
|
||||||
if rc != 1 {
|
|
||||||
return fmt.Errorf("wintoast: failed to hide notification %d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"github.com/safing/portmaster/base/metrics"
|
"github.com/safing/portmaster/base/metrics"
|
||||||
"github.com/safing/portmaster/service/mgr"
|
"github.com/safing/portmaster/service/mgr"
|
||||||
"github.com/safing/portmaster/service/updates"
|
"github.com/safing/portmaster/service/updates"
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
"github.com/safing/portmaster/spn"
|
"github.com/safing/portmaster/spn"
|
||||||
"github.com/safing/portmaster/spn/captain"
|
"github.com/safing/portmaster/spn/captain"
|
||||||
"github.com/safing/portmaster/spn/conf"
|
"github.com/safing/portmaster/spn/conf"
|
||||||
|
@ -38,7 +37,6 @@ func main() {
|
||||||
|
|
||||||
// Configure user agent and updates.
|
// Configure user agent and updates.
|
||||||
updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
||||||
helper.IntelOnly()
|
|
||||||
|
|
||||||
// Configure SPN mode.
|
// Configure SPN mode.
|
||||||
conf.EnableClient(true)
|
conf.EnableClient(true)
|
||||||
|
|
6
cmds/portmaster-start/.gitignore
vendored
6
cmds/portmaster-start/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
||||||
# binaries
|
|
||||||
portmaster-start
|
|
||||||
portmaster-start.exe
|
|
||||||
|
|
||||||
# test dir
|
|
||||||
test
|
|
|
@ -1,77 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
||||||
# set build options
|
|
||||||
export CGO_ENABLED=0
|
|
||||||
|
|
||||||
# special handling for Windows
|
|
||||||
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/portmaster/base/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}" "$@"
|
|
|
@ -1,11 +0,0 @@
|
||||||
//go:build !windows
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "os/exec"
|
|
||||||
|
|
||||||
func attachToParentConsole() (attached bool, err error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideWindow(cmd *exec.Cmd) {}
|
|
|
@ -1,150 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(cleanStructureCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanStructureCmd = &cobra.Command{
|
|
||||||
Use: "clean-structure",
|
|
||||||
Short: "Create and clean the required directory structure",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := ensureLoggingDir(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return cleanAndEnsureExecDir()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanAndEnsureExecDir() error {
|
|
||||||
execDir := dataRoot.ChildDir("exec", 0o777)
|
|
||||||
|
|
||||||
// Clean up and remove exec dir.
|
|
||||||
err := os.RemoveAll(execDir.Path)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WARNING: failed to fully remove exec dir (%q) for cleaning: %s", execDir.Path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-create exec dir.
|
|
||||||
err = execDir.Ensure()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize exec dir (%q): %w", execDir.Path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
processInfo "github.com/shirou/gopsutil/process"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err error) {
|
|
||||||
lockFilePath := getLockFilePath(path, name, perUser)
|
|
||||||
|
|
||||||
// read current pid file
|
|
||||||
data, err := os.ReadFile(lockFilePath)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
// 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))
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
// Process exists, continue.
|
|
||||||
case errors.Is(err, processInfo.ErrorProcessNotRunning):
|
|
||||||
// A process with the locked PID does not exist.
|
|
||||||
// This is expected, so we can continue normally.
|
|
||||||
return 0, createInstanceLock(lockFilePath)
|
|
||||||
default:
|
|
||||||
// There was an internal error getting the process.
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the process paths and evaluate and clean them.
|
|
||||||
executingBinaryPath, err := p.Exe()
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to get path of existing process: %w", err)
|
|
||||||
}
|
|
||||||
cleanedExecutingBinaryPath, err := filepath.EvalSymlinks(executingBinaryPath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to evaluate path of existing process: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the binary is portmaster-start with high probability.
|
|
||||||
if !strings.Contains(filepath.Base(cleanedExecutingBinaryPath), "portmaster-start") {
|
|
||||||
// The process with the locked PID belongs to another binary.
|
|
||||||
// As the Portmaster usually starts very early, it will have a low PID,
|
|
||||||
// which could be assigned to another process on next boot.
|
|
||||||
return 0, createInstanceLock(lockFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return PID of already running instance.
|
|
||||||
return p.Pid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// TODO: Investigate required permissions.
|
|
||||||
err = os.WriteFile(lockFilePath, []byte(strconv.Itoa(os.Getpid())), 0o0666) //nolint:gosec
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteInstanceLock(path, name string, perUser bool) error {
|
|
||||||
return os.Remove(getLockFilePath(path, name, perUser))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLockFilePath(path, name string, perUser bool) string {
|
|
||||||
if !perUser {
|
|
||||||
return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-lock.pid", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user ID for per-user lock file.
|
|
||||||
var userID string
|
|
||||||
usr, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to get current user: %s\n", err)
|
|
||||||
userID = "no-user"
|
|
||||||
} else {
|
|
||||||
userID = usr.Uid
|
|
||||||
}
|
|
||||||
return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-%s-lock.pid", name, userID))
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/database/record"
|
|
||||||
"github.com/safing/portmaster/base/info"
|
|
||||||
"github.com/safing/structures/container"
|
|
||||||
"github.com/safing/structures/dsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initializeLogFile(logFilePath string, identifier string, version string) *os.File {
|
|
||||||
logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0o0440) //nolint:gosec // As desired.
|
|
||||||
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:unused // false positive on linux, currently used by windows only. TODO: move to a _windows file.
|
|
||||||
func logControlError(cErr error) {
|
|
||||||
// check if error present
|
|
||||||
if cErr == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errorFile := getPmStartLogFile(".error.log")
|
|
||||||
if errorFile == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = errorFile.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
fmt.Fprintln(errorFile, cErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:deadcode,unused // false positive on linux, currently used by windows only. TODO: move to a _windows file.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,257 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/dataroot"
|
|
||||||
"github.com/safing/portmaster/base/info"
|
|
||||||
portlog "github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/portmaster/base/updater"
|
|
||||||
"github.com/safing/portmaster/base/utils"
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dataDir string
|
|
||||||
maxRetries int
|
|
||||||
dataRoot *utils.DirStructure
|
|
||||||
logsRoot *utils.DirStructure
|
|
||||||
forceOldUI bool
|
|
||||||
|
|
||||||
updateURLFlag string
|
|
||||||
userAgentFlag string
|
|
||||||
|
|
||||||
// Create registry.
|
|
||||||
registry = &updater.ResourceRegistry{
|
|
||||||
Name: "updates",
|
|
||||||
UpdateURLs: []string{
|
|
||||||
"https://updates.safing.io",
|
|
||||||
},
|
|
||||||
UserAgent: fmt.Sprintf("Portmaster Start (%s %s)", runtime.GOOS, runtime.GOARCH),
|
|
||||||
Verification: helper.VerificationConfig,
|
|
||||||
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) {
|
|
||||||
mustLoadIndex := indexRequired(cmd)
|
|
||||||
if err := configureRegistry(mustLoadIndex); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ensureLoggingDir(); 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.StringVar(&updateURLFlag, "update-server", "", "Set an alternative update server (full URL)")
|
|
||||||
flags.StringVar(&userAgentFlag, "update-agent", "", "Set an alternative user agent for requests to the update server")
|
|
||||||
flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component")
|
|
||||||
flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.")
|
|
||||||
flags.BoolVar(&forceOldUI, "old-ui", false, "Use the old ui. (Beta)")
|
|
||||||
_ = rootCmd.MarkPersistentFlagDirname("data")
|
|
||||||
_ = flags.MarkHidden("input-signals")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cobra.OnInitialize(initCobra)
|
|
||||||
|
|
||||||
// set meta info
|
|
||||||
info.Set("Portmaster Start", "", "GPLv3")
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 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("[pmstart] ")
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
// not using portbase logger
|
|
||||||
portlog.SetLogLevel(portlog.CriticalLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func configureRegistry(mustLoadIndex bool) error {
|
|
||||||
// Check if update server URL supplied via flag is a valid URL.
|
|
||||||
if updateURLFlag != "" {
|
|
||||||
u, err := url.Parse(updateURLFlag)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("supplied update server URL is invalid: %w", err)
|
|
||||||
}
|
|
||||||
if u.Scheme != "https" {
|
|
||||||
return errors.New("supplied update server URL must use HTTPS")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override values from flags.
|
|
||||||
if userAgentFlag != "" {
|
|
||||||
registry.UserAgent = userAgentFlag
|
|
||||||
}
|
|
||||||
if updateURLFlag != "" {
|
|
||||||
registry.UpdateURLs = []string{updateURLFlag}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If dataDir is not set, check the environment variable.
|
|
||||||
if dataDir == "" {
|
|
||||||
dataDir = os.Getenv("PORTMASTER_DATA")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's still empty, try to auto-detect it.
|
|
||||||
if dataDir == "" {
|
|
||||||
dataDir = detectInstallationDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, if it's still empty, the user must provide it.
|
|
||||||
if dataDir == "" {
|
|
||||||
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove left over quotes.
|
|
||||||
dataDir = strings.Trim(dataDir, `\"`)
|
|
||||||
// Initialize data root.
|
|
||||||
err := dataroot.Initialize(dataDir, 0o0755)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize data root: %w", err)
|
|
||||||
}
|
|
||||||
dataRoot = dataroot.Root()
|
|
||||||
|
|
||||||
// Initialize registry.
|
|
||||||
err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateRegistryIndex(mustLoadIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureLoggingDir() error {
|
|
||||||
// set up logs root
|
|
||||||
logsRoot = dataRoot.ChildDir("logs", 0o0777)
|
|
||||||
err := logsRoot.Ensure()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, 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(mustLoadIndex bool) error {
|
|
||||||
// Set indexes based on the release channel.
|
|
||||||
warning := helper.SetIndexes(registry, "", false, false, false)
|
|
||||||
if warning != nil {
|
|
||||||
log.Printf("WARNING: %s\n", warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load indexes from disk or network, if needed and desired.
|
|
||||||
err := registry.LoadIndexes(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WARNING: error loading indexes: %s\n", err)
|
|
||||||
if mustLoadIndex {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load versions from disk to know which others we have and which are available.
|
|
||||||
err = registry.ScanStorage("")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WARNING: error during storage scan: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.SelectVersions()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectInstallationDir() string {
|
|
||||||
exePath, err := filepath.Abs(os.Args[0])
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := filepath.Dir(exePath)
|
|
||||||
stableJSONFile := filepath.Join(parent, "updates", "stable.json")
|
|
||||||
stat, err := os.Stat(stableJSONFile)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if stat.IsDir() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
baseDir="$( cd "$(dirname "$0")" && pwd )"
|
|
||||||
cd "$baseDir"
|
|
||||||
|
|
||||||
COL_OFF="\033[0m"
|
|
||||||
COL_BOLD="\033[01;01m"
|
|
||||||
COL_RED="\033[31m"
|
|
||||||
COL_GREEN="\033[32m"
|
|
||||||
COL_YELLOW="\033[33m"
|
|
||||||
|
|
||||||
destDirPart1="../../dist"
|
|
||||||
destDirPart2="start"
|
|
||||||
|
|
||||||
function prep {
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
function check {
|
|
||||||
prep
|
|
||||||
|
|
||||||
# check if file exists
|
|
||||||
if [[ -f $destPath ]]; then
|
|
||||||
echo "[start] $platform $version already built"
|
|
||||||
else
|
|
||||||
echo -e "${COL_BOLD}[start] $platform v$version${COL_OFF}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function build {
|
|
||||||
prep
|
|
||||||
|
|
||||||
# check if file exists
|
|
||||||
if [[ -f $destPath ]]; then
|
|
||||||
echo "[start] $platform already built in v$version, skipping..."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# build
|
|
||||||
./build
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_RED}BUILD FAILED.${COL_OFF}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p $(dirname $destPath)
|
|
||||||
cp $output $destPath
|
|
||||||
echo -e "\n${COL_BOLD}[start] $platform v$version: ${COL_GREEN}successfully built.${COL_OFF}"
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset {
|
|
||||||
prep
|
|
||||||
|
|
||||||
# delete if file exists
|
|
||||||
if [[ -f $destPath ]]; then
|
|
||||||
rm $destPath
|
|
||||||
echo "[start] $platform v$version deleted."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_all {
|
|
||||||
GOOS=linux GOARCH=amd64 check
|
|
||||||
GOOS=windows GOARCH=amd64 check
|
|
||||||
GOOS=darwin GOARCH=amd64 check
|
|
||||||
GOOS=linux GOARCH=arm64 check
|
|
||||||
GOOS=windows GOARCH=arm64 check
|
|
||||||
GOOS=darwin GOARCH=arm64 check
|
|
||||||
}
|
|
||||||
|
|
||||||
function build_all {
|
|
||||||
GOOS=linux GOARCH=amd64 build
|
|
||||||
GOOS=windows GOARCH=amd64 build
|
|
||||||
GOOS=darwin GOARCH=amd64 build
|
|
||||||
GOOS=linux GOARCH=arm64 build
|
|
||||||
GOOS=windows GOARCH=arm64 build
|
|
||||||
GOOS=darwin GOARCH=arm64 build
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset_all {
|
|
||||||
GOOS=linux GOARCH=amd64 reset
|
|
||||||
GOOS=windows GOARCH=amd64 reset
|
|
||||||
GOOS=darwin GOARCH=amd64 reset
|
|
||||||
GOOS=linux GOARCH=arm64 reset
|
|
||||||
GOOS=windows GOARCH=arm64 reset
|
|
||||||
GOOS=darwin GOARCH=arm64 reset
|
|
||||||
}
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"check" )
|
|
||||||
check_all
|
|
||||||
;;
|
|
||||||
"build" )
|
|
||||||
build_all
|
|
||||||
;;
|
|
||||||
"reset" )
|
|
||||||
reset_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
|
|
|
@ -1,82 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/service/firewall/interception"
|
|
||||||
)
|
|
||||||
|
|
||||||
var recoverIPTablesCmd = &cobra.Command{
|
|
||||||
Use: "recover-iptables",
|
|
||||||
Short: "Removes obsolete IP tables rules in case of an unclean shutdown",
|
|
||||||
RunE: func(*cobra.Command, []string) error {
|
|
||||||
// interception.DeactiveNfqueueFirewall uses coreos/go-iptables
|
|
||||||
// which shells out to the /sbin/iptables binary. As a result,
|
|
||||||
// we don't get the errno of the actual error and need to parse the
|
|
||||||
// output instead. Make sure it's always english by setting LC_ALL=C
|
|
||||||
currentLocale := os.Getenv("LC_ALL")
|
|
||||||
_ = os.Setenv("LC_ALL", "C")
|
|
||||||
defer func() {
|
|
||||||
_ = os.Setenv("LC_ALL", currentLocale)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := interception.DeactivateNfqueueFirewall()
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// we don't want to show ErrNotExists to the user
|
|
||||||
// as that only means portmaster did the cleanup itself.
|
|
||||||
var mr *multierror.Error
|
|
||||||
if !errors.As(err, &mr) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var filteredErrors *multierror.Error
|
|
||||||
for _, err := range mr.Errors {
|
|
||||||
// if we have a permission denied error, all errors will be the same
|
|
||||||
if strings.Contains(err.Error(), "Permission denied") {
|
|
||||||
return fmt.Errorf("failed to cleanup iptables: %w", os.ErrPermission)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), "No such file or directory") {
|
|
||||||
filteredErrors = multierror.Append(filteredErrors, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if filteredErrors != nil {
|
|
||||||
filteredErrors.ErrorFormat = formatNfqErrors
|
|
||||||
return filteredErrors.ErrorOrNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
SilenceUsage: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(recoverIPTablesCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatNfqErrors(es []error) string {
|
|
||||||
if len(es) == 1 {
|
|
||||||
return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
points := make([]string, len(es))
|
|
||||||
for i, err := range es {
|
|
||||||
// only display the very first line of each error
|
|
||||||
first := strings.Split(err.Error(), "\n")[0]
|
|
||||||
points[i] = fmt.Sprintf("* %s", first)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%d errors occurred:\n\t%s\n\n",
|
|
||||||
len(es), strings.Join(points, "\n\t"))
|
|
||||||
}
|
|
|
@ -1,486 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/tevino/abool"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// ControlledFailureExitCode is the exit code that any service started by
|
|
||||||
// portmaster-start can return in order to signify a controlled failure.
|
|
||||||
// This disables retrying and exits with an error code.
|
|
||||||
ControlledFailureExitCode = 24
|
|
||||||
|
|
||||||
// StartOldUIExitCode is an exit code that is returned by the UI when there. This is manfully triaged by the user, if the new UI does not work for them.
|
|
||||||
StartOldUIExitCode = 77
|
|
||||||
MissingDependencyExitCode = 0xc0000135 // Windows STATUS_DLL_NOT_FOUND
|
|
||||||
|
|
||||||
exeSuffix = ".exe"
|
|
||||||
zipSuffix = ".zip"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
runningInConsole bool
|
|
||||||
onWindows = runtime.GOOS == "windows"
|
|
||||||
stdinSignals bool
|
|
||||||
childIsRunning = abool.NewBool(false)
|
|
||||||
|
|
||||||
fallBackToOldUI bool = false
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options for starting component.
|
|
||||||
type Options struct {
|
|
||||||
Name string
|
|
||||||
Identifier string // component identifier
|
|
||||||
ShortIdentifier string // populated automatically
|
|
||||||
LockPathPrefix string
|
|
||||||
LockPerUser bool
|
|
||||||
PIDFile bool
|
|
||||||
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)
|
|
||||||
RestartOnFail bool // Try restarting automatically, if the started component fails.
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a temp value that will be used to test the new UI in beta.
|
|
||||||
var app2Options = Options{
|
|
||||||
Name: "Portmaster App2",
|
|
||||||
Identifier: "app2/portmaster-app",
|
|
||||||
AllowDownload: false,
|
|
||||||
AllowHidingWindow: false,
|
|
||||||
RestartOnFail: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Make sure the new UI has a proper extension.
|
|
||||||
if onWindows {
|
|
||||||
app2Options.Identifier += ".zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
registerComponent([]Options{
|
|
||||||
{
|
|
||||||
Name: "Portmaster Core",
|
|
||||||
Identifier: "core/portmaster-core",
|
|
||||||
AllowDownload: true,
|
|
||||||
AllowHidingWindow: true,
|
|
||||||
PIDFile: true,
|
|
||||||
RestartOnFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Portmaster App",
|
|
||||||
Identifier: "app/portmaster-app.zip",
|
|
||||||
AllowDownload: false,
|
|
||||||
AllowHidingWindow: false,
|
|
||||||
RestartOnFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Portmaster Notifier",
|
|
||||||
Identifier: "notifier/portmaster-notifier",
|
|
||||||
LockPerUser: true,
|
|
||||||
AllowDownload: false,
|
|
||||||
AllowHidingWindow: true,
|
|
||||||
PIDFile: true,
|
|
||||||
LockPathPrefix: "exec",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Safing Privacy Network",
|
|
||||||
Identifier: "hub/spn-hub",
|
|
||||||
AllowDownload: true,
|
|
||||||
AllowHidingWindow: true,
|
|
||||||
PIDFile: true,
|
|
||||||
RestartOnFail: true,
|
|
||||||
},
|
|
||||||
app2Options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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(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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS == "linux" && opts.ShortIdentifier == "app" {
|
|
||||||
// see https://www.freedesktop.org/software/systemd/man/pam_systemd.html#type=
|
|
||||||
if xdgSessionType := os.Getenv("XDG_SESSION_TYPE"); xdgSessionType == "wayland" {
|
|
||||||
// we're running the Portmaster UI App under Wayland so make sure we add some arguments
|
|
||||||
// required by Electron.
|
|
||||||
args = append(args,
|
|
||||||
[]string{
|
|
||||||
"--enable-features=UseOzonePlatform,WaylandWindowDecorations",
|
|
||||||
"--ozone-platform=wayland",
|
|
||||||
}...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, cmdArgs...)
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(opts *Options, cmdArgs []string) (err error) {
|
|
||||||
// set download option
|
|
||||||
registry.Online = opts.AllowDownload
|
|
||||||
|
|
||||||
if isShuttingDown() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for duplicate instances
|
|
||||||
if opts.PIDFile {
|
|
||||||
pid, err := checkAndCreateInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to exec lock: %w", err)
|
|
||||||
}
|
|
||||||
if pid != 0 {
|
|
||||||
return fmt.Errorf("another instance of %s is already running: PID %d", opts.Name, pid)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := deleteInstanceLock(opts.LockPathPrefix, opts.ShortIdentifier, opts.LockPerUser)
|
|
||||||
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 && !strings.HasSuffix(opts.Identifier, zipSuffix) {
|
|
||||||
opts.Identifier += exeSuffix
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, cmdArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 !opts.RestartOnFail || !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(false) // will always return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() == 0o0755 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights.
|
|
||||||
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: writing 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) {
|
|
||||||
// Auto-upgrade to new UI if in beta and new UI is not disabled or failed.
|
|
||||||
if opts.ShortIdentifier == "app" &&
|
|
||||||
registry.UsePreReleases &&
|
|
||||||
!forceOldUI &&
|
|
||||||
!fallBackToOldUI {
|
|
||||||
log.Println("auto-upgraded to new UI")
|
|
||||||
opts = &app2Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile arguments and add additional arguments based on system configuration.
|
|
||||||
// Extra parameters can be specified using "-- --some-parameter".
|
|
||||||
args = getExecArgs(opts, args)
|
|
||||||
|
|
||||||
file, err := registry.GetFile(
|
|
||||||
helper.PlatformIdentifier(opts.Identifier),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return true, fmt.Errorf("could not get component: %w", err)
|
|
||||||
}
|
|
||||||
binPath := file.Path()
|
|
||||||
|
|
||||||
// Adapt path for packaged software.
|
|
||||||
if strings.HasSuffix(binPath, zipSuffix) {
|
|
||||||
// Remove suffix from binary path.
|
|
||||||
binPath = strings.TrimSuffix(binPath, zipSuffix)
|
|
||||||
// Add binary with the same name to access the unpacked binary.
|
|
||||||
binPath = filepath.Join(binPath, filepath.Base(binPath))
|
|
||||||
|
|
||||||
// Adapt binary path on Windows.
|
|
||||||
if onWindows {
|
|
||||||
binPath += exeSuffix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check permission
|
|
||||||
if err := fixExecPerm(binPath); err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("starting %s %s\n", binPath, strings.Join(args, " "))
|
|
||||||
|
|
||||||
// create command
|
|
||||||
exc := exec.Command(binPath, args...)
|
|
||||||
|
|
||||||
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(3 * time.Minute): // portmaster core prints stack if not able to shutdown in 3 minutes, give it one more ...
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var exErr *exec.ExitError
|
|
||||||
if errors.As(err, &exErr) {
|
|
||||||
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
|
|
||||||
case ControlledFailureExitCode:
|
|
||||||
return false, errors.New("controlled failure, check logs")
|
|
||||||
case StartOldUIExitCode:
|
|
||||||
fallBackToOldUI = true
|
|
||||||
return true, errors.New("user requested old UI")
|
|
||||||
case MissingDependencyExitCode:
|
|
||||||
fallBackToOldUI = true
|
|
||||||
return true, errors.New("new UI failed with missing dependency")
|
|
||||||
default:
|
|
||||||
return true, fmt.Errorf("unknown exit code %w", exErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, fmt.Errorf("unexpected error type: %w", err)
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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{
|
|
||||||
Name: "Portmaster Core Service",
|
|
||||||
Identifier: "core/portmaster-core",
|
|
||||||
ShortIdentifier: "core",
|
|
||||||
AllowDownload: true,
|
|
||||||
AllowHidingWindow: false,
|
|
||||||
NoOutput: true,
|
|
||||||
RestartOnFail: 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(_ *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
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 registry but no logging.
|
|
||||||
return configureRegistry(false)
|
|
||||||
},
|
|
||||||
Short: "Show the command to run a Portmaster component yourself",
|
|
||||||
}
|
|
||||||
|
|
||||||
func show(opts *Options, cmdArgs []string) error {
|
|
||||||
// get original arguments
|
|
||||||
args := getExecArgs(opts, cmdArgs)
|
|
||||||
|
|
||||||
// adapt identifier
|
|
||||||
if onWindows {
|
|
||||||
opts.Identifier += exeSuffix
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := registry.GetFile(
|
|
||||||
helper.PlatformIdentifier(opts.Identifier),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not get component: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " "))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// startupComplete signals that the start procedure completed.
|
|
||||||
// The channel is not closed, just signaled once.
|
|
||||||
startupComplete = make(chan struct{})
|
|
||||||
|
|
||||||
// shuttingDown signals that we are shutting down.
|
|
||||||
// The channel will be closed, but may not be closed directly - only via initiateShutdown.
|
|
||||||
shuttingDown = make(chan struct{})
|
|
||||||
|
|
||||||
// shutdownError is protected by shutdownLock.
|
|
||||||
shutdownError error //nolint:unused,errname // Not what the linter thinks it is. Currently used on windows only.
|
|
||||||
shutdownLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func initiateShutdown(err error) {
|
|
||||||
shutdownLock.Lock()
|
|
||||||
defer shutdownLock.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-shuttingDown:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
shutdownError = err
|
|
||||||
close(shuttingDown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isShuttingDown() 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
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
portlog "github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/portmaster/base/updater"
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
reset bool
|
|
||||||
intelOnly bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
|
||||||
rootCmd.AddCommand(purgeCmd)
|
|
||||||
|
|
||||||
flags := updateCmd.Flags()
|
|
||||||
flags.BoolVar(&reset, "reset", false, "Delete all resources and re-download the basic set")
|
|
||||||
flags.BoolVar(&intelOnly, "intel-only", false, "Only make downloading intel updates mandatory")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
updateCmd = &cobra.Command{
|
|
||||||
Use: "update",
|
|
||||||
Short: "Run a manual update process",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return downloadUpdates()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
purgeCmd = &cobra.Command{
|
|
||||||
Use: "purge",
|
|
||||||
Short: "Remove old resource versions that are superseded by at least three versions",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return purge()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func indexRequired(cmd *cobra.Command) bool {
|
|
||||||
switch cmd {
|
|
||||||
case updateCmd, purgeCmd:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadUpdates() error {
|
|
||||||
// Check if only intel data is mandatory.
|
|
||||||
if intelOnly {
|
|
||||||
helper.IntelOnly()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set registry state notify callback.
|
|
||||||
registry.StateNotifyFunc = logProgress
|
|
||||||
|
|
||||||
// Set required updates.
|
|
||||||
registry.MandatoryUpdates = helper.MandatoryUpdates()
|
|
||||||
registry.AutoUnpack = helper.AutoUnpackUpdates()
|
|
||||||
|
|
||||||
if reset {
|
|
||||||
// Delete storage.
|
|
||||||
err := os.RemoveAll(registry.StorageDir().Path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to reset update dir: %w", err)
|
|
||||||
}
|
|
||||||
err = registry.StorageDir().Ensure()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create update dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset registry resources.
|
|
||||||
registry.ResetResources()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all indexes.
|
|
||||||
err := registry.UpdateIndexes(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if updates are available.
|
|
||||||
if len(registry.GetState().Updates.PendingDownload) == 0 {
|
|
||||||
log.Println("all resources are up to date")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download all required updates.
|
|
||||||
err = registry.DownloadUpdates(context.TODO(), true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select versions and unpack the selected.
|
|
||||||
registry.SelectVersions()
|
|
||||||
err = registry.UnpackResources()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to unpack resources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !intelOnly {
|
|
||||||
// Fix chrome-sandbox permissions
|
|
||||||
if err := helper.EnsureChromeSandboxPermissions(registry); err != nil {
|
|
||||||
return fmt.Errorf("failed to fix electron permissions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logProgress(state *updater.RegistryState) {
|
|
||||||
switch state.ID {
|
|
||||||
case updater.StateChecking:
|
|
||||||
if state.Updates.LastCheckAt == nil {
|
|
||||||
log.Println("checking for new versions")
|
|
||||||
}
|
|
||||||
case updater.StateDownloading:
|
|
||||||
if state.Details == nil {
|
|
||||||
log.Printf("downloading %d updates\n", len(state.Updates.PendingDownload))
|
|
||||||
} else if downloadDetails, ok := state.Details.(*updater.StateDownloadingDetails); ok {
|
|
||||||
if downloadDetails.FinishedUpTo < len(downloadDetails.Resources) {
|
|
||||||
log.Printf(
|
|
||||||
"[%d/%d] downloading %s",
|
|
||||||
downloadDetails.FinishedUpTo+1,
|
|
||||||
len(downloadDetails.Resources),
|
|
||||||
downloadDetails.Resources[downloadDetails.FinishedUpTo],
|
|
||||||
)
|
|
||||||
} else if state.Updates.LastDownloadAt == nil {
|
|
||||||
log.Println("finalizing downloads")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func purge() error {
|
|
||||||
portlog.SetLogLevel(portlog.TraceLevel)
|
|
||||||
|
|
||||||
// logging is configured as a persistent pre-run method inherited from
|
|
||||||
// the root command but since we don't use run.Run() we need to start
|
|
||||||
// logging ourself.
|
|
||||||
err := portlog.Start()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to start logging: %s\n", err)
|
|
||||||
}
|
|
||||||
defer portlog.Shutdown()
|
|
||||||
|
|
||||||
registry.Purge(3)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/jess"
|
|
||||||
"github.com/safing/jess/filesig"
|
|
||||||
portlog "github.com/safing/portmaster/base/log"
|
|
||||||
"github.com/safing/portmaster/base/updater"
|
|
||||||
"github.com/safing/portmaster/service/updates/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
verifyVerbose bool
|
|
||||||
verifyFix bool
|
|
||||||
|
|
||||||
verifyCmd = &cobra.Command{
|
|
||||||
Use: "verify",
|
|
||||||
Short: "Check integrity of updates / components",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return verifyUpdates(cmd.Context())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(verifyCmd)
|
|
||||||
|
|
||||||
flags := verifyCmd.Flags()
|
|
||||||
flags.BoolVarP(&verifyVerbose, "verbose", "v", false, "Enable verbose output")
|
|
||||||
flags.BoolVar(&verifyFix, "fix", false, "Delete and re-download broken components")
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyUpdates(ctx context.Context) error {
|
|
||||||
// Force registry to require signatures for all enabled scopes.
|
|
||||||
for _, opts := range registry.Verification {
|
|
||||||
if opts != nil {
|
|
||||||
opts.DownloadPolicy = updater.SignaturePolicyRequire
|
|
||||||
opts.DiskLoadPolicy = updater.SignaturePolicyRequire
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load indexes again to ensure they are correctly signed.
|
|
||||||
err := registry.LoadIndexes(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if verifyFix {
|
|
||||||
log.Println("[WARN] loading indexes failed, re-downloading...")
|
|
||||||
err = registry.UpdateIndexes(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to download indexes: %w", err)
|
|
||||||
}
|
|
||||||
log.Println("[ OK ] indexes re-downloaded and verified")
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("failed to verify indexes: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Println("[ OK ] indexes verified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all resources.
|
|
||||||
export := registry.Export()
|
|
||||||
var verified, fails, skipped int
|
|
||||||
for _, rv := range export {
|
|
||||||
for _, version := range rv.Versions {
|
|
||||||
// Don't verify files we don't have.
|
|
||||||
if !version.Available {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify file signature.
|
|
||||||
file := version.GetFile()
|
|
||||||
fileData, err := file.Verify()
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
verified++
|
|
||||||
if verifyVerbose {
|
|
||||||
verifOpts := registry.GetVerificationOptions(file.Identifier())
|
|
||||||
if verifOpts != nil {
|
|
||||||
log.Printf(
|
|
||||||
"[ OK ] valid signature for %s: signed by %s",
|
|
||||||
file.Path(), getSignedByMany(fileData, verifOpts.TrustStore),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
log.Printf("[ OK ] valid signature for %s", file.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case errors.Is(err, updater.ErrVerificationNotConfigured):
|
|
||||||
skipped++
|
|
||||||
if verifyVerbose {
|
|
||||||
log.Printf("[SKIP] no verification configured for %s", file.Path())
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Printf("[FAIL] failed to verify %s: %s", file.Path(), err)
|
|
||||||
fails++
|
|
||||||
if verifyFix {
|
|
||||||
// Delete file.
|
|
||||||
err = os.Remove(file.Path())
|
|
||||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path(), err)
|
|
||||||
} else {
|
|
||||||
// We should not be changing the version, but we are in a cmd-like
|
|
||||||
// scenario here without goroutines.
|
|
||||||
version.Available = false
|
|
||||||
}
|
|
||||||
// Delete file sig.
|
|
||||||
err = os.Remove(file.Path() + filesig.Extension)
|
|
||||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path()+filesig.Extension, err)
|
|
||||||
} else {
|
|
||||||
// We should not be changing the version, but we are in a cmd-like
|
|
||||||
// scenario here without goroutines.
|
|
||||||
version.SigAvailable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if verified > 0 {
|
|
||||||
log.Printf("[STAT] verified %d files", verified)
|
|
||||||
}
|
|
||||||
if skipped > 0 && verifyVerbose {
|
|
||||||
log.Printf("[STAT] skipped %d files (no verification configured)", skipped)
|
|
||||||
}
|
|
||||||
if fails > 0 {
|
|
||||||
if verifyFix {
|
|
||||||
log.Printf("[WARN] verification failed on %d files, re-downloading...", fails)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("failed to verify %d files", fails)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Everything was verified!
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start logging system for update process.
|
|
||||||
portlog.SetLogLevel(portlog.InfoLevel)
|
|
||||||
err = portlog.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] failed to start logging for monitoring update process: %s\n", err)
|
|
||||||
}
|
|
||||||
defer portlog.Shutdown()
|
|
||||||
|
|
||||||
// Re-download broken files.
|
|
||||||
registry.MandatoryUpdates = helper.MandatoryUpdates()
|
|
||||||
registry.AutoUnpack = helper.AutoUnpackUpdates()
|
|
||||||
err = registry.DownloadUpdates(ctx, true)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to re-download files: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string {
|
|
||||||
signedBy := make([]string, 0, len(fds))
|
|
||||||
for _, fd := range fds {
|
|
||||||
if sig := fd.Signature(); sig != nil {
|
|
||||||
for _, seal := range sig.Signatures {
|
|
||||||
if signet, err := trustStore.GetSignet(seal.ID, true); err == nil {
|
|
||||||
signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID))
|
|
||||||
} else {
|
|
||||||
signedBy = append(signedBy, seal.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(signedBy, " and ")
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/info"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
showShortVersion bool
|
|
||||||
showAllVersions bool
|
|
||||||
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 registry to be configured.
|
|
||||||
if err := configureRegistry(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
RunE: func(*cobra.Command, []string) error {
|
|
||||||
if !showAllVersions {
|
|
||||||
if showShortVersion {
|
|
||||||
fmt.Println(info.Version())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(info.FullVersion())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("portmaster-start %s\n\n", info.Version())
|
|
||||||
fmt.Printf("Assets:\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)
|
|
||||||
}
|
|
||||||
return tw.Flush()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flags := versionCmd.Flags()
|
|
||||||
{
|
|
||||||
flags.BoolVar(&showShortVersion, "short", false, "Print only the version number.")
|
|
||||||
flags.BoolVar(&showAllVersions, "all", false, "Dump versions for all assets.")
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd.AddCommand(versionCmd)
|
|
||||||
}
|
|
|
@ -21,7 +21,7 @@ type testInstance struct {
|
||||||
|
|
||||||
var _ instance = &testInstance{}
|
var _ instance = &testInstance{}
|
||||||
|
|
||||||
func (stub *testInstance) Updates() *updates.Updates {
|
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||||
return stub.updates
|
return stub.updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,15 @@ func runTest(m *testing.M) error {
|
||||||
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
installDir, err := os.MkdirTemp("", "geoip_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
stub.db, err = dbmodule.New(stub)
|
stub.db, err = dbmodule.New(stub)
|
||||||
|
@ -68,7 +77,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create api: %w", err)
|
return fmt.Errorf("failed to create api: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub)
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ type testInstance struct {
|
||||||
|
|
||||||
var _ instance = &testInstance{}
|
var _ instance = &testInstance{}
|
||||||
|
|
||||||
func (stub *testInstance) Updates() *updates.Updates {
|
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||||
return stub.updates
|
return stub.updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,15 @@ func runTest(m *testing.M) error {
|
||||||
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
installDir, err := os.MkdirTemp("", "netenv_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
stub.db, err = dbmodule.New(stub)
|
stub.db, err = dbmodule.New(stub)
|
||||||
|
@ -68,7 +77,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create api: %w", err)
|
return fmt.Errorf("failed to create api: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub)
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ type testInstance struct {
|
||||||
geoip *geoip.GeoIP
|
geoip *geoip.GeoIP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (stub *testInstance) Updates() *updates.Updates {
|
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||||
return stub.updates
|
return stub.updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,16 @@ func runTest(m *testing.M) error {
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
|
||||||
|
installDir, err := os.MkdirTemp("", "endpoints_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
stub.db, err = dbmodule.New(stub)
|
stub.db, err = dbmodule.New(stub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -74,7 +84,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create api: %w", err)
|
return fmt.Errorf("failed to create api: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub)
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,7 @@ type testInstance struct {
|
||||||
netenv *netenv.NetEnv
|
netenv *netenv.NetEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
// var _ instance = &testInstance{}
|
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||||
|
|
||||||
func (stub *testInstance) Updates() *updates.Updates {
|
|
||||||
return stub.updates
|
return stub.updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +68,16 @@ func runTest(m *testing.M) error {
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
|
||||||
|
installDir, err := os.MkdirTemp("", "resolver_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
stub.db, err = dbmodule.New(stub)
|
stub.db, err = dbmodule.New(stub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,7 +99,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create netenv: %w", err)
|
return fmt.Errorf("failed to create netenv: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub)
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Portmaster by Safing
|
|
||||||
Documentation=https://safing.io
|
|
||||||
Documentation=https://docs.safing.io
|
|
||||||
Before=nss-lookup.target network.target shutdown.target
|
|
||||||
After=systemd-networkd.service
|
|
||||||
Conflicts=shutdown.target
|
|
||||||
Conflicts=firewalld.service
|
|
||||||
Wants=nss-lookup.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=10
|
|
||||||
LockPersonality=yes
|
|
||||||
MemoryDenyWriteExecute=yes
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
PIDFile=/opt/safing/portmaster/core-lock.pid
|
|
||||||
Environment=LOGLEVEL=info
|
|
||||||
Environment=PORTMASTER_ARGS=
|
|
||||||
EnvironmentFile=-/etc/default/portmaster
|
|
||||||
ProtectSystem=true
|
|
||||||
#ReadWritePaths=/var/lib/portmaster
|
|
||||||
#ReadWritePaths=/run/xtables.lock
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
|
||||||
RestrictNamespaces=yes
|
|
||||||
# In future version portmaster will require access to user home
|
|
||||||
# directories to verify application permissions.
|
|
||||||
ProtectHome=read-only
|
|
||||||
ProtectKernelTunables=yes
|
|
||||||
ProtectKernelLogs=yes
|
|
||||||
ProtectControlGroups=yes
|
|
||||||
PrivateDevices=yes
|
|
||||||
AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
|
||||||
CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid cap_sys_resource cap_bpf cap_perfmon
|
|
||||||
# SystemCallArchitectures=native
|
|
||||||
# SystemCallFilter=@system-service @module
|
|
||||||
# SystemCallErrorNumber=EPERM
|
|
||||||
ExecStart=/opt/safing/portmaster/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
|
|
||||||
ExecStopPost=-/opt/safing/portmaster/portmaster-start recover-iptables
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -3,6 +3,7 @@ package updates
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -179,3 +180,50 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string,
|
||||||
// `dirPath + filename` is guaranteed by path.Split()
|
// `dirPath + filename` is guaranteed by path.Split()
|
||||||
return dirPath + filename, version, true
|
return dirPath + filename, version, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateMockFolder generates mock bundle folder for testing.
|
||||||
|
func GenerateMockFolder(dir, name, version string) error {
|
||||||
|
// Make sure dir exists
|
||||||
|
_ = os.MkdirAll(dir, defaultDirMode)
|
||||||
|
|
||||||
|
// Create empty files
|
||||||
|
file, err := os.Create(filepath.Join(dir, "portmaster"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = file.Close()
|
||||||
|
file, err = os.Create(filepath.Join(dir, "portmaster-core"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = file.Close()
|
||||||
|
file, err = os.Create(filepath.Join(dir, "portmaster.zip"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = file.Close()
|
||||||
|
file, err = os.Create(filepath.Join(dir, "assets.zip"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = file.Close()
|
||||||
|
|
||||||
|
bundle, err := GenerateBundleFromDir(dir, BundleFileSettings{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleStr, err := json.MarshalIndent(bundle, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to marshal bundle: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "index.json"), bundleStr, defaultFileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -40,8 +40,10 @@ func CreateDownloader(index UpdateIndex) Downloader {
|
||||||
|
|
||||||
func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
||||||
// Make sure dir exists
|
// Make sure dir exists
|
||||||
_ = os.MkdirAll(d.dir, defaultDirMode)
|
err := os.MkdirAll(d.dir, defaultDirMode)
|
||||||
var err error
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory for updates: %s", d.dir)
|
||||||
|
}
|
||||||
var content string
|
var content string
|
||||||
for _, url := range d.indexURLs {
|
for _, url := range d.indexURLs {
|
||||||
content, err = d.downloadIndexFileFromURL(ctx, url)
|
content, err = d.downloadIndexFileFromURL(ctx, url)
|
||||||
|
@ -50,15 +52,15 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Downloading was successful.
|
// Downloading was successful.
|
||||||
|
var bundle *Bundle
|
||||||
bundle, err := ParseBundle(content)
|
bundle, err = ParseBundle(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: %s", err)
|
log.Warningf("updates: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Parsing was successful
|
// Parsing was successful
|
||||||
|
var version *semver.Version
|
||||||
version, err := semver.NewVersion(d.bundle.Version)
|
version, err = semver.NewVersion(d.bundle.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: failed to parse bundle version: %s", err)
|
log.Warningf("updates: failed to parse bundle version: %s", err)
|
||||||
continue
|
continue
|
||||||
|
@ -79,7 +81,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
||||||
indexFilepath := filepath.Join(d.dir, d.indexFile)
|
indexFilepath := filepath.Join(d.dir, d.indexFile)
|
||||||
err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode)
|
err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write index file: %s", err)
|
return fmt.Errorf("failed to write index file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api"
|
|
||||||
"github.com/safing/portmaster/base/config"
|
|
||||||
"github.com/safing/portmaster/base/log"
|
"github.com/safing/portmaster/base/log"
|
||||||
"github.com/safing/portmaster/base/notifications"
|
"github.com/safing/portmaster/base/notifications"
|
||||||
"github.com/safing/portmaster/service/mgr"
|
"github.com/safing/portmaster/service/mgr"
|
||||||
|
@ -215,8 +213,6 @@ func (u *Updates) Stop() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type instance interface {
|
type instance interface {
|
||||||
API() *api.API
|
|
||||||
Config() *config.Config
|
|
||||||
Restart()
|
Restart()
|
||||||
Shutdown()
|
Shutdown()
|
||||||
Notifications() *notifications.Notifications
|
Notifications() *notifications.Notifications
|
||||||
|
|
89
service/updates/updates_test.go
Normal file
89
service/updates/updates_test.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/base/notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testInstance struct{}
|
||||||
|
|
||||||
|
func (i *testInstance) Restart() {}
|
||||||
|
func (i *testInstance) Shutdown() {}
|
||||||
|
|
||||||
|
func (i *testInstance) Notifications() *notifications.Notifications {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *testInstance) Ready() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *testInstance) SetCmdLineOperation(f func() error) {}
|
||||||
|
|
||||||
|
func TestPreformUpdate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Initialize mock instance
|
||||||
|
stub := &testInstance{}
|
||||||
|
|
||||||
|
// Make tmp dirs
|
||||||
|
installedDir, err := os.MkdirTemp("", "updates_current")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installedDir) }()
|
||||||
|
updateDir, err := os.MkdirTemp("", "updates_new")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(updateDir) }()
|
||||||
|
purgeDir, err := os.MkdirTemp("", "updates_purge")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(purgeDir) }()
|
||||||
|
|
||||||
|
// Generate mock files
|
||||||
|
if err := GenerateMockFolder(installedDir, "Test", "1.0.0"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := GenerateMockFolder(updateDir, "Test", "1.0.1"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create updater
|
||||||
|
updates, err := New(stub, "Test", UpdateIndex{
|
||||||
|
Directory: installedDir,
|
||||||
|
DownloadDirectory: updateDir,
|
||||||
|
PurgeDirectory: purgeDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
AutoApply: false,
|
||||||
|
NeedsRestart: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Read and parse the index file
|
||||||
|
if err := updates.downloader.Verify(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Try to apply the updates
|
||||||
|
err = updates.applyUpdates(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHeck if the current version is now the new.
|
||||||
|
bundle, err := LoadBundle(filepath.Join(installedDir, "index.json"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bundle.Version != "1.0.1" {
|
||||||
|
panic(fmt.Errorf("expected version 1.0.1 found %s", bundle.Version))
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ type testInstance struct {
|
||||||
base *base.Base
|
base *base.Base
|
||||||
}
|
}
|
||||||
|
|
||||||
func (stub *testInstance) Updates() *updates.Updates {
|
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||||
return stub.updates
|
return stub.updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,16 @@ func runTest(m *testing.M) error {
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
|
||||||
|
installDir, err := os.MkdirTemp("", "hub_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
// Init
|
// Init
|
||||||
stub.db, err = dbmodule.New(stub)
|
stub.db, err = dbmodule.New(stub)
|
||||||
|
@ -76,7 +86,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create config: %w", err)
|
return fmt.Errorf("failed to create config: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub)
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,12 @@ type Instance struct {
|
||||||
runtime *runtime.Runtime
|
runtime *runtime.Runtime
|
||||||
rng *rng.Rng
|
rng *rng.Rng
|
||||||
|
|
||||||
core *core.Core
|
core *core.Core
|
||||||
updates *updates.Updates
|
binaryUpdates *updates.Updates
|
||||||
geoip *geoip.GeoIP
|
intelUpdates *updates.Updates
|
||||||
netenv *netenv.NetEnv
|
geoip *geoip.GeoIP
|
||||||
filterLists *filterlists.FilterLists
|
netenv *netenv.NetEnv
|
||||||
|
filterLists *filterlists.FilterLists
|
||||||
|
|
||||||
access *access.Access
|
access *access.Access
|
||||||
cabin *cabin.Cabin
|
cabin *cabin.Cabin
|
||||||
|
@ -74,6 +75,14 @@ func New() (*Instance, error) {
|
||||||
instance := &Instance{}
|
instance := &Instance{}
|
||||||
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
|
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
binaryUpdateIndex := updates.UpdateIndex{
|
||||||
|
// FIXME: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
intelUpdateIndex := updates.UpdateIndex{
|
||||||
|
// FIXME: fill
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Base modules
|
// Base modules
|
||||||
|
@ -111,7 +120,11 @@ func New() (*Instance, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return instance, fmt.Errorf("create core module: %w", err)
|
return instance, fmt.Errorf("create core module: %w", err)
|
||||||
}
|
}
|
||||||
instance.updates, err = updates.New(instance)
|
instance.binaryUpdates, err = updates.New(instance, "Binary Updater", binaryUpdateIndex)
|
||||||
|
if err != nil {
|
||||||
|
return instance, fmt.Errorf("create updates module: %w", err)
|
||||||
|
}
|
||||||
|
instance.intelUpdates, err = updates.New(instance, "Intel Updater", intelUpdateIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return instance, fmt.Errorf("create updates module: %w", err)
|
return instance, fmt.Errorf("create updates module: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -181,7 +194,8 @@ func New() (*Instance, error) {
|
||||||
instance.rng,
|
instance.rng,
|
||||||
|
|
||||||
instance.core,
|
instance.core,
|
||||||
instance.updates,
|
instance.binaryUpdates,
|
||||||
|
instance.intelUpdates,
|
||||||
instance.geoip,
|
instance.geoip,
|
||||||
instance.netenv,
|
instance.netenv,
|
||||||
|
|
||||||
|
@ -255,9 +269,14 @@ func (i *Instance) Base() *base.Base {
|
||||||
return i.base
|
return i.base
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates returns the updates module.
|
// BinaryUpdates returns the updates module.
|
||||||
func (i *Instance) Updates() *updates.Updates {
|
func (i *Instance) BinaryUpdates() *updates.Updates {
|
||||||
return i.updates
|
return i.binaryUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntelUpdates returns the updates module.
|
||||||
|
func (i *Instance) IntelUpdates() *updates.Updates {
|
||||||
|
return i.intelUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeoIP returns the geoip module.
|
// GeoIP returns the geoip module.
|
||||||
|
|
|
@ -62,6 +62,16 @@ func runTest(m *testing.M) error {
|
||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(ds) }()
|
defer func() { _ = os.RemoveAll(ds) }()
|
||||||
|
|
||||||
|
installDir, err := os.MkdirTemp("", "geoip_installdir")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tmp install dir: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(installDir) }()
|
||||||
|
err = updates.GenerateMockFolder(installDir, "Test Intel", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate mock installation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
stub := &testInstance{}
|
stub := &testInstance{}
|
||||||
log.SetLogLevel(log.DebugLevel)
|
log.SetLogLevel(log.DebugLevel)
|
||||||
|
|
||||||
|
@ -78,7 +88,10 @@ func runTest(m *testing.M) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create config: %w", err)
|
return fmt.Errorf("failed to create config: %w", err)
|
||||||
}
|
}
|
||||||
stub.updates, err = updates.New(stub, "Intel Test", updates.UpdateIndex{})
|
stub.updates, err = updates.New(stub, "Test Intel", updates.UpdateIndex{
|
||||||
|
Directory: installDir,
|
||||||
|
IndexFile: "index.json",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create updates: %w", err)
|
return fmt.Errorf("failed to create updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue