Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APP-7157 windows support #52

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
66fe210
factor out runPlatformProvisioning, setupProvisioningPaths, ignoredSi…
abe-winter Dec 9, 2024
758477b
utils.go platform imp
abe-winter Dec 9, 2024
7f33956
windows build working
abe-winter Dec 12, 2024
12110a7
agent starts on windows, fails at starting viam-server
abe-winter Dec 12, 2024
767f492
factor out WaitOnline
abe-winter Dec 12, 2024
d6368fc
checkpoint
abe-winter Dec 16, 2024
ad4966a
don't return zero interval
abe-winter Dec 31, 2024
5f8fa2b
cleaner error when bin missing
abe-winter Dec 31, 2024
b0cff46
Merge branch 'main' into aw-windows
abe-winter Jan 1, 2025
e852518
setupProvisioningPaths for win (which isn't just for provisioning it …
abe-winter Jan 1, 2025
0984c6e
return minInterval in nil GetConfig case
abe-winter Jan 1, 2025
9c09ede
nil updateInfo is what is breaking download, aha
abe-winter Jan 1, 2025
fbfc8e2
try hardcoding win path
abe-winter Jan 1, 2025
9415af1
correct sha, correct downloadfile control flow
abe-winter Jan 2, 2025
914166e
rdk installing from network
abe-winter Jan 2, 2025
19db1cf
nonworking service install, notes for fixing
abe-winter Jan 2, 2025
f21dbb2
service stub
abe-winter Jan 2, 2025
17bbf00
windows service working
abe-winter Jan 2, 2025
4cd3f44
rm static viam-server info on windows, working with pinURL
abe-winter Jan 6, 2025
6534c3e
service working (move up goroutine start)
abe-winter Jan 6, 2025
1a29820
working windows install script
abe-winter Jan 6, 2025
4a98019
use vars in batch script
abe-winter Jan 6, 2025
d637d1d
skip healthcheck error
abe-winter Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ bin/viam-agent-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go subsyste
go build -o $@ -trimpath -tags $(TAGS) -ldflags $(LDFLAGS) ./cmd/viam-agent/main.go
test "$(PATH_VERSION)" != "custom" && cp $@ bin/viam-agent-stable-$(LINUX_ARCH) || true

windows: bin/viam-agent.exe

bin/viam-agent.exe:
GOOS=windows GOARCH=amd64 go build -o $@ -trimpath -tags $(TAGS) -ldflags $(LDFLAGS) ./cmd/viam-agent
file $@
du -hc $@

.PHONY: clean
clean:
rm -rf bin/
Expand Down
13 changes: 13 additions & 0 deletions agent.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@echo off
:: installer for agent on windows

set root=\opt\viam
set fname=viam-agent-windows-amd64-alpha-1-17bbf00.exe
mkdir %root%\cache
mkdir %root%\bin
curl https://storage.googleapis.com/packages.viam.com/temp/%fname% -o %root%\cache\%fname%
:: netsh %root%\cache\%fname%
del %root%\bin\viam-agent.exe
mklink %root%\bin\viam-agent.exe %root%\cache\%fname%
sc create viam-agent binpath= c:%root%\bin\viam-agent.exe start= auto
sc start viam-agent
118 changes: 27 additions & 91 deletions cmd/viam-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import (
"bytes"
"context"
"fmt"
"io/fs"
"os"
"os/exec"
"os/signal"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
Expand All @@ -19,12 +18,10 @@ import (
"github.com/nightlyone/lockfile"
"github.com/pkg/errors"
"github.com/viamrobotics/agent"
"github.com/viamrobotics/agent/subsystems/provisioning"
_ "github.com/viamrobotics/agent/subsystems/syscfg"
"github.com/viamrobotics/agent/subsystems/viamagent"
"github.com/viamrobotics/agent/subsystems/viamserver"
autils "github.com/viamrobotics/agent/utils"
"go.viam.com/rdk/logging"
"go.viam.com/utils"
)

var (
Expand All @@ -34,26 +31,28 @@ var (
globalLogger = logging.NewLogger("viam-agent")
)

//nolint:lll
type agentOpts struct {
Config string `default:"/etc/viam.json" description:"Path to config file" long:"config" short:"c"`
ProvisioningConfig string `default:"/etc/viam-provisioning.json" description:"Path to provisioning (customization) config file" long:"provisioning" short:"p"`
Debug bool `description:"Enable debug logging (agent only)" env:"VIAM_AGENT_DEBUG" long:"debug" short:"d"`
Fast bool `description:"Enable fast start mode" env:"VIAM_AGENT_FAST_START" long:"fast" short:"f"`
Help bool `description:"Show this help message" long:"help" short:"h"`
Version bool `description:"Show version" long:"version" short:"v"`
Install bool `description:"Install systemd service" long:"install"`
DevMode bool `description:"Allow non-root and non-service" env:"VIAM_AGENT_DEVMODE" long:"dev-mode"`
}

//nolint:gocognit
func main() {
func commonMain() {
ctx, cancel := setupExitSignalHandling()

defer func() {
cancel()
activeBackgroundWorkers.Wait()
}()

//nolint:lll
var opts struct {
Config string `default:"/etc/viam.json" description:"Path to config file" long:"config" short:"c"`
ProvisioningConfig string `default:"/etc/viam-provisioning.json" description:"Path to provisioning (customization) config file" long:"provisioning" short:"p"`
Debug bool `description:"Enable debug logging (agent only)" env:"VIAM_AGENT_DEBUG" long:"debug" short:"d"`
Fast bool `description:"Enable fast start mode" env:"VIAM_AGENT_FAST_START" long:"fast" short:"f"`
Help bool `description:"Show this help message" long:"help" short:"h"`
Version bool `description:"Show version" long:"version" short:"v"`
Install bool `description:"Install systemd service" long:"install"`
DevMode bool `description:"Allow non-root and non-service" env:"VIAM_AGENT_DEVMODE" long:"dev-mode"`
}
var opts agentOpts

parser := flags.NewParser(&opts, flags.IgnoreUnknown)
parser.Usage = "runs as a background service and manages updates and the process lifecycle for viam-server."
Expand Down Expand Up @@ -82,7 +81,7 @@ func main() {
// need to be root to go any further than this
curUser, err := user.Current()
exitIfError(err)
if curUser.Uid != "0" && !opts.DevMode {
if runtime.GOOS != "windows" && curUser.Uid != "0" && !opts.DevMode {
//nolint:forbidigo
fmt.Printf("viam-agent must be run as root (uid 0), but current user is %s (uid %s)\n", curUser.Username, curUser.Uid)
return
Expand All @@ -93,7 +92,7 @@ func main() {
return
}

if !opts.DevMode {
if !opts.DevMode && runtime.GOOS != "windows" {
// confirm that we're running from a proper install
if !strings.HasPrefix(os.Args[0], agent.ViamDirs["viam"]) {
//nolint:forbidigo
Expand All @@ -117,63 +116,16 @@ func main() {
}
}()

// pass the provisioning path arg to the subsystem
absProvConfigPath, err := filepath.Abs(opts.ProvisioningConfig)
exitIfError(err)
provisioning.ProvisioningConfigFilePath = absProvConfigPath
globalLogger.Infof("provisioning config file path: %s", absProvConfigPath)

// tie the manager config to the viam-server config
absConfigPath, err := filepath.Abs(opts.Config)
exitIfError(err)
viamserver.ConfigFilePath = absConfigPath
provisioning.AppConfigFilePath = absConfigPath
globalLogger.Infof("config file path: %s", absConfigPath)
absConfigPath := setupProvisioningPaths(opts)

// main manager structure
manager, err := agent.NewManager(ctx, globalLogger)
exitIfError(err)

err = manager.LoadConfig(absConfigPath)
loadConfigErr := manager.LoadConfig(absConfigPath)
//nolint:nestif
if err != nil {
// If the local /etc/viam.json config is corrupted, invalid, or missing (due to a new install), we can get stuck here.
// Rename the file (if it exists) and wait to provision a new one.
if !errors.Is(err, fs.ErrNotExist) {
if err := os.Rename(absConfigPath, absConfigPath+".old"); err != nil {
// if we can't rename the file, we're up a creek, and it's fatal
globalLogger.Error(errors.Wrapf(err, "removing invalid config file %s", absConfigPath))
globalLogger.Error("unable to continue with provisioning, exiting")
manager.CloseAll()
return
}
}

// We manually start the provisioning service to allow the user to update it and wait.
// The user may be updating it soon, so better to loop quietly than to exit and let systemd keep restarting infinitely.
globalLogger.Infof("main config file %s missing or corrupt, entering provisioning mode", absConfigPath)

if err := manager.StartSubsystem(ctx, provisioning.SubsysName); err != nil {
if errors.Is(err, agent.ErrSubsystemDisabled) {
globalLogger.Warn("provisioning subsystem disabled, please manually update /etc/viam.json and connect to internet")
} else {
globalLogger.Error(errors.Wrapf(err,
"could not start provisioning subsystem, please manually update /etc/viam.json and connect to internet"))
manager.CloseAll()
return
}
}

for {
globalLogger.Warn("waiting for user provisioning")
if !utils.SelectContextOrWait(ctx, time.Second*10) {
manager.CloseAll()
return
}
if err := manager.LoadConfig(absConfigPath); err == nil {
break
}
}
if loadConfigErr != nil {
runPlatformProvisioning(ctx, manager, loadConfigErr, absConfigPath)
}
netAppender, err := manager.CreateNetAppender()
if err != nil {
Expand All @@ -199,23 +151,7 @@ func main() {
// wait to be online
timeoutCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
for {
cmd := exec.CommandContext(timeoutCtx, "systemctl", "is-active", "network-online.target")
_, err := cmd.CombinedOutput()

if err == nil {
break
}

if e := (&exec.ExitError{}); !errors.As(err, &e) {
// if it's not an ExitError, that means it didn't even start, so bail out
globalLogger.Error(errors.Wrap(err, "running 'systemctl is-active network-online.target'"))
break
}
if !utils.SelectContextOrWait(timeoutCtx, time.Second) {
break
}
}
autils.WaitOnline(globalLogger, timeoutCtx)

// Check for self-update and restart if needed.
needRestart, err := manager.SelfUpdate(ctx)
Expand Down Expand Up @@ -268,12 +204,11 @@ func setupExitSignalHandling() (context.Context, func()) {
// this will eventually be handled elsewhere as a restart, not exit
case syscall.SIGHUP:

// ignore SIGURG entirely, it's used for real-time scheduling notifications
case syscall.SIGURG:

// log everything else
default:
globalLogger.Debugw("received unknown signal", "signal", sig)
if !ignoredSignal(sig) {
globalLogger.Debugw("received unknown signal", "signal", sig)
}
}
}
}()
Expand All @@ -282,6 +217,7 @@ func setupExitSignalHandling() (context.Context, func()) {
return ctx, cancel
}

// helper to log.Fatal if error is non-nil.
func exitIfError(err error) {
if err != nil {
globalLogger.Fatal(err)
Expand Down
59 changes: 59 additions & 0 deletions cmd/viam-agent/main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"fmt"

"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/debug"
"golang.org/x/sys/windows/svc/eventlog"
)

var elog debug.Log

const serviceName = "viam-agent"

type agentService struct{}

// control loop for a windows service
func (*agentService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
for {
c := <-r
if c.Cmd == svc.Stop || c.Cmd == svc.Shutdown {
// testOutput := strings.Join(args, "-")
// testOutput += fmt.Sprintf("-%d", c.Context)
// elog.Info(1, testOutput)
break
} else {
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
}
changes <- svc.Status{State: svc.StopPending}
return
}

func main() {
if inService, err := svc.IsWindowsService(); err != nil {
panic(err)
} else if !inService {
println("no service detected -- running as normal process")
commonMain()
return
}

var err error
elog, err = eventlog.Open(serviceName)
if err != nil {
return
}
defer elog.Close()

elog.Info(1, fmt.Sprintf("starting %s service", serviceName))
go commonMain()
err = svc.Run(serviceName, &agentService{})
if err != nil {
elog.Error(1, fmt.Sprintf("%s service failed: %v", serviceName, err))
return
}
elog.Info(1, fmt.Sprintf("%s service stopped", serviceName))
}
88 changes: 88 additions & 0 deletions cmd/viam-agent/subsystems_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"io/fs"
"os"
"path/filepath"
"syscall"
"time"

"github.com/pkg/errors"
"github.com/viamrobotics/agent"
"github.com/viamrobotics/agent/subsystems/provisioning"

// register-only.
_ "github.com/viamrobotics/agent/subsystems/syscfg"
"github.com/viamrobotics/agent/subsystems/viamserver"
"go.viam.com/utils"
)

func main() {
commonMain()
}

// platform-specific provisioning logic.
func runPlatformProvisioning(ctx context.Context, manager *agent.Manager, loadConfigErr error, absConfigPath string) {
// If the local /etc/viam.json config is corrupted, invalid, or missing (due to a new install), we can get stuck here.
// Rename the file (if it exists) and wait to provision a new one.
if !errors.Is(loadConfigErr, fs.ErrNotExist) {
if err := os.Rename(absConfigPath, absConfigPath+".old"); err != nil {
// if we can't rename the file, we're up a creek, and it's fatal
globalLogger.Error(errors.Wrapf(err, "removing invalid config file %s", absConfigPath))
globalLogger.Error("unable to continue with provisioning, exiting")
manager.CloseAll()
return
}
}

// We manually start the provisioning service to allow the user to update it and wait.
// The user may be updating it soon, so better to loop quietly than to exit and let systemd keep restarting infinitely.
globalLogger.Infof("main config file %s missing or corrupt, entering provisioning mode", absConfigPath)

if err := manager.StartSubsystem(ctx, provisioning.SubsysName); err != nil {
if errors.Is(err, agent.ErrSubsystemDisabled) {
globalLogger.Warn("provisioning subsystem disabled, please manually update /etc/viam.json and connect to internet")
} else {
globalLogger.Error(errors.Wrapf(err,
"could not start provisioning subsystem, please manually update /etc/viam.json and connect to internet"))
manager.CloseAll()
return
}
}

for {
globalLogger.Warn("waiting for user provisioning")
if !utils.SelectContextOrWait(ctx, time.Second*10) {
manager.CloseAll()
return
}
if err := manager.LoadConfig(absConfigPath); err == nil {
break
}
}
}

// platform-specific path setup.
func setupProvisioningPaths(opts agentOpts) string {
// pass the provisioning path arg to the subsystem
absProvConfigPath, err := filepath.Abs(opts.ProvisioningConfig)
exitIfError(err)
provisioning.ProvisioningConfigFilePath = absProvConfigPath
globalLogger.Infof("provisioning config file path: %s", absProvConfigPath)

// tie the manager config to the viam-server config
absConfigPath, err := filepath.Abs(opts.Config)
exitIfError(err)
viamserver.ConfigFilePath = absConfigPath
provisioning.AppConfigFilePath = absConfigPath
globalLogger.Infof("config file path: %s", absConfigPath)

return absConfigPath
}

// return true if this error is safe to ignore on this platform.
func ignoredSignal(sig os.Signal) bool {
// ignore SIGURG entirely, it's used for real-time scheduling notifications
return sig == syscall.SIGURG
}
Loading
Loading