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 50 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
28a28d7
todo: graceful stop
abe-winter Jan 10, 2025
0991304
top-level firewall rule seems to be working
abe-winter Jan 10, 2025
7737497
alpha-3
abe-winter Jan 10, 2025
1471893
download as .exe on win
abe-winter Jan 10, 2025
cde5d1f
firewall rule for downloaded viam-server
abe-winter Jan 10, 2025
6af4e0f
use a globalmanager (#57)
ale7714 Jan 11, 2025
2b02f75
alpha 4
abe-winter Jan 11, 2025
540d5e6
skip systemd postinstall on win
abe-winter Jan 12, 2025
07b1cc5
restart service on win (in place of systemd stuff :tm:)
abe-winter Jan 12, 2025
55d2459
sigh actually start them
abe-winter Jan 12, 2025
25840a6
try closeall
abe-winter Jan 12, 2025
cdf3fc8
globalcancel
abe-winter Jan 12, 2025
f68e150
sigh double restart
abe-winter Jan 12, 2025
935c778
alpha-9 in agent.bat
abe-winter Jan 12, 2025
c0ad514
remove globals
abe-winter Jan 12, 2025
4725f4c
remove annoying log
abe-winter Jan 12, 2025
24b60ac
bump agent.bat
abe-winter Jan 12, 2025
06484d5
request restart
abe-winter Jan 12, 2025
3bf9a13
back to globalcancel
abe-winter Jan 12, 2025
8e38aa7
factor out KillTree, use it in viamServer.Stop()
abe-winter Jan 13, 2025
837af6a
logger, sleep, exit
abe-winter Jan 13, 2025
6dece14
killtree so viam-server doesn't dangle
abe-winter Jan 13, 2025
4e173ff
alpha-16, service restart
abe-winter Jan 13, 2025
9fdfb4e
Add msi (#58)
ale7714 Jan 21, 2025
1308151
missing space in log
abe-winter Jan 22, 2025
aa268bc
downgrade 'updateInfo nil' to warning (it's still busy, but will go a…
abe-winter Jan 22, 2025
8985f7e
revert prev, always allow restart on win
abe-winter Jan 22, 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
46 changes: 46 additions & 0 deletions .github/workflows/build-msi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Build MSI

on:
workflow_dispatch:
inputs:
# since this is being triggered manually on branch but we should use our regular git tags when we're back on the normal flow
msi_version:
description: "MSI package version (e.g., 1.0.0)"
required: true

jobs:
build-msi:
runs-on: windows-2019

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23.1'

- name: Install Make
run: choco install make --yes

- name: Install WiX CLI
run: dotnet tool install --global wix

- name: Install WiX Extensions
run: |
wix extension add -g WixToolset.Firewall.wixext/5.0.2
wix extension add -g WixToolset.Util.wixext/5.0.2

- name: Build Go binary
run: make windows

- name: Build MSI
run: |
wix build agent.wxs -define GoBinDir="${{ github.workspace }}\bin" -define MSIProductVersion="${{ github.event.inputs.msi_version }}" -ext WixToolset.Util.wixext -ext WixToolset.Firewall.wixext -o agent-${{ github.event.inputs.msi_version }}.msi

- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{ github.event.inputs.msi_version }}.msi
path: agent-${{ github.event.inputs.msi_version }}.msi
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ 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

.PHONY: windows
windows: bin/viam-agent.exe

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

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

set root=\opt\viam
set fname=viam-agent-windows-amd64-alpha-16-6dece14.exe
mkdir %root%\cache
mkdir %root%\bin
curl https://storage.googleapis.com/packages.viam.com/temp/%fname% -o %root%\cache\%fname%
netsh advfirewall firewall add rule name="%fname%" dir=in action=allow program="c:\%root%\cache\%fname%" enable=yes
del %root%\bin\viam-agent.exe
mklink %root%\bin\viam-agent.exe %root%\cache\%fname%
:: todo: restart on error
sc create viam-agent binpath= c:%root%\bin\viam-agent.exe start= auto
sc failure viam-agent reset= 0 actions= restart/30000/restart/30000/restart/30000
sc failureflag viam-agent 1
sc start viam-agent
28 changes: 28 additions & 0 deletions agent.wxs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
xmlns:fire="http://wixtoolset.org/schemas/v4/wxs/firewall">
<Package Name="viam-agent" Manufacturer="viam" Version="$(var.MSIProductVersion)" UpgradeCode="d3b5bca3-4bec-46cb-a063-ca6315de7de4" Language="1033" Scope="perMachine">
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

<StandardDirectory Id="TARGETDIR">
<Directory Id="CustomInstallPath" Name="opt">
<Directory Id="ViamFolder" Name="viam">
<Directory Id="INSTALLFOLDER" Name="bin">
<Component Id="MainServiceComponent" Guid="d478ceba-537c-4e49-9262-bf24ccfe4909">
<File Id="ViamExe" Source="$(var.GoBinDir)\viam-agent.exe" KeyPath="yes" />
<ServiceInstall Id="InstallAgentervice" Name="viam-agent" DisplayName="viam-agent Service" Description="viam-agent Windows service" Start="auto" Type="ownProcess" ErrorControl="normal" Account="LocalSystem" Interactive="yes" />
<ServiceControl Id="ControlAgentService" Name="viam-agent" Start="install" Stop="both" Remove="uninstall" Wait="yes" />
<fire:FirewallException Id="AllowAllTCP" Name="viam-agent" Profile="all" Protocol="tcp" Scope="any" />
<fire:FirewallException Id="AllowAllUDP" Name="viam-agent" Profile="all" Protocol="tcp" Scope="any" />
</Component>
</Directory>
</Directory>
</Directory>
</StandardDirectory>

<Feature Id="MainFeature" Title="Main Feature" Level="1">
<ComponentRef Id="MainServiceComponent" />
</Feature>
</Package>
</Wix>
119 changes: 28 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,29 @@ 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()
agent.GlobalCancel = cancel

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 +82,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 +93,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 +117,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 +152,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 +205,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 +218,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
63 changes: 63 additions & 0 deletions cmd/viam-agent/main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"fmt"

"github.com/viamrobotics/agent/utils"
"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 {
elog.Info(1, fmt.Sprintf("%s service stopping", serviceName))
if err := utils.KillTree(-1); err != nil {
elog.Error(1, fmt.Sprintf("error killing subtree %s", err))
}
elog.Info(1, "taskkilled")
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
}
// todo(windows): gracefully stop. without this, RDK stays running in the background.
elog.Info(1, fmt.Sprintf("%s service stopped", serviceName))
}
Loading