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/pecklist/-/releases/permalink/latest/downloads'
Invoke-WebRequest "$base/pecklist-win-x64.zip" -OutFile pecklist-win-x64.zip
Invoke-WebRequest "$base/SHA256SUMS" -OutFile SHA256SUMS
# The two hashes must match:
(Get-FileHash pecklist-win-x64.zip -Algorithm SHA256).Hash.ToLower()
Select-String 'win-x64' SHA256SUMS
Expand-Archive pecklist-win-x64.zip -DestinationPath .
& .\pecklist-win-x64\examples\install-service.ps1
The script installs and starts the PeckList 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\PeckList | The program | Replaced |
C:\ProgramData\PeckList\pecklist.conf | Your settings (settings file) | Kept |
C:\ProgramData\PeckList | Database, snapshots, logs | Kept |
The service runs as its own virtual account, NT SERVICE\PeckList, 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) PeckList as a Windows service.
.DESCRIPTION
Run from the unpacked download in an elevated PowerShell:
Expand-Archive pecklist-win-x64.zip -DestinationPath .
.\pecklist-win-x64\examples\install-service.ps1
Program: C:\Program Files\PeckList (replaced on every run)
Data: C:\ProgramData\PeckList (database, snapshots, logs; kept)
Settings: C:\ProgramData\PeckList\pecklist.conf (kept)
The service runs as its own virtual account, NT SERVICE\PeckList, 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.
An install from before the rename (the Punchlist service, C:\ProgramData\Punchlist) is moved
over first: the old service goes, and its data and settings move to the new folder.
.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 'pecklist.exe'))) {
throw 'Run this from the unpacked download (no pecklist.exe next to it).'
}
$name = 'PeckList'
$program = Join-Path $env:ProgramFiles $name
$data = Join-Path $env:ProgramData $name
$config = Join-Path $data 'pecklist.conf'
$database = Join-Path $data 'pecklist.db'
$exe = Join-Path $program 'pecklist.exe'
# --- Moving over from Punchlist, once ---------------------------------------------------------------
$old = Get-Service -Name 'Punchlist' -ErrorAction SilentlyContinue
$oldData = Join-Path $env:ProgramData 'Punchlist'
if (-not (Get-Service -Name $name -ErrorAction SilentlyContinue) -and ($old -or (Test-Path $oldData))) {
Write-Host 'Moving the Punchlist install over to PeckList...'
if ($old) {
if ($old.Status -ne 'Stopped') {
Stop-Service -Name 'Punchlist'
$old.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30))
}
sc.exe delete Punchlist | Out-Null
}
Get-NetFirewallRule -DisplayName 'Punchlist' -ErrorAction SilentlyContinue | Remove-NetFirewallRule
# Same drive, so the move is a rename: quick, whatever the size of the data.
if ((Test-Path $oldData) -and -not (Test-Path $data)) {
Move-Item $oldData $data
foreach ($suffix in '', '-wal', '-shm') {
$was = Join-Path $data "punchlist.db$suffix"
if (Test-Path $was) { Rename-Item $was "pecklist.db$suffix" }
}
$wasConfig = Join-Path $data 'punchlist.conf'
if (Test-Path $wasConfig) { Rename-Item $wasConfig 'pecklist.conf' }
}
$oldProgram = Join-Path $env:ProgramFiles 'Punchlist'
if (Test-Path $oldProgram) { Remove-Item -Recurse -Force $oldProgram }
}
$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 'pecklist.conf') $config
}
if (-not $existing) {
New-Service -Name $name -BinaryPathName ('"' + $exe + '"') -DisplayName 'PeckList' `
-Description 'PeckList 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 -or $old) {
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 PeckList
Restart-Service PeckList # after editing C:\ProgramData\PeckList\pecklist.conf
Get-Content C:\ProgramData\PeckList\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\PeckList\pecklist.db'
$env:CONFIG_FILE = 'C:\ProgramData\PeckList\pecklist.conf'
& 'C:\Program Files\PeckList\pecklist.exe' snapshot
& 'C:\Program Files\PeckList\pecklist.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 pecklist-win-x64
$env:ASPNETCORE_HTTP_PORTS = '8080'
.\pecklist.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\PeckList\cert.pfx
ASPNETCORE_Kestrel__Certificates__Default__Password=...
Uninstall
& 'C:\Program Files\PeckList\examples\uninstall-service.ps1' # keeps your data
& 'C:\Program Files\PeckList\examples\uninstall-service.ps1' -RemoveData # removes it too