Version 1.0.0 · Apache 2.0 · Copyright 2026 Job Done Marketing LLC
Six files, 321 lines total. No dependencies beyond the Go standard library. Build with:
go build -ldflags="-s -w" -o serverwake-free .
Requires Go 1.25 or newer. The complete source follows, one file per section, delimited by BEGIN/END markers.
===== BEGIN version.go =====
package main
import "fmt"
// version is the current release version of Serverwake Free.
// Bump this on every release.
const version = "1.0.0"
// printVersion writes the version string to stdout.
func printVersion() {
fmt.Printf("serverwake-free v%s\n", version)
}
===== END version.go =====
===== BEGIN log.go =====
package main
import (
"fmt"
"os"
"time"
)
// logFilePath is where Serverwake Free writes its intervention log.
// The file is pre-created at install time with owner admin:admin.
// The process never creates it; if it is missing, interventions are
// reported to stderr only.
const logFilePath = "/var/log/serverwake-free.log"
// timestamp returns the current time in ISO-8601 UTC format.
func timestamp() string {
return time.Now().UTC().Format("2006-01-02T15:04:05Z")
}
// logInfo writes a timestamped line to stdout.
func logInfo(msg string) {
fmt.Printf("%s %s\n", timestamp(), msg)
}
// logFile appends a timestamped line to the log file.
//
// The file is opened with O_APPEND|O_WRONLY only — no O_CREATE. If
// the file does not exist, the append fails and the error is written
// to stderr. This guarantees the log's ownership never flips from
// admin:admin to root:root at runtime.
func logFile(msg string) {
f, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "%s cannot append to log: %v\n", timestamp(), err)
return
}
defer f.Close()
if _, err := fmt.Fprintf(f, "%s %s\n", timestamp(), msg); err != nil {
fmt.Fprintf(os.Stderr, "%s failed to write to log: %v\n", timestamp(), err)
}
}
// logBoth writes a timestamped line to both stdout and the log file.
// Used for every intervention.
func logBoth(msg string) {
logInfo(msg)
logFile(msg)
}
===== END log.go =====
===== BEGIN systemd.go =====
package main
import (
"os/exec"
"path/filepath"
"strings"
)
// isFailed reports whether a systemd service is in the failed state.
//
// This is the only condition that triggers a restart. A service that
// was deliberately stopped (systemctl stop) is "inactive", not
// "failed", and Serverwake must leave it alone. A service that
// crashed or gave up after retries is "failed", and Serverwake
// restarts it.
func isFailed(name string) bool {
err := exec.Command("systemctl", "is-failed", "--quiet", name).Run()
return err == nil
}
// isActive reports whether a systemd service is currently active.
// Used only to verify a restart succeeded.
func isActive(name string) bool {
err := exec.Command("systemctl", "is-active", "--quiet", name).Run()
return err == nil
}
// restart asks systemd to restart a service.
func restart(name string) error {
return exec.Command("systemctl", "restart", name).Run()
}
// listAllServiceUnits returns the names (without .service suffix) of
// all service unit files known to systemd on this host.
func listAllServiceUnits() ([]string, error) {
out, err := exec.Command("systemctl", "list-unit-files",
"--type=service",
"--no-legend",
"--no-pager",
).Output()
if err != nil {
return nil, err
}
var units []string
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
name := strings.TrimSuffix(fields[0], ".service")
units = append(units, name)
}
return units, nil
}
// listMatchingUnits returns the names of service units matching a
// pattern. The pattern may be an exact name (e.g. "nginx") or a shell
// glob (e.g. "php*-fpm"). Matching is done in Go with filepath.Match.
func listMatchingUnits(pattern string) ([]string, error) {
all, err := listAllServiceUnits()
if err != nil {
return nil, err
}
var matches []string
for _, name := range all {
ok, err := filepath.Match(pattern, name)
if err != nil || !ok {
continue
}
matches = append(matches, name)
}
return matches, nil
}
===== END systemd.go =====
===== BEGIN catalogue.go =====
package main
// catalogue is the built-in list of delivery-layer services that
// Serverwake Free knows how to watch. Entries may be exact unit names
// or glob patterns (e.g. "php*-fpm"). At startup, each entry is
// resolved against systemd, and only present services are watched.
//
// This list deliberately excludes OS plumbing (systemd-*, dbus, cron,
// rsyslog, getty, polkit, ssh) and hardware/system management
// (ModemManager, multipathd, udisks2, upower). Serverwake watches the
// services that deliver the product, not the services that keep the
// operating system alive.
var catalogue = []string{
// Web servers
"nginx",
"apache2",
// Application runtimes
"php*-fpm",
"gunicorn",
"uwsgi",
// Caches and data stores
"redis-server",
"memcached",
"mariadb",
"mysql",
"postgresql@*-main",
// Containers
"docker",
// Security and integrity
"clamav-daemon",
"fail2ban",
// File transfer
"pure-ftpd",
// Process supervision
"supervisor",
}
// resolveCatalogue expands the built-in catalogue against the local
// systemd instance and returns the list of concrete unit names that
// are present. Entries that match nothing are silently skipped.
// Aliases are treated as distinct services.
func resolveCatalogue() []string {
var resolved []string
for _, entry := range catalogue {
matches, err := listMatchingUnits(entry)
if err != nil || len(matches) == 0 {
continue
}
resolved = append(resolved, matches...)
}
return resolved
}
===== END catalogue.go =====
===== BEGIN watch.go =====
package main
import (
"os"
"os/signal"
"strconv"
"syscall"
"time"
)
// checkInterval is how often Serverwake Free polls the watched
// services. Fixed at 60 seconds in the free version.
const checkInterval = 60 * time.Second
// restartGracePeriod is how long to wait after issuing a restart
// before re-checking whether the service came back up.
const restartGracePeriod = 3 * time.Second
// watchLoop resolves the catalogue once, then checks each service on
// every tick of the interval. On SIGHUP, it re-resolves the catalogue
// and logs the new watch list. It never returns.
//
// The sysadmin sends SIGHUP with:
//
// sudo systemctl reload serverwake-free
//
// This is required after installing or removing a service, so that
// Serverwake picks up the change.
func watchLoop() {
services := resolveCatalogue()
reload := make(chan struct{}, 1)
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGHUP)
go func() {
for range sigc {
select {
case reload <- struct{}{}:
default:
}
}
}()
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
for _, name := range services {
checkOne(name)
}
case <-reload:
services = resolveCatalogue()
logBoth("catalogue reloaded; watching " +
strconv.Itoa(len(services)) + " services")
}
}
}
// checkOne evaluates a single service. Only services in the failed
// state are restarted. Services that are active, inactive (deliberately
// stopped), activating, deactivating, or masked are left alone.
// Only interventions are logged.
func checkOne(name string) {
if !isFailed(name) {
return
}
if err := restart(name); err != nil {
logBoth(name + " restart command failed: " + err.Error())
return
}
time.Sleep(restartGracePeriod)
if isActive(name) {
logBoth(name + " was down; restarted successfully")
} else {
logBoth(name + " still down after restart")
}
}
===== END watch.go =====
===== BEGIN main.go =====
package main
import (
"fmt"
"os"
)
// usage prints the minimal CLI help.
func usage() {
fmt.Println("serverwake-free — a minimal service watchdog")
fmt.Println()
fmt.Println("Usage: serverwake-free [--version | --daemon]")
}
// runDaemon enters the watch loop. It never returns.
func runDaemon() {
watchLoop()
}
// main is the entry point.
func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
case "--version", "-v":
printVersion()
return
case "--daemon":
runDaemon()
return
default:
fmt.Fprintf(os.Stderr, "unknown flag: %s\n", os.Args[1])
usage()
os.Exit(1)
}
}
usage()
os.Exit(1)
}
===== END main.go =====
===== BEGIN go.mod =====
module serverwake-free
go 1.25
===== END go.mod =====
go.mod.
No dependencies beyond the standard library. Build with
go build -ldflags="-s -w" -o serverwake-free .