mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-18 16:17:26 +01:00
Merge pull request #1552 from binwiederhier/windows-server
Support "ntfy serve" on Windows
This commit is contained in:
@@ -48,13 +48,15 @@ builds:
|
|||||||
- id: ntfy_windows_amd64
|
- id: ntfy_windows_amd64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
tags: [ noserver ] # don't include server files
|
- CC=x86_64-w64-mingw32-gcc # apt install gcc-mingw-w64-x86-64
|
||||||
|
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||||
ldflags:
|
ldflags:
|
||||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [ windows ]
|
goos: [ windows ]
|
||||||
goarch: [ amd64 ]
|
goarch: [amd64 ]
|
||||||
- id: ntfy_darwin_all
|
-
|
||||||
|
id: ntfy_darwin_all
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||||
@@ -201,4 +203,4 @@ docker_manifests:
|
|||||||
- *amd64_image
|
- *amd64_image
|
||||||
- *arm64v8_image
|
- *arm64v8_image
|
||||||
- *armv7_image
|
- *armv7_image
|
||||||
- *armv6_image
|
- *armv6_image
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -31,6 +31,7 @@ help:
|
|||||||
@echo "Build server & client (without GoReleaser):"
|
@echo "Build server & client (without GoReleaser):"
|
||||||
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
||||||
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
||||||
|
@echo " make cli-windows-server - Build client & server (no GoReleaser, amd64 only, Windows)"
|
||||||
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build dev Docker:"
|
@echo "Build dev Docker:"
|
||||||
@@ -106,6 +107,7 @@ build-deps-ubuntu:
|
|||||||
curl \
|
curl \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
|
gcc-mingw-w64-x86-64 \
|
||||||
python3 \
|
python3 \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
jq
|
jq
|
||||||
@@ -201,6 +203,16 @@ cli-darwin-server: cli-deps-static-sites
|
|||||||
-ldflags \
|
-ldflags \
|
||||||
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
|
cli-windows-server: cli-deps-static-sites
|
||||||
|
# This is a target to build the CLI (including the server) for Windows.
|
||||||
|
# Use this for Windows development, if you really don't want to install GoReleaser ...
|
||||||
|
mkdir -p dist/ntfy_windows_server server/docs
|
||||||
|
CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build \
|
||||||
|
-o dist/ntfy_windows_server/ntfy.exe \
|
||||||
|
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||||
|
-ldflags \
|
||||||
|
"-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
cli-client: cli-deps-static-sites
|
cli-client: cli-deps-static-sites
|
||||||
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
||||||
# Use this for development, if you really don't want to install GoReleaser ...
|
# Use this for development, if you really don't want to install GoReleaser ...
|
||||||
@@ -213,7 +225,7 @@ cli-client: cli-deps-static-sites
|
|||||||
|
|
||||||
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
||||||
|
|
||||||
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64
|
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 cli-deps-gcc-windows
|
||||||
|
|
||||||
cli-deps-static-sites:
|
cli-deps-static-sites:
|
||||||
mkdir -p server/docs server/site
|
mkdir -p server/docs server/site
|
||||||
@@ -228,6 +240,9 @@ cli-deps-gcc-armv6-armv7:
|
|||||||
cli-deps-gcc-arm64:
|
cli-deps-gcc-arm64:
|
||||||
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
||||||
|
|
||||||
|
cli-deps-gcc-windows:
|
||||||
|
which x86_64-w64-mingw32-gcc || { echo "ERROR: Windows cross compiler not installed. On Ubuntu, run: apt install gcc-mingw-w64-x86-64"; exit 1; }
|
||||||
|
|
||||||
cli-deps-update:
|
cli-deps-update:
|
||||||
go get -u
|
go get -u
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const (
|
|||||||
DefaultBaseURL = "https://ntfy.sh"
|
DefaultBaseURL = "https://ntfy.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultConfigFile is the default path to the client config file (set in config_*.go)
|
||||||
|
var DefaultConfigFile string
|
||||||
|
|
||||||
// Config is the config struct for a Client
|
// Config is the config struct for a Client
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultHost string `yaml:"default-host"`
|
DefaultHost string `yaml:"default-host"`
|
||||||
|
|||||||
18
client/config_darwin.go
Normal file
18
client/config_darwin.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err == nil && u.Uid == "0" {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||||
|
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
18
client/config_unix.go
Normal file
18
client/config_unix.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err == nil && u.Uid == "0" {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||||
|
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
client/config_windows.go
Normal file
14
client/config_windows.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
52
cmd/serve.go
52
cmd/serve.go
@@ -10,10 +10,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"runtime"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -350,6 +348,8 @@ func execServe(c *cli.Context) error {
|
|||||||
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
|
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
|
||||||
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
|
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
|
||||||
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
|
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
|
||||||
|
} else if runtime.GOOS == "windows" && listenUnix != "" {
|
||||||
|
return errors.New("listen-unix is not supported on Windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
@@ -503,6 +503,14 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
||||||
conf.Version = c.App.Version
|
conf.Version = c.App.Version
|
||||||
|
|
||||||
|
// Check if we should run as a Windows service
|
||||||
|
if ranAsService, err := maybeRunAsService(conf); err != nil {
|
||||||
|
log.Fatal("%s", err.Error())
|
||||||
|
} else if ranAsService {
|
||||||
|
log.Info("Exiting.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Set up hot-reloading of config
|
// Set up hot-reloading of config
|
||||||
go sigHandlerConfigReload(config)
|
go sigHandlerConfigReload(config)
|
||||||
|
|
||||||
@@ -517,22 +525,6 @@ func execServe(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sigHandlerConfigReload(config string) {
|
|
||||||
sigs := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigs, syscall.SIGHUP)
|
|
||||||
for range sigs {
|
|
||||||
log.Info("Partially hot reloading configuration ...")
|
|
||||||
inputSource, err := newYamlSourceFromFile(config, flagsServe)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Hot reload failed: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := reloadLogLevel(inputSource); err != nil {
|
|
||||||
log.Warn("Reloading log level failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
||||||
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
|
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
|
||||||
prefix, err := netip.ParsePrefix(host)
|
prefix, err := netip.ParsePrefix(host)
|
||||||
@@ -663,25 +655,3 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok
|
|||||||
}
|
}
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
|
||||||
newLevelStr, err := inputSource.String("log-level")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level: %s", err.Error())
|
|
||||||
}
|
|
||||||
overrides, err := inputSource.StringSlice("log-level-overrides")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level overrides (1): %s", err.Error())
|
|
||||||
}
|
|
||||||
log.ResetLevelOverrides()
|
|
||||||
if err := applyLogLevelOverrides(overrides); err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level overrides (2): %s", err.Error())
|
|
||||||
}
|
|
||||||
log.SetLevel(log.ToLevel(newLevelStr))
|
|
||||||
if len(overrides) > 0 {
|
|
||||||
log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides))
|
|
||||||
} else {
|
|
||||||
log.Info("Log level is %v", strings.ToUpper(newLevelStr))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
55
cmd/serve_unix.go
Normal file
55
cmd/serve_unix.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/v2/log"
|
||||||
|
"heckel.io/ntfy/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sigHandlerConfigReload(config string) {
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGHUP)
|
||||||
|
for range sigs {
|
||||||
|
log.Info("Partially hot reloading configuration ...")
|
||||||
|
inputSource, err := newYamlSourceFromFile(config, flagsServe)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Hot reload failed: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := reloadLogLevel(inputSource); err != nil {
|
||||||
|
log.Warn("Reloading log level failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
||||||
|
newLevelStr, err := inputSource.String("log-level")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
overrides, err := inputSource.StringSlice("log-level-overrides")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.ResetLevelOverrides()
|
||||||
|
if err := applyLogLevelOverrides(overrides); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.SetLevel(log.ToLevel(newLevelStr))
|
||||||
|
if len(overrides) > 0 {
|
||||||
|
log.Info("Log level is %v, %d override(s) in place", newLevelStr, len(overrides))
|
||||||
|
} else {
|
||||||
|
log.Info("Log level is %v", newLevelStr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
100
cmd/serve_windows.go
Normal file
100
cmd/serve_windows.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//go:build windows && !noserver
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"heckel.io/ntfy/v2/log"
|
||||||
|
"heckel.io/ntfy/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const serviceName = "ntfy"
|
||||||
|
|
||||||
|
// sigHandlerConfigReload is a no-op on Windows since SIGHUP is not available.
|
||||||
|
// Windows users can restart the service to reload configuration.
|
||||||
|
func sigHandlerConfigReload(config string) {
|
||||||
|
log.Debug("Config hot-reload via SIGHUP is not supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runAsWindowsService runs the ntfy server as a Windows service
|
||||||
|
func runAsWindowsService(conf *server.Config) error {
|
||||||
|
return svc.Run(serviceName, &windowsService{conf: conf})
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsService implements the svc.Handler interface
|
||||||
|
type windowsService struct {
|
||||||
|
conf *server.Config
|
||||||
|
server *server.Server
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute is the main entry point for the Windows service
|
||||||
|
func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||||
|
status <- svc.Status{State: svc.StartPending}
|
||||||
|
|
||||||
|
// Create and start the server
|
||||||
|
var err error
|
||||||
|
s.mu.Lock()
|
||||||
|
s.server, err = server.New(s.conf)
|
||||||
|
s.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create server: %s", err.Error())
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server in a goroutine
|
||||||
|
serverErrChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
serverErrChan <- s.server.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
log.Info("Windows service started")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-serverErrChan:
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Server error: %s", err.Error())
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
case req := <-requests:
|
||||||
|
switch req.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
status <- req.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
log.Info("Windows service stopping...")
|
||||||
|
status <- svc.Status{State: svc.StopPending}
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.server != nil {
|
||||||
|
s.server.Stop()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
return false, 0
|
||||||
|
default:
|
||||||
|
log.Warn("Unexpected service control request: %d", req.Cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeRunAsService checks if the process is running as a Windows service,
|
||||||
|
// and if so, runs the server as a service. Returns true if it ran as a service.
|
||||||
|
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||||
|
isService, err := svc.IsWindowsService()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to detect Windows service mode: %w", err)
|
||||||
|
} else if !isService {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
log.Info("Running as Windows service")
|
||||||
|
if err := runAsWindowsService(conf); err != nil {
|
||||||
|
return true, fmt.Errorf("failed to run as Windows service: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -3,28 +3,21 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/v2/client"
|
"heckel.io/ntfy/v2/client"
|
||||||
"heckel.io/ntfy/v2/log"
|
"heckel.io/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/v2/util"
|
"heckel.io/ntfy/v2/util"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commands = append(commands, cmdSubscribe)
|
commands = append(commands, cmdSubscribe)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
|
|
||||||
clientUserConfigFileUnixRelative = "ntfy/client.yml"
|
|
||||||
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagsSubscribe = append(
|
var flagsSubscribe = append(
|
||||||
append([]cli.Flag{}, flagsDefault...),
|
append([]cli.Flag{}, flagsDefault...),
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
||||||
@@ -310,45 +303,16 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
|
|||||||
if filename != "" {
|
if filename != "" {
|
||||||
return client.LoadConfig(filename)
|
return client.LoadConfig(filename)
|
||||||
}
|
}
|
||||||
configFile, err := defaultClientConfigFile()
|
if client.DefaultConfigFile != "" {
|
||||||
if err != nil {
|
if s, _ := os.Stat(client.DefaultConfigFile); s != nil {
|
||||||
log.Warn("Could not determine default client config file: %s", err.Error())
|
return client.LoadConfig(client.DefaultConfigFile)
|
||||||
} else {
|
|
||||||
if s, _ := os.Stat(configFile); s != nil {
|
|
||||||
return client.LoadConfig(configFile)
|
|
||||||
}
|
}
|
||||||
log.Debug("Config file %s not found", configFile)
|
log.Debug("Config file %s not found", client.DefaultConfigFile)
|
||||||
}
|
}
|
||||||
log.Debug("Loading default config")
|
log.Debug("Loading default config")
|
||||||
return client.NewConfig(), nil
|
return client.NewConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 Conditionally used in different builds
|
|
||||||
func defaultClientConfigFileUnix() (string, error) {
|
|
||||||
u, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine current user: %w", err)
|
|
||||||
}
|
|
||||||
configFile := clientRootConfigFileUnixAbsolute
|
|
||||||
if u.Uid != "0" {
|
|
||||||
homeDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine user config dir: %w", err)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil
|
|
||||||
}
|
|
||||||
return configFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//lint:ignore U1000 Conditionally used in different builds
|
|
||||||
func defaultClientConfigFileWindows() (string, error) {
|
|
||||||
homeDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine user config dir: %w", err)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logMessagePrefix(m *client.Message) string {
|
func logMessagePrefix(m *client.Message) string {
|
||||||
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
|
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -10,7 +12,3 @@ or "~/Library/Application Support/ntfy/client.yml" for all other users.`
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"sh", "-c"}
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileUnix()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,3 @@ or ~/.config/ntfy/client.yml for all other users.`
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"sh", "-c"}
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileUnix()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -9,7 +11,3 @@ const (
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileWindows()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -228,19 +228,29 @@ brew install ntfy
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
||||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
To install, you can either
|
||||||
|
|
||||||
|
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
||||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
|
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
||||||
|
|
||||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
Once installed, you can run the ntfy CLI commands like so:
|
||||||
|
|
||||||
Also available in [Scoop's](https://scoop.sh) Main repository:
|
```
|
||||||
|
ntfy.exe -h
|
||||||
|
```
|
||||||
|
|
||||||
`scoop install ntfy`
|
The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`)
|
||||||
|
for the server, and `%AppData%\ntfy\client.yml` for the client. You may need to create the directory and config file manually.
|
||||||
|
|
||||||
!!! info
|
To install the ntfy server as a Windows service, you can use the built-in `sc` command. For example, run this in an
|
||||||
There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a
|
elevated command prompt (adjust the path to `ntfy.exe` accordingly):
|
||||||
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
|
|
||||||
|
```
|
||||||
|
sc create ntfy binPath="C:\path\to\ntfy.exe serve" start=auto
|
||||||
|
sc start ntfy
|
||||||
|
```
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
||||||
|
|||||||
@@ -1605,7 +1605,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
||||||
* Support for a [custom Twilio call format](config.md#phone-calls) ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
||||||
|
* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328), thanks to [@wtf911](https://github.com/wtf911))
|
||||||
|
|
||||||
### ntfy Android app v1.22.x (UNRELEASED)
|
### ntfy Android app v1.22.x (UNRELEASED)
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -35,6 +35,7 @@ require (
|
|||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0
|
github.com/stripe/stripe-go/v74 v74.30.0
|
||||||
|
golang.org/x/sys v0.40.0
|
||||||
golang.org/x/text v0.33.0
|
golang.org/x/text v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +94,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
|
||||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect
|
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import (
|
|||||||
// Defines default config settings (excluding limits, see below)
|
// Defines default config settings (excluding limits, see below)
|
||||||
const (
|
const (
|
||||||
DefaultListenHTTP = ":80"
|
DefaultListenHTTP = ":80"
|
||||||
DefaultConfigFile = "/etc/ntfy/server.yml"
|
|
||||||
DefaultTemplateDir = "/etc/ntfy/templates"
|
|
||||||
DefaultCacheDuration = 12 * time.Hour
|
DefaultCacheDuration = 12 * time.Hour
|
||||||
DefaultCacheBatchTimeout = time.Duration(0)
|
DefaultCacheBatchTimeout = time.Duration(0)
|
||||||
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
||||||
@@ -27,6 +25,12 @@ const (
|
|||||||
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Platform-specific default paths (set in config_unix.go or config_windows.go)
|
||||||
|
var (
|
||||||
|
DefaultConfigFile string
|
||||||
|
DefaultTemplateDir string
|
||||||
|
)
|
||||||
|
|
||||||
// Defines default Web Push settings
|
// Defines default Web Push settings
|
||||||
const (
|
const (
|
||||||
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
|
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
|
||||||
|
|||||||
8
server/config_unix.go
Normal file
8
server/config_unix.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/server.yml"
|
||||||
|
DefaultTemplateDir = "/etc/ntfy/templates"
|
||||||
|
}
|
||||||
17
server/config_windows.go
Normal file
17
server/config_windows.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
programData := os.Getenv("ProgramData")
|
||||||
|
if programData == "" {
|
||||||
|
programData = `C:\ProgramData`
|
||||||
|
}
|
||||||
|
DefaultConfigFile = filepath.Join(programData, "ntfy", "server.yml")
|
||||||
|
DefaultTemplateDir = filepath.Join(programData, "ntfy", "templates")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user