mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19: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/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
"github.com/safing/portmaster/spn"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -40,7 +39,7 @@ func main() {
|
|||
|
||||
// Configure user agent and updates.
|
||||
updates.UserAgent = fmt.Sprintf("SPN Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
||||
helper.IntelOnly()
|
||||
// helper.IntelOnly()
|
||||
|
||||
// Set SPN public hub mode.
|
||||
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/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/safing/portmaster/service/updates/helper"
|
||||
"github.com/safing/portmaster/spn"
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
|
@ -38,7 +37,6 @@ func main() {
|
|||
|
||||
// Configure user agent and updates.
|
||||
updates.UserAgent = fmt.Sprintf("SPN Observation Hub (%s %s)", runtime.GOOS, runtime.GOARCH)
|
||||
helper.IntelOnly()
|
||||
|
||||
// Configure SPN mode.
|
||||
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{}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,15 @@ func runTest(m *testing.M) error {
|
|||
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||
}
|
||||
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.db, err = dbmodule.New(stub)
|
||||
|
@ -68,7 +77,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ type testInstance struct {
|
|||
|
||||
var _ instance = &testInstance{}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,15 @@ func runTest(m *testing.M) error {
|
|||
return fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||
}
|
||||
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.db, err = dbmodule.New(stub)
|
||||
|
@ -68,7 +77,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ type testInstance struct {
|
|||
geoip *geoip.GeoIP
|
||||
}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,16 @@ func runTest(m *testing.M) error {
|
|||
}
|
||||
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.db, err = dbmodule.New(stub)
|
||||
if err != nil {
|
||||
|
@ -74,7 +84,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
|
|
|
@ -26,9 +26,7 @@ type testInstance struct {
|
|||
netenv *netenv.NetEnv
|
||||
}
|
||||
|
||||
// var _ instance = &testInstance{}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
|
@ -70,6 +68,16 @@ func runTest(m *testing.M) error {
|
|||
}
|
||||
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.db, err = dbmodule.New(stub)
|
||||
if err != nil {
|
||||
|
@ -91,7 +99,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
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 (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -179,3 +180,50 @@ func getIdentifierAndVersion(versionedPath string) (identifier, version string,
|
|||
// `dirPath + filename` is guaranteed by path.Split()
|
||||
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 {
|
||||
// Make sure dir exists
|
||||
_ = os.MkdirAll(d.dir, defaultDirMode)
|
||||
var err error
|
||||
err := os.MkdirAll(d.dir, defaultDirMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory for updates: %s", d.dir)
|
||||
}
|
||||
var content string
|
||||
for _, url := range d.indexURLs {
|
||||
content, err = d.downloadIndexFileFromURL(ctx, url)
|
||||
|
@ -50,15 +52,15 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
|||
continue
|
||||
}
|
||||
// Downloading was successful.
|
||||
|
||||
bundle, err := ParseBundle(content)
|
||||
var bundle *Bundle
|
||||
bundle, err = ParseBundle(content)
|
||||
if err != nil {
|
||||
log.Warningf("updates: %s", err)
|
||||
continue
|
||||
}
|
||||
// Parsing was successful
|
||||
|
||||
version, err := semver.NewVersion(d.bundle.Version)
|
||||
var version *semver.Version
|
||||
version, err = semver.NewVersion(d.bundle.Version)
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to parse bundle version: %s", err)
|
||||
continue
|
||||
|
@ -79,7 +81,7 @@ func (d *Downloader) downloadIndexFile(ctx context.Context) error {
|
|||
indexFilepath := filepath.Join(d.dir, d.indexFile)
|
||||
err = os.WriteFile(indexFilepath, []byte(content), defaultFileMode)
|
||||
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
|
||||
|
|
|
@ -6,8 +6,6 @@ import (
|
|||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
|
@ -215,8 +213,6 @@ func (u *Updates) Stop() error {
|
|||
}
|
||||
|
||||
type instance interface {
|
||||
API() *api.API
|
||||
Config() *config.Config
|
||||
Restart()
|
||||
Shutdown()
|
||||
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
|
||||
}
|
||||
|
||||
func (stub *testInstance) Updates() *updates.Updates {
|
||||
func (stub *testInstance) IntelUpdates() *updates.Updates {
|
||||
return stub.updates
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,16 @@ func runTest(m *testing.M) error {
|
|||
}
|
||||
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{}
|
||||
// Init
|
||||
stub.db, err = dbmodule.New(stub)
|
||||
|
@ -76,7 +86,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
|
|
|
@ -48,11 +48,12 @@ type Instance struct {
|
|||
runtime *runtime.Runtime
|
||||
rng *rng.Rng
|
||||
|
||||
core *core.Core
|
||||
updates *updates.Updates
|
||||
geoip *geoip.GeoIP
|
||||
netenv *netenv.NetEnv
|
||||
filterLists *filterlists.FilterLists
|
||||
core *core.Core
|
||||
binaryUpdates *updates.Updates
|
||||
intelUpdates *updates.Updates
|
||||
geoip *geoip.GeoIP
|
||||
netenv *netenv.NetEnv
|
||||
filterLists *filterlists.FilterLists
|
||||
|
||||
access *access.Access
|
||||
cabin *cabin.Cabin
|
||||
|
@ -74,6 +75,14 @@ func New() (*Instance, error) {
|
|||
instance := &Instance{}
|
||||
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
|
||||
|
||||
binaryUpdateIndex := updates.UpdateIndex{
|
||||
// FIXME: fill
|
||||
}
|
||||
|
||||
intelUpdateIndex := updates.UpdateIndex{
|
||||
// FIXME: fill
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Base modules
|
||||
|
@ -111,7 +120,11 @@ func New() (*Instance, error) {
|
|||
if err != nil {
|
||||
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 {
|
||||
return instance, fmt.Errorf("create updates module: %w", err)
|
||||
}
|
||||
|
@ -181,7 +194,8 @@ func New() (*Instance, error) {
|
|||
instance.rng,
|
||||
|
||||
instance.core,
|
||||
instance.updates,
|
||||
instance.binaryUpdates,
|
||||
instance.intelUpdates,
|
||||
instance.geoip,
|
||||
instance.netenv,
|
||||
|
||||
|
@ -255,9 +269,14 @@ func (i *Instance) Base() *base.Base {
|
|||
return i.base
|
||||
}
|
||||
|
||||
// Updates returns the updates module.
|
||||
func (i *Instance) Updates() *updates.Updates {
|
||||
return i.updates
|
||||
// BinaryUpdates returns the updates module.
|
||||
func (i *Instance) BinaryUpdates() *updates.Updates {
|
||||
return i.binaryUpdates
|
||||
}
|
||||
|
||||
// IntelUpdates returns the updates module.
|
||||
func (i *Instance) IntelUpdates() *updates.Updates {
|
||||
return i.intelUpdates
|
||||
}
|
||||
|
||||
// GeoIP returns the geoip module.
|
||||
|
|
|
@ -62,6 +62,16 @@ func runTest(m *testing.M) error {
|
|||
}
|
||||
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{}
|
||||
log.SetLogLevel(log.DebugLevel)
|
||||
|
||||
|
@ -78,7 +88,10 @@ func runTest(m *testing.M) error {
|
|||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to create updates: %w", err)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue