diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9be2c20..f0aee82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,3 +63,54 @@ jobs: args: release --snapshot --skip-publish --clean env: PRIVATE_ACCESS_TOKEN: placeholder + - name: Upload Windows Artifacts + uses: actions/upload-artifact@v3 + with: + name: windows_exporter_binaries + path: | + dist/nvidia_gpu_exporter_windows_*/ + dist/checksums.txt + build-msi: + needs: build + runs-on: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@v3.5.3 + + - name: Setup Go + uses: actions/setup-go@v4.1.0 + with: + go-version-file: go.mod + + - name: Download Windows Artifacts + uses: actions/download-artifact@v3 + with: + name: windows_exporter_binaries + path: .\dist\ + + - name: Build Release Artifacts + if: startsWith(github.ref, 'refs/tags/') + run: | + $ErrorActionPreference = "Stop" + $TagName = $env:GITHUB_REF -replace 'refs/tags/', '' + # The MSI version is not semver compliant, so just take the numerical parts + $MSIVersion = $TagName -replace '^v?([0-9\.]+).*$','$1' + Get-ChildItem -Path .\dist\nvidia_gpu_exporter_windows_* | ForEach-Object { + $Arch = ($_ -split "nvidia_gpu_exporter_windows_")[1] + Write-Verbose "Building windows_exporter ${MSIVersion} msi for $Arch" + .\install\build.ps1 -PathToExecutable ".\dist\nvidia_gpu_exporter_windows_${Arch}/nvidia_gpu_exporter.exe" -Version "${MSIVersion}" -Arch "$Arch" + Move-Item ".\install\Output\nvidia_gpu_exporter_${MSIVersion}_${Arch}.msi" .\dist\ + } + + - name: Display structure of downloaded files + run: ls -R .\dist\ + + - name: Generate checksums for msi + run: | + "`n" | Add-Content -Path ".\dist\checksums.txt" -NoNewline -Encoding UTF8 + Get-FileHash -Algorithm SHA256 -Path .\dist\*.deb | ForEach-Object { + $filename = Split-Path -Leaf $_.Path + $hash = $_.Hash + Write-Output "$hash $filename" | Add-Content -Path ".\dist\checksums.txt" -Encoding UTF8 + } + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6521ecc..07f38ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: push: tags: - "v*.*.*" + +permissions: + contents: write + jobs: release: runs-on: ubuntu-22.04 @@ -32,3 +36,60 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRIVATE_ACCESS_TOKEN: ${{ secrets.PRIVATE_ACCESS_TOKEN }} + - name: Upload Windows Artifacts + uses: actions/upload-artifact@v3 + with: + name: windows_exporter_binaries + path: | + dist/nvidia_gpu_exporter_windows_*/ + dist/checksums.txt + release-msi: + needs: release + runs-on: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@v3.5.3 + + - name: Setup Go + uses: actions/setup-go@v4.1.0 + with: + go-version-file: go.mod + + - name: Download Windows Artifacts + uses: actions/download-artifact@v3 + with: + name: windows_exporter_binaries + path: .\dist\ + + - name: Build Release Artifacts + if: startsWith(github.ref, 'refs/tags/') + run: | + $ErrorActionPreference = "Stop" + $TagName = $env:GITHUB_REF -replace 'refs/tags/', '' + # The MSI version is not semver compliant, so just take the numerical parts + $MSIVersion = $TagName -replace '^v?([0-9\.]+).*$','$1' + Get-ChildItem -Path .\dist\nvidia_gpu_exporter_windows_* | ForEach-Object { + $Arch = ($_ -split "nvidia_gpu_exporter_windows_")[1] + Write-Verbose "Building windows_exporter ${MSIVersion} msi for $Arch" + .\install\build.ps1 -PathToExecutable ".\dist\nvidia_gpu_exporter_windows_${Arch}/nvidia_gpu_exporter.exe" -Version "${MSIVersion}" -Arch "$Arch" + Move-Item ".\install\Output\nvidia_gpu_exporter_${MSIVersion}_${Arch}.msi" .\dist\ + } + + - name: Display structure of downloaded files + run: ls -R .\dist\ + + - name: Generate checksums for msi + run: | + Get-FileHash -Algorithm SHA256 -Path .\dist\*.msi | ForEach-Object { + $filename = Split-Path -Leaf $_.Path + $hash = $_.Hash + Write-Output "$hash $filename" | Add-Content -Path ".\dist\checksums.txt" -Encoding UTF8 + } + + - name: Release + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $TagName = $env:GITHUB_REF -replace 'refs/tags/', '' + Get-ChildItem -Path .\dist\* -Include @('nvidia_gpu_exporter*.msi', 'checksums.txt') | Foreach-Object {gh release upload --clobber $TagName $_} \ No newline at end of file diff --git a/cmd/nvidia_gpu_exporter/main.go b/cmd/nvidia_gpu_exporter/main.go index 071bb67..eca25e2 100644 --- a/cmd/nvidia_gpu_exporter/main.go +++ b/cmd/nvidia_gpu_exporter/main.go @@ -6,6 +6,8 @@ import ( "net" "net/http" "os" + "os/signal" + "syscall" "github.com/alecthomas/kingpin/v2" "github.com/coreos/go-systemd/v22/activation" @@ -20,6 +22,7 @@ import ( webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" "github.com/utkuozdemir/nvidia_gpu_exporter/internal/exporter" + "github.com/utkuozdemir/nvidia_gpu_exporter/internal/initiate" ) const ( @@ -95,11 +98,25 @@ func main() { IdleTimeout: *idleTimeout, } - if err := listenAndServe(srv, webConfig, *network, logger); err != nil { - _ = level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) + go func() { + if err := listenAndServe(srv, webConfig, *network, logger); err != nil { + _ = level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) - os.Exit(1) + os.Exit(1) + } + }() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT) + + select { + case <-initiate.StopCh: + _ = level.Info(logger).Log("msg", "Shutting down from service") + case <-sig: + _ = level.Info(logger).Log("msg", "Shutting down from signal") } + + os.Exit(0) } type RootHandler struct { diff --git a/go.mod b/go.mod index c809054..be1115e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/prometheus/exporter-toolkit v0.10.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 + golang.org/x/sys v0.11.0 ) require ( @@ -33,7 +34,6 @@ require ( golang.org/x/net v0.10.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/install/build.ps1 b/install/build.ps1 new file mode 100644 index 0000000..7cd269b --- /dev/null +++ b/install/build.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +Param ( + [Parameter(Mandatory = $true)] + [String] $PathToExecutable, + [Parameter(Mandatory = $true)] + [String] $Version, + [Parameter(Mandatory = $false)] + [ValidateSet("amd64", "386", "amd64_v1")] + [String] $Arch = "amd64" +) +$ErrorActionPreference = "Stop" + +# Get absolute path to executable before switching directories +$PathToExecutable = Resolve-Path $PathToExecutable +# Set working dir to this directory, reset previous on exit +Push-Location $PSScriptRoot +Trap { + # Reset working dir on error + Pop-Location +} + +if ($PSVersionTable.PSVersion.Major -lt 5) { + Write-Error "Powershell version 5 required" + exit 1 +} + +$wc = New-Object System.Net.WebClient +function Get-FileIfNotExists { + Param ( + $Url, + $Destination + ) + if (-not (Test-Path $Destination)) { + Write-Verbose "Downloading $Url" + $wc.DownloadFile($Url, $Destination) + } + else { + Write-Verbose "${Destination} already exists. Skipping." + } +} + +$sourceDir = mkdir -Force Source +mkdir -Force Work, Output | Out-Null + +Write-Verbose "Downloading WiX..." +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Get-FileIfNotExists "https://github.com/wixtoolset/wix3/releases/download/wix311rtm/wix311-binaries.zip" "$sourceDir\wix-binaries.zip" +mkdir -Force WiX | Out-Null +Expand-Archive -Path "${sourceDir}\wix-binaries.zip" -DestinationPath WiX -Force + +Copy-Item -Force $PathToExecutable Work/nvidia_gpu_exporter.exe + +Write-Verbose "Creating nvidia_gpu_exporter_${Version}_${Arch}.msi" +$wixArch = @{"amd64" = "x64"; "amd64_v1" = "x64"; "386" = "x86"}[$Arch] +$wixOpts = "-ext WixFirewallExtension -ext WixUtilExtension" +Invoke-Expression "WiX\candle.exe -nologo -arch $wixArch $wixOpts -out Work\nvidia_gpu_exporter.wixobj -dVersion=`"$Version`" nvidia_gpu_exporter.wxs" +Invoke-Expression "WiX\light.exe -nologo -spdb $wixOpts -out `"Output\nvidia_gpu_exporter_${Version}_${Arch}.msi`" Work\nvidia_gpu_exporter.wixobj" + +Write-Verbose "Done!" +Pop-Location diff --git a/install/nvidia_gpu_exporter.wxs b/install/nvidia_gpu_exporter.wxs new file mode 100644 index 0000000..6a61f33 --- /dev/null +++ b/install/nvidia_gpu_exporter.wxs @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + EXTRA_FLAGS + + + + LISTEN_ADDR AND LISTEN_PORT + LISTEN_ADDR AND (NOT LISTEN_PORT) + LISTEN_PORT AND (NOT LISTEN_ADDR) + + + METRICS_PATH + + + REMOTE_ADDR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/internal/initiate/initiate.go b/internal/initiate/initiate.go new file mode 100644 index 0000000..a65ab0b --- /dev/null +++ b/internal/initiate/initiate.go @@ -0,0 +1,9 @@ +// Package initiate This package allows us to initiate Time +// Sensitive components (Like registering the windows service) +// as early as possible in the startup process +package initiate + +// StopCh is used by Windows service. +// +//nolint:gochecknoglobals +var StopCh = make(chan bool) diff --git a/internal/initiate/initiate_windows.go b/internal/initiate/initiate_windows.go new file mode 100644 index 0000000..c1e126a --- /dev/null +++ b/internal/initiate/initiate_windows.go @@ -0,0 +1,65 @@ +//go:build windows +// +build windows + +// Package initiate This package allows us to initiate Time Sensitive components (Like registering the windows service) as early as possible in the startup process +package initiate + +import ( + "fmt" + "os" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "golang.org/x/sys/windows/svc" +) + +const ( + serviceName = "nvidia_gpu_exporter" +) + +var logger = log.NewLogfmtLogger(os.Stdout) + +type windowsExporterService struct { + stopCh chan<- bool +} + +func (s *windowsExporterService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + level.Debug(logger).Log("msg", "Service Stop Received") + s.stopCh <- true + break loop + default: + level.Error(logger).Log("msg", fmt.Sprintf("unexpected control request #%d", c)) + } + } + } + changes <- svc.Status{State: svc.StopPending} + return +} + +func init() { + level.Debug(logger).Log("msg", "Checking if We are a service") + isService, err := svc.IsWindowsService() + if err != nil { + level.Error(logger).Log("msg", err) + } + level.Debug(logger).Log("msg", "Attempting to start exporter service") + if isService { + go func() { + err = svc.Run(serviceName, &windowsExporterService{stopCh: StopCh}) + if err != nil { + level.Error(logger).Log("msg", "Failed to start service: ", "error", err) + } + }() + } +}