Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/windows-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,31 @@ jobs:
cd $GITHUB_WORKSPACE/build
mingw32-make -j$(nproc) cli_wallet
# -------------------------------------------------------
# 10b. Build the tray launcher (Go, static, GUI subsystem)
# -------------------------------------------------------
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
cache-dependency-path: contrib/windows/vizd-tray/go.sum

- name: Build vizd-tray
shell: pwsh
run: |
cd $env:GITHUB_WORKSPACE/contrib/windows/vizd-tray
$env:CGO_ENABLED = "0"
$env:GOOS = "windows"
$env:GOARCH = "amd64"
go build -trimpath -ldflags "-s -w -H windowsgui" -o $env:GITHUB_WORKSPACE/build/vizd-tray.exe .
# -------------------------------------------------------
# 11. Verify binaries
# -------------------------------------------------------
- name: Verify binaries
run: |
ls -lh $GITHUB_WORKSPACE/build/programs/vizd/vizd.exe
ls -lh $GITHUB_WORKSPACE/build/programs/cli_wallet/cli_wallet.exe
ls -lh $GITHUB_WORKSPACE/build/vizd-tray.exe
# -------------------------------------------------------
# 12. Derive release metadata
# Tag: win-<short-sha> e.g. win-a1b2c3d
Expand Down Expand Up @@ -226,5 +245,6 @@ jobs:
files: |
build/programs/vizd/vizd.exe
build/programs/cli_wallet/cli_wallet.exe
build/vizd-tray.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 changes: 31 additions & 0 deletions contrib/windows/vizd-tray/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# vizd-tray

Tiny system-tray launcher for the Windows `vizd.exe` node (Go, static, GUI subsystem).

## What it does

- Runs `vizd.exe -d data` sharing this process's console, so vizd's colored log
output (Windows `SetConsoleTextAttribute`) keeps working.
- Shows a tray icon with a context menu:
- **Open window** — bring the node console back.
- **Close vizd** — stop the node cleanly (Ctrl+C → appbase SIGINT), then exit.
- **Enable autostart** — toggle a `HKCU\...\Run` entry (checked when enabled).
- **Quit** — same as Close vizd.
- Closing the console window (X) hides it to the tray instead of stopping vizd —
see the `SetConsoleCtrlHandler(CTRL_CLOSE_EVENT)` handler in `programs/vizd/main.cpp`.

## Build

```sh
cd contrib/windows/vizd-tray
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath \
-ldflags "-s -w -H windowsgui" -o vizd-tray.exe .
```

The release workflow (`windows-release.yml`) builds it with the same flags and
ships `vizd-tray.exe` next to `vizd.exe`.

## Layout

- `main.go` — tray, console management, vizd lifecycle, autostart.
- `icon.ico` — placeholder tray icon (replace with a proper VIZ icon).
10 changes: 10 additions & 0 deletions contrib/windows/vizd-tray/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module vizd-tray

go 1.22

require (
fyne.io/systray v1.12.2
golang.org/x/sys v0.15.0
)

require github.com/godbus/dbus/v5 v5.1.0 // indirect
6 changes: 6 additions & 0 deletions contrib/windows/vizd-tray/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Binary file added contrib/windows/vizd-tray/icon.ico
Binary file not shown.
260 changes: 260 additions & 0 deletions contrib/windows/vizd-tray/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
//go:build windows

// vizd-tray is a tiny system-tray launcher for the Windows vizd node. It runs
// vizd.exe sharing this process's console (so vizd's colored log output keeps
// working), shows a tray icon, and:
// - hides the console window to the tray when it is closed (X) instead of
// stopping the node;
// - "Open window" brings the console back;
// - "Close vizd" sends Ctrl+C so vizd shuts down cleanly (appbase SIGINT),
// then the launcher exits;
// - "Enable autostart" (checkbox) writes/removes a HKCU Run entry.
package main

import (
_ "embed"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"time"
"unsafe"

"fyne.io/systray"
"golang.org/x/sys/windows/registry"
)

//go:embed icon.ico
var trayIcon []byte

var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
user32 = syscall.NewLazyDLL("user32.dll")

procAllocConsole = kernel32.NewProc("AllocConsole")
procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow")
procSetConsoleCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler")
procGenerateConsoleCtrlEvent = kernel32.NewProc("GenerateConsoleCtrlEvent")

procShowWindow = user32.NewProc("ShowWindow")
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
procMessageBox = user32.NewProc("MessageBoxW")
)

const (
ctrlCEvent = 0
ctrlBreakEvent = 1
ctrlCloseEvent = 2

swHide = 0
swRestore = 9

runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
runValue = `vizd-tray`

stopGrace = 10 * time.Second
)

// --- console window management -------------------------------------------

func allocConsole() { procAllocConsole.Call() }
func consoleWindow() uintptr {
hwnd, _, _ := procGetConsoleWindow.Call()
return hwnd
}
func showConsole() {
if hwnd := consoleWindow(); hwnd != 0 {
procShowWindow.Call(hwnd, swRestore)
procSetForegroundWindow.Call(hwnd)
}
}
func hideConsole() {
if hwnd := consoleWindow(); hwnd != 0 {
procShowWindow.Call(hwnd, swHide)
}
}

// consoleCtrlHandler is installed via SetConsoleCtrlHandler. The launcher owns
// the console window: closing it (X) hides it to the tray instead of exiting,
// and the launcher survives the Ctrl+C it sends to stop vizd (vizd receives its
// own copy and shuts down cleanly).
func consoleCtrlHandler(ctrlType uintptr) uintptr {
switch ctrlType {
case ctrlCloseEvent:
hideConsole()
return 1 // handled
case ctrlCEvent:
return 1 // swallow so the launcher survives
}
return 0
}

func installCtrlHandler() {
cb := syscall.NewCallback(consoleCtrlHandler)
procSetConsoleCtrlHandler.Call(cb, 1)
}

// --- vizd lifecycle -------------------------------------------------------

func bundleDir() (string, error) {
self, err := os.Executable()
if err != nil {
return "", err
}
return filepath.Dir(self), nil
}

func spawnVizd() (*exec.Cmd, error) {
dir, err := bundleDir()
if err != nil {
return nil, err
}
vizdExe := filepath.Join(dir, "vizd.exe")
if _, err := os.Stat(vizdExe); err != nil {
return nil, fmt.Errorf("vizd.exe not found next to the launcher: %v", err)
}
cmd := exec.Command(vizdExe, "-d", "data")
cmd.Dir = dir
// No CREATE_NEW_CONSOLE / CREATE_NO_WINDOW: vizd inherits this process's
// console, so its SetConsoleTextAttribute coloring works and Ctrl+C can be
// delivered to the shared console group.
if err := cmd.Start(); err != nil {
return nil, err
}
return cmd, nil
}

func stopVizd(cmd *exec.Cmd) {
if cmd == nil || cmd.Process == nil {
return
}
// Ctrl+C to the whole shared console group (launcher + vizd).
procGenerateConsoleCtrlEvent.Call(ctrlCEvent, 0)

done := make(chan struct{})
go func() {
_, _ = cmd.Process.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(stopGrace):
_ = cmd.Process.Kill()
}
}

// --- autostart (HKCU Run) -------------------------------------------------

func autostartEnabled() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
_, _, err = k.GetStringValue(runValue)
return err == nil
}

func setAutostart(enabled bool) error {
exe, err := os.Executable()
if err != nil {
return err
}
if enabled {
k, _, err := registry.CreateKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
return k.SetStringValue(runValue, fmt.Sprintf(`"%s"`, exe))
}
k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
if err != nil {
return nil // key absent: nothing to remove
}
defer k.Close()
_ = k.DeleteValue(runValue)
return nil
}

func errBox(title, text string) {
t, _ := syscall.UTF16PtrFromString(text)
c, _ := syscall.UTF16PtrFromString(title)
procMessageBox.Call(0, uintptr(unsafe.Pointer(t)), uintptr(unsafe.Pointer(c)), 0x10)
}

func toggleAutostart(item *systray.MenuItem) {
enable := !item.Checked()
if err := setAutostart(enable); err != nil {
errBox("vizd-tray", "Could not update autostart: "+err.Error())
return
}
if enable {
item.Check()
} else {
item.Uncheck()
}
}

// --- entry point ----------------------------------------------------------

func main() {
// Allocate the console vizd will share, then install our ctrl handler so
// closing that window (X) hides it to the tray instead of killing vizd.
allocConsole()
installCtrlHandler()
signal.Ignore(os.Interrupt) // defensive: survive the Ctrl+C we send

vizd, err := spawnVizd()
if err != nil {
errBox("vizd-tray", err.Error())
}

ready := make(chan struct{})

// Follow vizd: when it exits (Close vizd, Ctrl+C in the window, crash) the
// launcher has nothing left to do and exits as well.
if vizd != nil {
go func() {
_ = vizd.Wait()
<-ready
systray.Quit()
}()
}

onReady := func() {
systray.SetIcon(trayIcon)
systray.SetTitle("VIZ node")
systray.SetTooltip("VIZ node (vizd)")

mOpen := systray.AddMenuItem("Open window", "Show the node console")
mClose := systray.AddMenuItem("Close vizd", "Stop the node")
mAuto := systray.AddMenuItemCheckbox("Enable autostart", "Start the node at Windows login", autostartEnabled())
systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit", "Stop vizd and exit")

close(ready)

go func() {
for {
select {
case <-mOpen.ClickedCh:
showConsole()
case <-mClose.ClickedCh:
stopVizd(vizd)
systray.Quit()
case <-mAuto.ClickedCh:
toggleAutostart(mAuto)
case <-mQuit.ClickedCh:
stopVizd(vizd)
systray.Quit()
}
}
}()
}
onExit := func() {}

systray.Run(onReady, onExit)
}
21 changes: 21 additions & 0 deletions programs/vizd/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@

#include <graphene/utilities/git_revision.hpp>

#ifdef _WIN32
#include <windows.h>

/// Tray-launcher support: the launcher owns the console window lifecycle.
/// Surviving CTRL_CLOSE_EVENT lets the launcher hide the window when it is
/// closed instead of the process being terminated. All other events
/// (CTRL_C_EVENT in particular) fall through so the appbase SIGINT handler
/// performs a clean shutdown.
static BOOL WINAPI vizd_tray_console_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_CLOSE_EVENT)
return TRUE;
return FALSE;
}
#endif

using graphene::protocol::version;


Expand Down Expand Up @@ -121,6 +136,12 @@ int main( int argc, char** argv ) {
wlog("Error parsing logging config");
}

#ifdef _WIN32
// Let the tray launcher manage the console window: closing it (X)
// must hide vizd, not stop it. See vizd_tray_console_ctrl_handler.
SetConsoleCtrlHandler(vizd_tray_console_ctrl_handler, TRUE);
#endif

appbase::app().startup();
appbase::app().exec();
ilog("exited cleanly");
Expand Down