Skip to content

Windows

Each release has a self-contained download for Windows x64 (Windows 10, 11, Server 2019 and later). Nothing else to install.

Install as a service

In an elevated PowerShell (Run as administrator), from the folder you downloaded to:

$base = 'https://gitlab.com/killswitch10/punchlist/-/releases/permalink/latest/downloads'
Invoke-WebRequest "$base/punchlist-win-x64.zip" -OutFile punchlist-win-x64.zip
Invoke-WebRequest "$base/SHA256SUMS" -OutFile SHA256SUMS
# The two hashes must match:
(Get-FileHash punchlist-win-x64.zip -Algorithm SHA256).Hash.ToLower()
Select-String 'win-x64' SHA256SUMS
Expand-Archive punchlist-win-x64.zip -DestinationPath .
& .\punchlist-win-x64\examples\install-service.ps1

The script installs and starts the Punchlist service and prints the admin password to sign in with (and change). Options: -Port 9000 for another port, -OpenFirewall to allow the port from your private network.

Path What On upgrade
C:\Program Files\Punchlist The program Replaced
C:\ProgramData\Punchlist\punchlist.conf Your settings (settings file) Kept
C:\ProgramData\Punchlist Database, snapshots, logs Kept

The service runs as its own virtual account, NT SERVICE\Punchlist, which may change only the data folder. It starts with Windows and restarts by itself after a crash.

install-service.ps1
<#
.SYNOPSIS
  Install (or upgrade) Punchlist as a Windows service.

.DESCRIPTION
  Run from the unpacked download in an elevated PowerShell:
    Expand-Archive punchlist-win-x64.zip -DestinationPath .
    .\punchlist-win-x64\examples\install-service.ps1

  Program:  C:\Program Files\Punchlist                (replaced on every run)
  Data:     C:\ProgramData\Punchlist                  (database, snapshots, logs; kept)
  Settings: C:\ProgramData\Punchlist\punchlist.conf   (kept)

  The service runs as its own virtual account, NT SERVICE\Punchlist, which may change only the
  data folder. On a first install the script prints the admin password (to change at first
  sign-in). Upgrading is the same command with the newer download.

.PARAMETER Port
  The port the server listens on (all addresses). Default 8080.

.PARAMETER OpenFirewall
  Also allow the port through Windows Defender Firewall (private networks only).
#>
[CmdletBinding()]
param(
    [int]$Port = 8080,
    [switch]$OpenFirewall
)
$ErrorActionPreference = 'Stop'

$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw 'Run this in an elevated PowerShell (Run as administrator).'
}

$source = Split-Path -Parent $PSScriptRoot
if (-not (Test-Path (Join-Path $source 'punchlist.exe'))) {
    throw 'Run this from the unpacked download (no punchlist.exe next to it).'
}
$name = 'Punchlist'
$program = Join-Path $env:ProgramFiles $name
$data = Join-Path $env:ProgramData $name
$config = Join-Path $data 'punchlist.conf'
$database = Join-Path $data 'punchlist.db'
$exe = Join-Path $program 'punchlist.exe'

$existing = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($existing -and $existing.Status -ne 'Stopped') {
    Write-Host 'Stopping the running service for the upgrade...'
    Stop-Service -Name $name
    $existing.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30))
}

New-Item -ItemType Directory -Force -Path $data | Out-Null
if (Test-Path $program) { Remove-Item -Recurse -Force $program }
Copy-Item -Recurse -Path $source -Destination $program
if (-not (Test-Path $config)) {
    Copy-Item (Join-Path $PSScriptRoot 'punchlist.conf') $config
}

if (-not $existing) {
    New-Service -Name $name -BinaryPathName ('"' + $exe + '"') -DisplayName 'Punchlist' `
        -Description 'Punchlist tasks and calendar' -StartupType Automatic | Out-Null
    sc.exe config $name obj= "NT SERVICE\$name" | Out-Null
    # Restart after a crash: after 5 seconds, again, then every minute.
    sc.exe failure $name reset= 86400 actions= restart/5000/restart/5000/restart/60000 | Out-Null
}
icacls $data /grant "NT SERVICE\${name}:(OI)(CI)M" /T /Q | Out-Null

# The service's own environment: the port, the database and the settings file.
$environment = @(
    "ASPNETCORE_URLS=http://+:$Port",
    "DB_PATH=$database",
    "CONFIG_FILE=$config"
)
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$name" -Name Environment `
    -Type MultiString -Value $environment

if ($OpenFirewall -and -not (Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue)) {
    New-NetFirewallRule -DisplayName $name -Direction Inbound -Protocol TCP -LocalPort $Port `
        -Action Allow -Profile Private | Out-Null
}

Start-Service -Name $name
& $exe --version

# Wait until it answers (the first start creates the database and the admin account).
$healthy = $false
foreach ($attempt in 1..60) {
    try {
        Invoke-WebRequest -UseBasicParsing "http://localhost:$Port/healthz" -TimeoutSec 2 | Out-Null
        $healthy = $true
        break
    } catch { Start-Sleep -Seconds 1 }
}
if (-not $healthy) {
    throw "The service started but does not answer on port $Port. See Event Viewer > Windows Logs > Application."
}

if ($existing) {
    Write-Host 'Upgraded and running.'
} else {
    # A service has no console for the first-start password, so set one here instead.
    $env:DB_PATH = $database
    $env:CONFIG_FILE = $config
    & $exe admin reset-password admin
    Write-Host "Running on http://localhost:$Port - sign in as admin with the password above."
}
Write-Host "Settings: $config (then: Restart-Service $name)"

Day to day

Get-Service Punchlist
Restart-Service Punchlist          # after editing C:\ProgramData\Punchlist\punchlist.conf
Get-Content C:\ProgramData\Punchlist\logs\*.log -Tail 50   # log files (JSON lines)

Start, stop and crash messages are also in Event Viewer > Windows Logs > Application.

Run CLI commands with the service's settings, in an elevated PowerShell:

$env:DB_PATH = 'C:\ProgramData\Punchlist\punchlist.db'
$env:CONFIG_FILE = 'C:\ProgramData\Punchlist\punchlist.conf'
& 'C:\Program Files\Punchlist\punchlist.exe' snapshot
& 'C:\Program Files\Punchlist\punchlist.exe' admin reset-password

Upgrade

Run the newer download's install-service.ps1 the same way: it stops the service, replaces the program and starts it again. Read upgrading first.

Run it by hand

cd punchlist-win-x64
$env:ASPNETCORE_HTTP_PORTS = '8080'
.\punchlist.exe

The first admin password is printed in that window. Data goes to .\data.

HTTPS

Put a reverse proxy in front (Caddy for Windows is the simplest, see reverse proxy and HTTPS), or give the server a certificate itself:

# In the service's environment (registry) or your session:
ASPNETCORE_URLS=https://+:8443
ASPNETCORE_Kestrel__Certificates__Default__Path=C:\ProgramData\Punchlist\cert.pfx
ASPNETCORE_Kestrel__Certificates__Default__Password=...

Uninstall

& 'C:\Program Files\Punchlist\examples\uninstall-service.ps1'              # keeps your data
& 'C:\Program Files\Punchlist\examples\uninstall-service.ps1' -RemoveData  # removes it too