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

support agent on windows #72

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ else
PATH_VERSION = v$(TAG_VERSION)
endif

LDFLAGS = "-s -w -X 'github.com/viamrobotics/agent/utils.Version=${TAG_VERSION}' -X 'github.com/viamrobotics/agent/utils.GitRevision=${GIT_REVISION}'"
LDFLAGS = "-s -w -X 'github.com/viamrobotics/agent.Version=${TAG_VERSION}' -X 'github.com/viamrobotics/agent.GitRevision=${GIT_REVISION}'"
TAGS = osusergo,netgo


Expand All @@ -33,16 +33,22 @@ arm64:
amd64:
make GOARCH=amd64

bin/viam-agent-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go *.service Makefile
bin/viam-agent-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go subsystems/viamagent/*.service Makefile
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/

bin/golangci-lint: Makefile
GOBIN=`pwd`/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5
GOBIN=`pwd`/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3

.PHONY: lint
lint: bin/golangci-lint
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>
102 changes: 23 additions & 79 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/networking"
_ "github.com/viamrobotics/agent/subsystems/syscfg"
"github.com/viamrobotics/agent/utils"
"go.uber.org/zap"
"go.viam.com/rdk/logging"
goutils "go.viam.com/utils"
)

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

//nolint:lll
type agentOpts struct {
Config string `default:"/etc/viam.json" description:"Path to machine credentials file" long:"config" short:"c"`
DefaultsConfig string `default:"/etc/viam-defaults.json" description:"Path to manufacturer defaults file" long:"defaults"`
Debug bool `description:"Enable debug logging (agent only)" env:"VIAM_AGENT_DEBUG" long:"debug" short:"d"`
UpdateFirst bool `description:"Update versions before starting" env:"VIAM_AGENT_WAIT_FOR_UPDATE" long:"wait" short:"w"`
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 machine credentials file" long:"config" short:"c"`
DefaultsConfig string `default:"/etc/viam-defaults.json" description:"Path to manufacturer defaults file" long:"defaults"`
Debug bool `description:"Enable debug logging (agent only)" env:"VIAM_AGENT_DEBUG" long:"debug" short:"d"`
UpdateFirst bool `description:"Update versions before starting" env:"VIAM_AGENT_WAIT_FOR_UPDATE" long:"wait" short:"w"`
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 @@ -87,7 +85,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 @@ -98,7 +96,7 @@ func main() {
return
}

if !opts.DevMode {
if runtime.GOOS != "windows" && !opts.DevMode {
// confirm that we're running from a proper install
if !strings.HasPrefix(os.Args[0], utils.ViamDirs["viam"]) {
//nolint:forbidigo
Expand Down Expand Up @@ -141,48 +139,10 @@ func main() {
err = manager.LoadAppConfig()
//nolint:nestif
if err != nil {
if cfg.AdvancedSettings.DisableNetworkConfiguration {
globalLogger.Errorf("Cannot read %s and network configuration is disabled. Please correct and restart viam-agent.",
utils.AppConfigFilePath)
manager.CloseAll()
return
}

// 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) {
globalLogger.Error(errors.Wrapf(err, "reading %s", utils.AppConfigFilePath))
globalLogger.Warn("renaming %s to %s.old", utils.AppConfigFilePath, utils.AppConfigFilePath)
if err := os.Rename(utils.AppConfigFilePath, utils.AppConfigFilePath+".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", utils.AppConfigFilePath))
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("machine credentials file %s missing or corrupt, entering provisioning mode", utils.AppConfigFilePath)

if err := manager.StartSubsystem(ctx, networking.SubsysName); err != nil {
globalLogger.Error(errors.Wrapf(err, "could not start networking subsystem, "+
"please manually update /etc/viam.json and connect to internet"))
if !runPlatformProvisioning(ctx, cfg, manager, err) {
manager.CloseAll()
return
}

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

// valid viam.json from this point forward
Expand All @@ -200,23 +160,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 !goutils.SelectContextOrWait(timeoutCtx, time.Second) {
break
}
}
waitOnline(globalLogger, timeoutCtx)

// Check for self-update and restart if needed.
needRestart, err := manager.SelfUpdate(ctx)
Expand Down Expand Up @@ -269,12 +213,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 @@ -283,6 +226,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.WithOptions(zap.AddCallerSkip(1)).Fatal(err)
Expand Down
Loading
Loading