Files
rvbox/cmd/rvbox/service_windows.go
T

125 lines
4.3 KiB
Go

//go:build windows
package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
clientwindows "github.com/rvbox/rvbox/internal/client/supervisor/windows"
"github.com/rvbox/rvbox/internal/client/windowsservice"
"github.com/rvbox/rvbox/internal/client/windowstray"
"golang.org/x/sys/windows/svc"
)
func defaultClientConfigPath() string {
root := os.Getenv("ProgramData")
if root == "" {
root = `C:\ProgramData`
}
return filepath.Join(root, "RVBox", "client.toml")
}
func runService(configPath string, diagnostics io.Writer) error {
inService, err := svc.IsWindowsService()
if err != nil {
return fmt.Errorf("detect service control manager context: %w", err)
}
if !inService {
return errors.New("--service is reserved for the installed Windows service")
}
return runWindowsService(configPath, diagnostics)
}
func runWindowsService(configPath string, diagnostics io.Writer) error {
return windowsservice.Run(func(ctx context.Context) error {
// The tray endpoint lives in the same LocalSystem service process. It
// has no store access; the handler below returns only bounded status/path
// data and rechecks SCM authorization in the native pipe adapter.
go func() {
if err := windowstray.Serve(ctx, func(requestContext context.Context, _ windowstray.Peer, request windowstray.Frame) (windowstray.Frame, error) {
return handleTrayRequest(requestContext, configPath, request)
}); err != nil && ctx.Err() == nil && diagnostics != nil {
_, _ = fmt.Fprintf(diagnostics, "rvbox tray endpoint stopped: %v\n", err)
}
}()
return runClientDaemon(ctx, configPath, diagnostics)
})
}
func runTray(configPath string, diagnostics io.Writer) error {
_ = configPath // the tray obtains the canonical paths from the service.
return windowstray.Run(context.Background(), diagnostics)
}
func runLauncher(channel string, _ io.Writer) error {
return clientwindows.RunLauncher(context.Background(), channel)
}
func runSignalHelper(channel string, _ io.Writer) error {
return clientwindows.RunSignalHelper(context.Background(), channel)
}
func handleTrayRequest(ctx context.Context, configPath string, request windowstray.Frame) (windowstray.Frame, error) {
response := windowstray.Frame{Action: windowstray.ActionStatus}
switch request.Action {
case windowstray.ActionStatus:
response.Payload = []byte("RVBox service=running")
case windowstray.ActionOpenConfig:
response.Payload = []byte(configPath)
case windowstray.ActionOpenLog:
configured, err := loadClientConfig(configPath)
if err != nil {
return response, err
}
logPath := configured.Observability.LogFile
if logPath == "" {
logPath = filepath.Join(filepath.Dir(configPath), "logs", "rvbox.log")
}
response.Payload = []byte(logPath)
case windowstray.ActionStartService:
if err := windowsservice.Start(); err != nil {
return response, err
}
response.Payload = []byte("RVBox service start requested")
case windowstray.ActionStopService:
// A service cannot synchronously wait for its own stop request from the
// pipe handler: the SCM callback must return so the process can unwind.
go func() { _ = windowsservice.Stop(30) }()
response.Payload = []byte("RVBox service stop requested")
case windowstray.ActionRestartService:
// Restart must be performed by the external UAC helper. If the tray is
// already elevated, its native fallback still launches the canonical
// --restart-service mode outside this service process.
return response, errors.New("restart requires the external service helper")
case windowstray.ActionSetAutomatic:
if err := windowsservice.Configure(windowsservice.StartupAutomatic); err != nil {
return response, err
}
response.Payload = []byte("RVBox service startup set to automatic")
case windowstray.ActionSetManual:
if err := windowsservice.Configure(windowsservice.StartupManual); err != nil {
return response, err
}
response.Payload = []byte("RVBox service startup set to manual")
case windowstray.ActionExitTray:
response.Payload = []byte("tray exit acknowledged")
default:
return response, fmt.Errorf("unknown tray action %d", request.Action)
}
if len(response.Payload) > 4<<10 {
response.Payload = []byte(strings.TrimSpace(string(response.Payload[:4<<10])))
}
select {
case <-ctx.Done():
return response, ctx.Err()
default:
return response, nil
}
}