mirror of
https://github.com/Xevion/HATray.git
synced 2025-12-06 15:15:16 -06:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a232578a32 | |||
| 57ddab9032 | |||
| d5536423bb | |||
| 5bb9f327dc | |||
| 4bacef7bb9 | |||
| 21463775c0 | |||
| b6a333ae2d | |||
| e252f62f52 | |||
| d650beb2e0 | |||
| b9adf96c39 | |||
| 8048fcd953 | |||
| 7a2b778803 | |||
| 74abe9d2e4 | |||
| 22ad371760 | |||
| 79240db736 | |||
| 8a3864d4ae | |||
| 44c30c2b5f | |||
| a150c24e88 | |||
| e83de79207 | |||
| a35b7e77a3 | |||
| 6f313bbc41 | |||
| f536e3c2f7 | |||
| 821067765e | |||
| 06e036a116 | |||
| 34caa5c5a3 | |||
| 6bb8d4a3cb | |||
| 1f4728cab1 | |||
| 15b0e93feb | |||
| c5caec827f | |||
| 486b425e99 | |||
| fc96753188 | |||
| 6e01443b06 | |||
| 8d0bb4607e | |||
| 9060289297 | |||
| af17dca4e4 | |||
| 80d031e8c0 | |||
| 5778d3ece9 | |||
| a41b120473 | |||
| 8a96cb07d4 | |||
| 133c95cd36 | |||
| 5b17297837 | |||
| a6a774aac7 | |||
| 0aba8b4bfa | |||
| 2a23ddc9a5 | |||
| 151fe6eb1c | |||
| cb29ad2c22 | |||
| 0a51119e4c |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
117
.github/workflows/build.yml
vendored
Normal file
117
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: "3.x"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev
|
||||
|
||||
- name: Build Linux Binary
|
||||
run: task build
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=$(task version --silent)" >> $GITHUB_OUTPUT
|
||||
echo "BINARY_NAME=HATray-linux-amd64-$(task version --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename Linux Binary with Version
|
||||
run: mv bin/HATray ${{ steps.get_version.outputs.BINARY_NAME }}
|
||||
|
||||
- name: Upload Linux Binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.get_version.outputs.BINARY_NAME }}
|
||||
path: ${{ steps.get_version.outputs.BINARY_NAME }}
|
||||
if-no-files-found: error
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: "3.x"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Windows Binary
|
||||
run: task build
|
||||
|
||||
- name: Get Version
|
||||
id: get_version_win
|
||||
shell: pwsh
|
||||
run: |
|
||||
echo "VERSION=$(task version --silent)" >> $env:GITHUB_OUTPUT
|
||||
echo "BINARY_NAME=HATray-windows-amd64-$(task version --silent)" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Rename Windows Binary with Version
|
||||
shell: pwsh
|
||||
run: Rename-Item -Path bin/HATray.exe -NewName "${{ steps.get_version_win.outputs.BINARY_NAME }}.exe"
|
||||
|
||||
- name: Upload Windows Binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.get_version_win.outputs.BINARY_NAME }}.exe
|
||||
path: bin/${{ steps.get_version_win.outputs.BINARY_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Set up .NET for WiX
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
run: dotnet tool install --global wix
|
||||
|
||||
- name: Add WiX Extensions
|
||||
run: |
|
||||
wix extension add WixToolset.Util.wixext
|
||||
wix extension add WixToolset.UI.wixext
|
||||
|
||||
- name: Build MSI
|
||||
run: task package
|
||||
|
||||
- name: Rename MSI with Version
|
||||
shell: pwsh
|
||||
run: Rename-Item -Path bin/HATray.msi -NewName "${{ steps.get_version_win.outputs.BINARY_NAME }}.msi"
|
||||
|
||||
- name: Upload MSI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.get_version_win.outputs.BINARY_NAME }}.msi
|
||||
path: bin/${{ steps.get_version_win.outputs.BINARY_NAME }}.msi
|
||||
if-no-files-found: error
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,5 +2,5 @@
|
||||
*.log
|
||||
.env
|
||||
.vscode
|
||||
dist
|
||||
bin
|
||||
.wix
|
||||
78
README.md
78
README.md
@@ -8,24 +8,74 @@ A simple tray utility for Home Assistant
|
||||
- Easy to install, runs as a background service
|
||||
- Cross-platform support (Windows, Linux)
|
||||
|
||||
### Notes
|
||||
## Design
|
||||
|
||||
The application follows a layered architecture:
|
||||
|
||||
- **Command Layer**: A barebones entrypoint for the application, initializing the logger & emitting basic diagnostics.
|
||||
- **Service Layer**: OS-dependent implementation that communicates with the App layer. It contains the true entrypoint for the application.
|
||||
- **App Layer**: Generic, cross-platform implementation that exposes simple methods for controlling the application state
|
||||
- **Pause**: Disconnect from the server and cease any background tasks.
|
||||
- Once paused, no logging occurs from the App layer, no connections are made, and no background tasks should run.
|
||||
- **Resume**: Reads configuration files, connects to the server and initiates background tasks.
|
||||
- Once running, the App layer should be connected (or attempting to reconnect) to the server.
|
||||
- If an error occurs while attempting to resume or while running, the app layer will become paused.
|
||||
- **Reload**: If not paused, pause the application, re-read configuration files, then resume. This is just a macro for pause + resume.
|
||||
|
||||
### Windows Service Layer
|
||||
|
||||
The Windows service layer implements a pseudo-Windows service that mimics the behavior of a real service, but does not actually run as a service.
|
||||
|
||||
- This is required because tray icons are not supported by Windows services, as they run in the system space, and cannot interact with the user space.
|
||||
|
||||
Currently, I only have a MSI installer developed for Windows. I'm considering creating a specialized CLI-based installation method for Windows, one that will match the Linux experience, but that is yet to be completed.
|
||||
|
||||
### Linux Service Layer
|
||||
|
||||
The Linux service layer implements a systemd service 'notify' type service.
|
||||
|
||||
- Note that we don't take advantage of most modern systemd features, such as `notify-reload`, `ReloadSignal=SIGHUP`, and so on.
|
||||
- This is because I use WSL2 as my primary development environment, which only has systemd v249.
|
||||
- It uses the go-systemd package to interface with systemd, enabling proper handling of startup and reload signals.
|
||||
- The unit file is configured to send `SIGHUP` signals on reload, and will respond to `SIGHUP` (reload), and `SIGTERM` (stop). It also provides on-startup status updates, a watchdog mechanism, and a heartbeat mechanism (that updates the service status regularly).
|
||||
|
||||
Currently, the Linux service layer is only installed via the `task service` command.
|
||||
|
||||
Ideally, I plan to provide at least two different methods for installation:
|
||||
|
||||
- A one-command remote bash script that will download the binary, install the systemd unit file, and start the service.
|
||||
- An internal CLI-based method that provides customized systemd unit file generation & simple management commands.
|
||||
|
||||
### Feature Targets
|
||||
|
||||
- [ ] Easy MSI Installer/Uninstaller
|
||||
- [ ] Structured JSON Logging, Configurable
|
||||
- [ ] One-command/click Install, Background Service
|
||||
- [ ] Tray Icon, Tray Menu
|
||||
- [ ] Cross-platform Support (Linux, Windows)
|
||||
- [ ] Easy Development Testing
|
||||
- [ ] Go Tests
|
||||
- [ ] Conventional Commits
|
||||
- [ ] GitHub Actions
|
||||
- [ ] MSI Packages
|
||||
- [ ] Testing, Linting, Formatting
|
||||
- [x] Cross-platform Background Service (Linux, Windows)
|
||||
- [ ] Windows
|
||||
- [x] MSI-based Installer
|
||||
- [ ] CLI-based Installer
|
||||
- [ ] Winget Package Publishing
|
||||
- [ ] Linux
|
||||
- [x] `systemd` Service Implementation
|
||||
- [ ] CLI-based Installer
|
||||
- [ ] Script-based Installer
|
||||
- [ ] `systemd` Unit File Templating/Generation
|
||||
- [ ] Smart `journalctl` logging bypass
|
||||
- Application
|
||||
- [ ] TOML Configuration
|
||||
- [x] Health Checks
|
||||
- [x] Tray Icon
|
||||
- [ ] Tray Menu
|
||||
- [x] Structured Logging
|
||||
- [ ] Configurable
|
||||
- [ ] Better library (logrus, zap, zerolog, etc.)
|
||||
- [ ] Testing
|
||||
- [ ] Unit Tests
|
||||
- [ ] Integration Tests
|
||||
- [ ] Code Coverage
|
||||
- [x] Development Tooling
|
||||
- [x] Conventional Commits
|
||||
- [x] GitHub Actions
|
||||
- [x] Per-commit Artifacts
|
||||
- [x] MSI Packages
|
||||
- [ ] Automatic Releases (GitHub Releases, Winget)
|
||||
- [ ] Per-commit Artifacts
|
||||
- [ ] Winget Package Publishing
|
||||
- [ ] Testing, Linting, and/or Formatting
|
||||
- [ ] README Documentation Links
|
||||
13
Taskfile.yml
13
Taskfile.yml
@@ -1,4 +1,8 @@
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
APP_NAME: "HATray"
|
||||
VERSION: "0.0.1"
|
||||
|
||||
includes:
|
||||
build:
|
||||
@@ -36,7 +40,12 @@ tasks:
|
||||
|
||||
dev:
|
||||
desc: Development workflow - build, test, and run
|
||||
deps: [deps, fmt, vet, test, windows:build]
|
||||
deps: [deps, fmt, vet, test, build]
|
||||
cmds:
|
||||
- echo "Development workflow complete"
|
||||
- echo "Run 'task windows:run' to start the application"
|
||||
|
||||
version:
|
||||
desc: "Show the version of the application"
|
||||
cmds:
|
||||
- git describe --tags --abbrev=0 2>/dev/null || echo "unknown"
|
||||
|
||||
@@ -2,24 +2,32 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"
|
||||
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
|
||||
<Package Language="1033" Manufacturer="Xevion" Name="HATray" Scope="perMachine" UpgradeCode="87d36d2a-cb20-4d4b-87a2-a88c3f60ea44" Version="$(var.VERSION)">
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A later version of [ProductName] is already installed" AllowSameVersionUpgrades="yes" />
|
||||
<!-- <MediaTemplate EmbedCab="yes" /> -->
|
||||
<Package Language="1033" Manufacturer="Xevion" Name="HATray" Scope="perMachine"
|
||||
UpgradeCode="87d36d2a-cb20-4d4b-87a2-a88c3f60ea44" Version="$(var.VERSION)">
|
||||
<MajorUpgrade Schedule="afterInstallInitialize"
|
||||
DowngradeErrorMessage="A later version of [ProductName] is already installed"
|
||||
AllowSameVersionUpgrades="yes" />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
<!-- <Icon Id="icon.ico" SourceFile="ui\images\icon.ico"/> -->
|
||||
<!-- <Property Id="ARPPRODUCTICON" Value="icon.ico" /> -->
|
||||
<UI Id="UI">
|
||||
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLDIR" />
|
||||
</UI>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="build/msi/LICENSE.rtf"/>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="build/msi/LICENSE.rtf" />
|
||||
|
||||
<StandardDirectory Id="LocalAppDataFolder">
|
||||
<Directory Id="INSTALLDIR" Name="HATray">
|
||||
<Component Id="serviceComponent">
|
||||
<File Id="serviceBinary" Source="$(var.SOURCE)" KeyPath="yes" />
|
||||
<ServiceInstall Id="serviceInstall" Name="HATray" DisplayName="HATray" Description="..." Start="auto" Type="ownProcess" Vital="yes" ErrorControl="normal" Account="LocalSystem">
|
||||
<util:ServiceConfig FirstFailureActionType="restart" SecondFailureActionType="restart" ThirdFailureActionType="restart" RestartServiceDelayInSeconds="60" />
|
||||
<ServiceInstall Id="serviceInstall" Name="HATray" DisplayName="HATray"
|
||||
Description="..." Start="auto" Type="ownProcess" Vital="yes"
|
||||
ErrorControl="normal" Account="LocalSystem">
|
||||
<util:ServiceConfig FirstFailureActionType="restart"
|
||||
SecondFailureActionType="restart" ThirdFailureActionType="restart"
|
||||
RestartServiceDelayInSeconds="60" />
|
||||
</ServiceInstall>
|
||||
<ServiceControl Id="serviceControl" Name="HATray" Remove="both" Stop="both" Start="install" Wait="yes" />
|
||||
<ServiceControl Id="serviceControl" Name="HATray" Remove="both" Stop="both"
|
||||
Start="install" Wait="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
|
||||
45
build/unix/HATray.service
Normal file
45
build/unix/HATray.service
Normal file
@@ -0,0 +1,45 @@
|
||||
[Unit]
|
||||
Description=HATray - A tray utility for Home Assistant
|
||||
Documentation=https://github.com/Xevion/HATray
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
NotifyAccess=main
|
||||
ExecStart=$BINARY_PATH
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
WatchdogSec=10
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# CPUAccounting=yes
|
||||
# MemoryAccounting=yes
|
||||
# StandardOutput=journal
|
||||
# StandardError=journal
|
||||
# SyslogIdentifier=HATray
|
||||
|
||||
# Environment variables
|
||||
# Environment=HOME=/home/%i
|
||||
|
||||
# Security settings
|
||||
# NoNewPrivileges=true
|
||||
# PrivateTmp=true
|
||||
# ProtectSystem=strict
|
||||
# ProtectHome=true
|
||||
# ReadWritePaths=/home/%i/.config/HATray
|
||||
|
||||
# Resource limits (cgroups v2)
|
||||
# MemoryMax=128M
|
||||
# MemoryHigh=96M
|
||||
# MemorySwapMax=0
|
||||
# CPUQuota=10%
|
||||
# CPUWeight=100
|
||||
# IOWeight=100
|
||||
# TasksMax=100
|
||||
# LimitNOFILE=1024
|
||||
# LimitCORE=0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
75
cmd/main.go
Normal file
75
cmd/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"ha-tray/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
Commit = ""
|
||||
BuildDate = ""
|
||||
)
|
||||
|
||||
func main() {
|
||||
rootLogger, logFile, err := setupLogging()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to setup logging: %v", err)
|
||||
}
|
||||
defer logFile.Sync()
|
||||
defer logFile.Close()
|
||||
|
||||
mainLogger := rootLogger.With("type", "main")
|
||||
slog.SetDefault(rootLogger.With("type", "global"))
|
||||
|
||||
mainLogger.Info("HATray started", "version", Version, "commit", Commit, "built", BuildDate)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
mainLogger.Error("uncaught panic recovered", "panic", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create service layer
|
||||
svc := service.NewService(rootLogger)
|
||||
|
||||
mainLogger.Info("service initialized")
|
||||
|
||||
// Main loop
|
||||
if err := svc.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "application error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogging() (*slog.Logger, *os.File, error) {
|
||||
// Get the directory where the executable is located
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get executable path: %v", err)
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
|
||||
// Open log file in the same directory as the executable
|
||||
logFile, err := os.OpenFile(filepath.Join(exeDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
// Create multi-writer to log to both file and stdout
|
||||
multiWriter := io.MultiWriter(logFile, os.Stdout)
|
||||
|
||||
// Create JSON handler for structured logging
|
||||
handler := slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
})
|
||||
logger := slog.New(handler)
|
||||
|
||||
return logger, logFile, nil
|
||||
}
|
||||
45
go.mod
45
go.mod
@@ -1,5 +1,44 @@
|
||||
module hass-tray
|
||||
module ha-tray
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
require golang.org/x/sys v0.15.0
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/Xevion/go-ha v0.7.0
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/sys v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.1.5 // indirect
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
|
||||
github.com/getlantern/errors v1.0.4 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/gobuffalo/envy v1.10.2 // indirect
|
||||
github.com/gobuffalo/packd v1.0.2 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/golang-module/carbon v1.7.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/nathan-osman/go-sunrise v1.1.0 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
go.opentelemetry.io/otel v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.19.1 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
resty.dev/v3 v3.0.0-beta.3 // indirect
|
||||
)
|
||||
|
||||
204
go.sum
204
go.sum
@@ -1,2 +1,202 @@
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Workiva/go-datastructures v1.1.5 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEFvdAhd6Mkf4=
|
||||
github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/Xevion/go-ha v0.7.0 h1:jf+ZVSDaw0xjY0TcCA/TodWmAehtm47hDQI5z8XJMQE=
|
||||
github.com/Xevion/go-ha v0.7.0/go.mod h1:TN+40o0znxEdvR7GQgm5YWMiCEJvsoFbnro2oW38RVU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
|
||||
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
|
||||
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
|
||||
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
|
||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
|
||||
github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U=
|
||||
github.com/golang-module/carbon v1.7.3/go.mod h1:nUMnXq90Rv8a7h2+YOo2BGKS77Y0w/hMPm4/a8h19N8=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8=
|
||||
github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
|
||||
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
|
||||
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
|
||||
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
|
||||
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
|
||||
254
internal/app/app.go
Normal file
254
internal/app/app.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ha-tray/internal"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ga "github.com/Xevion/go-ha"
|
||||
)
|
||||
|
||||
// App represents the main application layer that is generic and cross-platform
|
||||
type App struct {
|
||||
logger *slog.Logger
|
||||
mu sync.RWMutex
|
||||
state AppState
|
||||
config *Config
|
||||
lastStarted *time.Time // time of last start, nil if never started
|
||||
tray *Tray // simple interface to systray
|
||||
ha *ga.App
|
||||
}
|
||||
|
||||
// AppState represents the current state of the application
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StatePaused AppState = iota
|
||||
StateRunning
|
||||
)
|
||||
|
||||
// String returns the string representation of the AppState
|
||||
func (s AppState) String() string {
|
||||
switch s {
|
||||
case StatePaused:
|
||||
return "paused"
|
||||
case StateRunning:
|
||||
return "running"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// NewApp creates a new application instance
|
||||
func NewApp(logger *slog.Logger) *App {
|
||||
return &App{
|
||||
logger: logger.With("type", "app"),
|
||||
state: StatePaused,
|
||||
config: nil,
|
||||
lastStarted: nil,
|
||||
tray: NewTray(logger.With("type", "tray")),
|
||||
ha: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Pause disconnects from the server and ceases any background tasks
|
||||
func (app *App) Pause() error {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
|
||||
switch app.state {
|
||||
case StatePaused:
|
||||
app.logger.Warn("application is already paused")
|
||||
return nil
|
||||
case StateRunning:
|
||||
// valid state to pause from, do nothing
|
||||
default:
|
||||
return fmt.Errorf("unexpected state encountered while pausing application: %s", app.state)
|
||||
}
|
||||
|
||||
app.logger.Info("pausing application",
|
||||
"action", "pause",
|
||||
"previous_state", app.state,
|
||||
"new_state", StatePaused)
|
||||
|
||||
// - Disconnect from Home Assistant WebSocket
|
||||
err := app.ha.Close()
|
||||
if err != nil {
|
||||
app.logger.Error("failed to close home assistant connection", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// - Stop tray icon event loop
|
||||
err = app.tray.Stop()
|
||||
if err != nil {
|
||||
app.logger.Error("failed to stop tray", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.state = StatePaused
|
||||
|
||||
app.logger.Info("paused successfully",
|
||||
"action", "pause",
|
||||
"state", app.state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume connects to the server and initiates background tasks
|
||||
// This function does not block permanently, it will return very quickly with an error if anything goes wrong.
|
||||
func (app *App) Resume() error {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
|
||||
switch app.state {
|
||||
case StateRunning:
|
||||
app.logger.Warn("application is already running")
|
||||
return nil
|
||||
case StatePaused:
|
||||
// valid state to resume from, do nothing
|
||||
default:
|
||||
return fmt.Errorf("unexpected state encountered while resuming application: %s", app.state)
|
||||
}
|
||||
|
||||
app.logger.Info("resuming application",
|
||||
"action", "resume",
|
||||
"previous_state", app.state,
|
||||
"new_state", StateRunning,
|
||||
"has_started", app.lastStarted,
|
||||
)
|
||||
|
||||
// TODO: Implement actual resume logic
|
||||
// - Connect to Home Assistant WebSocket
|
||||
// - Start background tasks
|
||||
// - Resume sensor monitoring
|
||||
err := app.tray.Start(fmt.Sprintf("HATray v%s", "0.0.1"))
|
||||
if err != nil {
|
||||
app.logger.Error("failed to start tray", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.config = DefaultConfig()
|
||||
|
||||
if err := app.config.Validate(); err != nil {
|
||||
app.logger.Error("invalid configuration", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.ha, err = ga.NewApp(ga.NewAppRequest{
|
||||
URL: *app.config.Server,
|
||||
HAAuthToken: app.config.APIKey,
|
||||
})
|
||||
if err != nil {
|
||||
app.logger.Error("failed to create Home Assistant app", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.ha.RegisterEntityListeners(ga.NewEntityListener().EntityIds("binary_sensor.bedroom_door_opening").Call(app.onEntityStateChange).Build())
|
||||
|
||||
go app.ha.Start()
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
state, err := app.ha.GetState().Get("binary_sensor.bedroom_door_opening")
|
||||
if err != nil {
|
||||
app.logger.Error("failed to get entity", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.logger.Info("state", "state", state.State)
|
||||
|
||||
if state.State == "on" {
|
||||
app.tray.SetIcon(IconOpen)
|
||||
} else {
|
||||
app.tray.SetIcon(IconClosed)
|
||||
}
|
||||
|
||||
app.state = StateRunning
|
||||
app.lastStarted = internal.Ptr(time.Now())
|
||||
|
||||
app.logger.Info("resumed successfully",
|
||||
"action", "resume",
|
||||
"state", app.state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) onEntityStateChange(se *ga.Service, st ga.State, e ga.EntityData) {
|
||||
entity, err := st.Get(e.TriggerEntityId)
|
||||
if err != nil {
|
||||
a.logger.Error("failed to get entity", "error", err)
|
||||
return
|
||||
}
|
||||
a.logger.Info("sensor.test state changed", "entity", e.TriggerEntityId, "state", entity.State)
|
||||
|
||||
if entity.State == "on" {
|
||||
a.tray.SetIcon(IconOpen)
|
||||
} else {
|
||||
a.tray.SetIcon(IconClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload pauses the application, re-reads configuration files, then resumes
|
||||
func (a *App) Reload() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
switch a.state {
|
||||
case StatePaused:
|
||||
return fmt.Errorf("cannot reload application when paused")
|
||||
case StateRunning:
|
||||
// valid state to reload from, do nothing
|
||||
default:
|
||||
return fmt.Errorf("unexpected state encountered while reloading application: %s", a.state)
|
||||
}
|
||||
|
||||
a.logger.Info("starting application reload",
|
||||
"action", "reload",
|
||||
"current_state", a.state)
|
||||
|
||||
// Pause if not already paused
|
||||
switch a.state {
|
||||
case StatePaused:
|
||||
// already paused, do nothing
|
||||
a.logger.Info("application is already paused during reload")
|
||||
case StateRunning:
|
||||
if err := a.Pause(); err != nil {
|
||||
a.logger.Error("failed to pause during reload",
|
||||
"action", "reload",
|
||||
"error", err)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected state encountered while pausing for reload: %s", a.state)
|
||||
}
|
||||
|
||||
// TODO: Implement configuration reload logic
|
||||
// - Re-read TOML configuration files
|
||||
// - Validate configuration
|
||||
// - Update internal state with new configuration
|
||||
|
||||
a.logger.Info("configuration reloaded successfully",
|
||||
"action", "reload")
|
||||
|
||||
// Resume the application
|
||||
if err := a.Resume(); err != nil {
|
||||
a.logger.Error("failed to resume during reload",
|
||||
"action", "reload",
|
||||
"error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
a.logger.Info("application reload completed successfully",
|
||||
"action", "reload",
|
||||
"final_state", a.state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetState returns the current state of the application
|
||||
func (a *App) GetState() AppState {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.state
|
||||
}
|
||||
89
internal/app/config.go
Normal file
89
internal/app/config.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ha-tray/internal"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
Server *string `toml:"server"`
|
||||
APIKey string `toml:"api_key"`
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default configuration
|
||||
func DefaultConfig() *Config {
|
||||
apiKey := os.Getenv("API_KEY")
|
||||
if apiKey == "" {
|
||||
// Try loading from .env
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
apiKey = os.Getenv("API_KEY")
|
||||
} else {
|
||||
apiKey = os.Getenv("API_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
instanceUrl := internal.Ptr(strings.TrimSpace(os.Getenv("INSTANCE_URL")))
|
||||
if *instanceUrl == "" {
|
||||
instanceUrl = nil
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Server: instanceUrl,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from a TOML file
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
config := DefaultConfig()
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
// Create default config file if it doesn't exist
|
||||
if err := SaveConfig(filename, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to create default config file: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Load existing config file
|
||||
if _, err := toml.DecodeFile(filename, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config file: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves configuration to a TOML file
|
||||
func SaveConfig(filename string, config *Config) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := toml.NewEncoder(file)
|
||||
if err := encoder.Encode(config); err != nil {
|
||||
return fmt.Errorf("failed to encode config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
if c.Server == nil || *c.Server == "" {
|
||||
return fmt.Errorf("server address is required")
|
||||
}
|
||||
if c.APIKey == "" {
|
||||
return fmt.Errorf("API key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
101
internal/app/tray.go
Normal file
101
internal/app/tray.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ha-tray/internal"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
type IconReference string
|
||||
|
||||
const (
|
||||
IconOpen IconReference = "open"
|
||||
IconClosed IconReference = "closed"
|
||||
IconUnknown IconReference = "unknown"
|
||||
)
|
||||
|
||||
// Path returns the path to the icon file
|
||||
func (i IconReference) Path() string {
|
||||
switch i {
|
||||
case IconOpen:
|
||||
return "resources/open.ico"
|
||||
case IconClosed:
|
||||
return "resources/closed.ico"
|
||||
default:
|
||||
return "resources/unknown.ico"
|
||||
}
|
||||
}
|
||||
|
||||
type Tray struct {
|
||||
active bool
|
||||
currentIcon *IconReference
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewTray(logger *slog.Logger) *Tray {
|
||||
return &Tray{
|
||||
logger: logger,
|
||||
currentIcon: nil,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tray) SetIcon(icon IconReference) error {
|
||||
if !t.active {
|
||||
return fmt.Errorf("tray is not active")
|
||||
}
|
||||
|
||||
iconBytes, err := internal.Icons.ReadFile(icon.Path())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read icon: %w", err)
|
||||
}
|
||||
systray.SetIcon(iconBytes)
|
||||
t.currentIcon = &icon
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tray) Start(title string) error {
|
||||
if t.active {
|
||||
t.logger.Warn("tray is already active")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.logger.Info("attempting to start systray", "title", title)
|
||||
readyTimeout := make(chan struct{}, 1)
|
||||
go systray.Run(func() {
|
||||
systray.SetTitle(title)
|
||||
systray.SetTooltip(title)
|
||||
|
||||
t.logger.Info("systray started")
|
||||
readyTimeout <- struct{}{}
|
||||
close(readyTimeout)
|
||||
}, func() {
|
||||
t.active = false
|
||||
})
|
||||
|
||||
select {
|
||||
case <-readyTimeout:
|
||||
t.logger.Info("systray start confirmed")
|
||||
t.active = true
|
||||
return nil
|
||||
case <-time.After(5 * time.Second):
|
||||
close(readyTimeout)
|
||||
t.logger.Error("systray start timed out")
|
||||
return fmt.Errorf("tray did not start in time")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tray) Stop() error {
|
||||
if !t.active {
|
||||
return fmt.Errorf("tray is not active")
|
||||
}
|
||||
|
||||
systray.Quit()
|
||||
t.active = false
|
||||
|
||||
return nil
|
||||
}
|
||||
5
internal/misc.go
Normal file
5
internal/misc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package internal
|
||||
|
||||
func Ptr[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
8
internal/resources.go
Normal file
8
internal/resources.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package internal
|
||||
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed resources/*.ico
|
||||
Icons embed.FS
|
||||
)
|
||||
BIN
internal/resources/closed.ico
Normal file
BIN
internal/resources/closed.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
internal/resources/open.ico
Normal file
BIN
internal/resources/open.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
internal/resources/unknown.ico
Normal file
BIN
internal/resources/unknown.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
108
internal/service/linux.go
Normal file
108
internal/service/linux.go
Normal file
@@ -0,0 +1,108 @@
|
||||
//go:build linux
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ha-tray/internal/app"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
)
|
||||
|
||||
// linuxService implements the Service interface for Linux
|
||||
// It integrates with systemd and controls the app layer
|
||||
// according to systemd signals (start, stop, reload)
|
||||
type linuxService struct {
|
||||
logger *slog.Logger
|
||||
app *app.App
|
||||
}
|
||||
|
||||
// NewService creates a new Linux service instance
|
||||
func NewService(logger *slog.Logger) Service {
|
||||
return &linuxService{
|
||||
logger: logger.With("type", "service", "variant", "linux"),
|
||||
app: app.NewApp(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements the Service interface for Linux
|
||||
func (s *linuxService) Run() error {
|
||||
startTime := time.Now()
|
||||
s.logger.Info("starting service", "start_time", startTime.Format(time.RFC3339))
|
||||
|
||||
// Notify systemd that we are starting
|
||||
daemon.SdNotify(false, "STATUS=starting\n")
|
||||
|
||||
// Setup signal handling for systemd
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
|
||||
// Setup watchdog to systemd
|
||||
var watchdog *time.Ticker
|
||||
if watchdogUSec, err := daemon.SdWatchdogEnabled(false); err == nil && watchdogUSec > 0 {
|
||||
watchdog = time.NewTicker(watchdogUSec / 2)
|
||||
}
|
||||
defer func() {
|
||||
if watchdog != nil {
|
||||
watchdog.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
// Setup heartbeat to systemd
|
||||
heartbeat := time.NewTicker(30 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
|
||||
// Start the service (backgrounded so that the service can still respond to systemd signals, the app layer is still designed for concurrency)
|
||||
go func() {
|
||||
if err := s.app.Resume(); err != nil {
|
||||
s.logger.Error("failed to start (resume) app layer", "error", err)
|
||||
|
||||
// TODO: This has no true error handling, retry mechanism, or timeout mechanism. If this fails, then the service will be stuck in the 'StartPending' state.
|
||||
}
|
||||
|
||||
// Notify systemd that we are ready (and running)
|
||||
daemon.SdNotify(false, daemon.SdNotifyReady)
|
||||
daemon.SdNotify(false, fmt.Sprintf("STATUS=running for %s\n", time.Since(startTime).String()))
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
// This is only called if the service is configured with watchdog
|
||||
case <-watchdog.C:
|
||||
daemon.SdNotify(false, daemon.SdNotifyWatchdog)
|
||||
case <-heartbeat.C:
|
||||
daemon.SdNotify(false, fmt.Sprintf("STATUS=running for %s\n", time.Since(startTime).String()))
|
||||
case sig := <-sigs:
|
||||
s.logger.Info("signal received", "signal", sig)
|
||||
|
||||
switch sig {
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
daemon.SdNotify(false, daemon.SdNotifyStopping)
|
||||
s.logger.Info("stopping service")
|
||||
|
||||
if err := s.app.Pause(); err != nil {
|
||||
s.logger.Error("failed to pause app layer", "error", err)
|
||||
}
|
||||
|
||||
return nil // exit the service
|
||||
case syscall.SIGHUP:
|
||||
s.logger.Info("reloading service")
|
||||
daemon.SdNotify(false, daemon.SdNotifyReloading)
|
||||
|
||||
if err := s.app.Reload(); err != nil {
|
||||
s.logger.Error("failed to reload app layer", "error", err)
|
||||
}
|
||||
|
||||
daemon.SdNotify(false, daemon.SdNotifyReady)
|
||||
default:
|
||||
s.logger.Warn("unhandled signal", "signal", sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
internal/service/service.go
Normal file
9
internal/service/service.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package service
|
||||
|
||||
// This is an intentionally very-simple interface as the main program entrypoint needs to know very little about the service layer.
|
||||
// The service layer is completely responsible for the lifecycle of the application, implemented per-platform.
|
||||
type Service interface {
|
||||
Run() error
|
||||
}
|
||||
|
||||
// You create a service using the NewService() function, implemented per-platform. If you don't have a NewService() function, you can't create a service on your platform.
|
||||
189
internal/service/windows.go
Normal file
189
internal/service/windows.go
Normal file
@@ -0,0 +1,189 @@
|
||||
//go:build windows
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ha-tray/internal/app"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// windowsService implements the Service interface for Windows
|
||||
// Note that this is a user application, not a true SCM service
|
||||
type windowsService struct {
|
||||
app *app.App
|
||||
logger *slog.Logger
|
||||
restartCount int
|
||||
maxRestarts int
|
||||
restartDelay time.Duration
|
||||
quitChan chan struct{}
|
||||
restartChan chan struct{}
|
||||
}
|
||||
|
||||
// NewService creates a new Windows tray service instance
|
||||
func NewService(logger *slog.Logger) Service {
|
||||
return &windowsService{
|
||||
logger: logger.With("type", "service", "variant", "windows"),
|
||||
app: app.NewApp(logger),
|
||||
maxRestarts: 3,
|
||||
restartDelay: 5 * time.Second,
|
||||
quitChan: make(chan struct{}),
|
||||
restartChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements the Service interface for Windows
|
||||
func (svc *windowsService) Run() error {
|
||||
svc.logger.Info("starting Windows tray service")
|
||||
|
||||
// Setup auto-start if not already configured
|
||||
if err := svc.setupAutoStart(); err != nil {
|
||||
svc.logger.Warn("failed to setup auto-start", "error", err)
|
||||
}
|
||||
|
||||
// Setup signal handling
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
|
||||
// Setup power management (sleep/wake)
|
||||
svc.setupPowerManagement()
|
||||
|
||||
// Main service loop with restart capability
|
||||
for {
|
||||
select {
|
||||
case <-svc.quitChan:
|
||||
svc.logger.Info("service shutdown requested")
|
||||
return nil
|
||||
default:
|
||||
if err := svc.runServiceLoop(sigs); err != nil {
|
||||
svc.logger.Error("service loop failed", "error", err)
|
||||
|
||||
if svc.restartCount < svc.maxRestarts {
|
||||
svc.restartCount++
|
||||
svc.logger.Info("restarting service", "attempt", svc.restartCount, "max", svc.maxRestarts)
|
||||
time.Sleep(svc.restartDelay)
|
||||
continue
|
||||
} else {
|
||||
svc.logger.Error("max restarts exceeded, shutting down")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runServiceLoop runs the main service loop
|
||||
func (svc *windowsService) runServiceLoop(sigs chan os.Signal) error {
|
||||
// Start the application in background
|
||||
go func() {
|
||||
if err := svc.app.Resume(); err != nil {
|
||||
svc.logger.Error("failed to start app layer", "error", err)
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
// Service heartbeat
|
||||
heartbeat := time.NewTicker(30 * time.Second)
|
||||
defer heartbeat.Stop()
|
||||
|
||||
// Watchdog for app health
|
||||
watchdog := time.NewTicker(60 * time.Second)
|
||||
defer watchdog.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-svc.quitChan:
|
||||
svc.logger.Info("shutting down service")
|
||||
if err := svc.app.Pause(); err != nil {
|
||||
svc.logger.Error("failed to pause app layer", "error", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
case <-svc.restartChan:
|
||||
svc.logger.Info("restarting service")
|
||||
if err := svc.app.Reload(); err != nil {
|
||||
svc.logger.Error("failed to reload app layer", "error", err)
|
||||
}
|
||||
|
||||
case <-heartbeat.C:
|
||||
svc.logger.Debug("service heartbeat", "uptime", time.Since(time.Now()))
|
||||
|
||||
case <-watchdog.C:
|
||||
// Check if app is healthy
|
||||
if !svc.isAppHealthy() {
|
||||
svc.logger.Warn("app health check failed, triggering restart")
|
||||
svc.restartChan <- struct{}{}
|
||||
}
|
||||
|
||||
case sig := <-sigs:
|
||||
svc.logger.Info("signal received", "signal", sig)
|
||||
close(svc.quitChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setupAutoStart configures the application to start automatically on login
|
||||
func (svc *windowsService) setupAutoStart() error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %v", err)
|
||||
}
|
||||
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open registry key: %v", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
// Use quotes around path to handle spaces
|
||||
exePath = fmt.Sprintf(`"%s"`, exePath)
|
||||
|
||||
if err := key.SetStringValue("HATray", exePath); err != nil {
|
||||
return fmt.Errorf("failed to set registry value: %v", err)
|
||||
}
|
||||
|
||||
svc.logger.Info("auto-start configured", "path", exePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeAutoStart removes the auto-start configuration
|
||||
func (svc *windowsService) removeAutoStart() error {
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open registry key: %v", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if err := key.DeleteValue("HATray"); err != nil {
|
||||
return fmt.Errorf("failed to delete registry value: %v", err)
|
||||
}
|
||||
|
||||
svc.logger.Info("auto-start removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupPowerManagement handles sleep/wake events
|
||||
func (svc *windowsService) setupPowerManagement() {
|
||||
// TODO: Implement Windows power management
|
||||
// - Listen for WM_POWERBROADCAST messages
|
||||
// - Handle system sleep/wake events
|
||||
// - Pause/resume app accordingly
|
||||
svc.logger.Debug("power management setup (not implemented)")
|
||||
}
|
||||
|
||||
// isAppHealthy checks if the application is running properly
|
||||
func (svc *windowsService) isAppHealthy() bool {
|
||||
// TODO: Implement health checks
|
||||
// - Check if Home Assistant connection is alive
|
||||
// - Check if systray is responsive
|
||||
// - Check memory usage
|
||||
// - Check for any error conditions
|
||||
return true
|
||||
}
|
||||
184
main.go
184
main.go
@@ -1,184 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/debug"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
var elog debug.Log
|
||||
|
||||
type myservice struct{}
|
||||
|
||||
func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
// Get the directory where the executable is located
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("Failed to get executable path: %v", err))
|
||||
return
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
|
||||
// Open log file in the same directory as the executable
|
||||
logFile, err := os.OpenFile(filepath.Join(exeDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("Failed to open log file: %v", err))
|
||||
return
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
// Create JSON logger
|
||||
logger := log.New(logFile, "", 0)
|
||||
|
||||
// Log startup
|
||||
startupLog := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"level": "debug",
|
||||
"message": "Service starting",
|
||||
"service": "hass-tray",
|
||||
}
|
||||
startupJSON, _ := json.Marshal(startupLog)
|
||||
logger.Println(string(startupJSON))
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
|
||||
// Main service loop
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
// Log shutdown
|
||||
shutdownLog := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"level": "debug",
|
||||
"message": "Service stopping",
|
||||
"service": "hass-tray",
|
||||
}
|
||||
shutdownJSON, _ := json.Marshal(shutdownLog)
|
||||
logger.Println(string(shutdownJSON))
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return
|
||||
case svc.Pause:
|
||||
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
||||
case svc.Continue:
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
default:
|
||||
elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case <-ticker.C:
|
||||
// Log heartbeat
|
||||
heartbeatLog := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"level": "debug",
|
||||
"message": "Service heartbeat",
|
||||
"service": "hass-tray",
|
||||
}
|
||||
heartbeatJSON, _ := json.Marshal(heartbeatLog)
|
||||
logger.Println(string(heartbeatJSON))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runService(name string, isDebug bool) {
|
||||
var err error
|
||||
if isDebug {
|
||||
elog = debug.New(name)
|
||||
} else {
|
||||
elog, err = eventlog.Open(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
elog.Info(1, fmt.Sprintf("starting %s service", name))
|
||||
run := svc.Run
|
||||
if isDebug {
|
||||
run = debug.Run
|
||||
}
|
||||
err = run(name, &myservice{})
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
|
||||
return
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", name))
|
||||
}
|
||||
|
||||
func main() {
|
||||
isDebug, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
}
|
||||
|
||||
if !isDebug {
|
||||
runService("hass-tray", false)
|
||||
return
|
||||
}
|
||||
|
||||
// Interactive mode - just run the service logic directly
|
||||
fmt.Println("Running in interactive mode...")
|
||||
|
||||
// Get the current directory for log file
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
|
||||
// Open log file
|
||||
logFile, err := os.OpenFile(filepath.Join(currentDir, "current.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open log file: %v", err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
// Create JSON logger
|
||||
logger := log.New(logFile, "", 0)
|
||||
|
||||
// Log startup
|
||||
startupLog := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"level": "debug",
|
||||
"message": "Application starting in interactive mode",
|
||||
"service": "hass-tray",
|
||||
}
|
||||
startupJSON, _ := json.Marshal(startupLog)
|
||||
logger.Println(string(startupJSON))
|
||||
|
||||
fmt.Println("Press Ctrl+C to stop...")
|
||||
|
||||
// Simple interactive loop
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Log heartbeat
|
||||
heartbeatLog := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"level": "debug",
|
||||
"message": "Application heartbeat",
|
||||
"service": "hass-tray",
|
||||
}
|
||||
heartbeatJSON, _ := json.Marshal(heartbeatLog)
|
||||
logger.Println(string(heartbeatJSON))
|
||||
}
|
||||
}
|
||||
}
|
||||
33
tasks/Taskfile_linux.yml
Normal file
33
tasks/Taskfile_linux.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
BINARY_NAME: "{{.APP_NAME}}"
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -ldflags "-X main.Version=$(git describe --tags --abbrev=0 2>/dev/null || echo 'unknown') -X main.Commit=$(git rev-parse --short HEAD) -X 'main.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" -o ./bin/{{.BINARY_NAME}} ./cmd/main.go
|
||||
|
||||
service:
|
||||
desc: "Install the service"
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p $HOME/.config/systemd/user
|
||||
- cmd: systemctl stop HATray --user
|
||||
ignore_error: true
|
||||
- cp ./bin/{{.BINARY_NAME}} $HOME/.local/bin/{{.BINARY_NAME}}
|
||||
# super hacky way of fixing MAINPID being replaced
|
||||
- BINARY_PATH="$HOME/.local/bin/{{.BINARY_NAME}}" MAINPID='$MAINPID' envsubst < ./build/unix/HATray.service > $HOME/.config/systemd/user/HATray.service
|
||||
- systemctl daemon-reload --user
|
||||
# - systemctl enable HATray --user
|
||||
- systemctl start HATray --user
|
||||
|
||||
status:
|
||||
desc: "Show the status of the service"
|
||||
cmds:
|
||||
- systemctl status HATray --user
|
||||
|
||||
tail:
|
||||
desc: "Tail the log file"
|
||||
cmds:
|
||||
- journalctl --user-unit HATray.service -ef
|
||||
@@ -1,18 +1,54 @@
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
APP_NAME: 'HATray'
|
||||
EXE_NAME: '{{.APP_NAME}}.exe'
|
||||
VERSION: '0.0.1'
|
||||
BINARY_NAME: "{{.APP_NAME}}.exe"
|
||||
VERSION:
|
||||
sh: powershell -Command "try { git describe --tags --abbrev=0 } catch { 'unknown' }"
|
||||
COMMIT:
|
||||
sh: git rev-parse --short HEAD
|
||||
|
||||
tasks:
|
||||
build:
|
||||
vars:
|
||||
BUILDDATE:
|
||||
sh: powershell -Command "Get-Date -Format yyyy-MM-ddTHH:mm:ssZ"
|
||||
cmds:
|
||||
- go build -o ./dist/{{.EXE_NAME}} .
|
||||
- go build -ldflags "-X main.Version={{.VERSION}} -X main.Commit={{.COMMIT}} -X 'main.BuildDate={{.BUILDDATE}}'" -o ./bin/{{.BINARY_NAME}} ./cmd/main.go
|
||||
|
||||
run:
|
||||
desc: "Run the application"
|
||||
cmds:
|
||||
- go run ./cmd/main.go
|
||||
|
||||
service:
|
||||
desc: "Install the service"
|
||||
deps: [build]
|
||||
cmds:
|
||||
# Create the service, if not already present
|
||||
- cmd: pwsh -c 'sc create HATray binPath= "$env:USERPROFILE\\AppData\\Local\\HATray\\{{.BINARY_NAME}}" start=auto'
|
||||
ignore_error: true
|
||||
# Stop the service, if running
|
||||
- cmd: pwsh -c 'sc stop HATray'
|
||||
ignore_error: true
|
||||
# Replace the binary
|
||||
- cmd: pwsh -c 'Copy-Item -Force -Path .\\bin\\{{.BINARY_NAME}} -Destination $env:USERPROFILE\\AppData\\Local\\HATray\\{{.BINARY_NAME}}'
|
||||
# Start the service
|
||||
- cmd: pwsh -c 'sc start HATray'
|
||||
|
||||
status:
|
||||
desc: "Show the status of the service"
|
||||
cmds:
|
||||
- cmd: sc query HATray
|
||||
|
||||
tail:
|
||||
desc: "Tail the log file"
|
||||
cmds:
|
||||
- cmd: pwsh -c 'Get-Content -Path $env:LOCALAPPDATA\\HATray\\current.log -Tail 10 -Wait'
|
||||
|
||||
package:
|
||||
desc: "Package the application as a MSI"
|
||||
deps: [build]
|
||||
cmds:
|
||||
- wix extension add WixToolset.Util.wixext
|
||||
- wix extension add WixToolset.UI.wixext
|
||||
- wix build -ext WixToolset.Util.wixext -ext WixToolset.UI.wixext -o ./dist/{{.APP_NAME}}-{{.VERSION}}.msi build/msi/HATray.wxs -arch x64 -d VERSION={{.VERSION}} -d SOURCE=./dist/{{.EXE_NAME}}
|
||||
- wix build -ext WixToolset.Util.wixext -ext WixToolset.UI.wixext -o ./bin/{{.APP_NAME}}.msi build/msi/HATray.wxs -arch x64 -d VERSION={{.VERSION}} -d SOURCE=./bin/{{.BINARY_NAME}}
|
||||
|
||||
Reference in New Issue
Block a user