Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ jobs:
run: |
mkdir -p target/sqlpage-test-binaries
tar -xzf target/sqlpage-linux-test-binaries.tar.gz -C target/sqlpage-test-binaries
- name: Download server for service lifecycle tests
uses: actions/download-artifact@v8
with:
name: sqlpage-linux-debug
path: target/debug
- run: chmod +x target/debug/sqlpage
- name: Install DuckDB ODBC driver
if: matrix.database == 'duckdb'
run: |
Expand Down Expand Up @@ -146,6 +152,7 @@ jobs:
DATABASE_URL: ${{ matrix.db_url }}
MALLOC_CHECK_: 3
MALLOC_PERTURB_: 10
SQLPAGE_BINARY: ${{ github.workspace }}/target/debug/sqlpage

windows_test:
runs-on: windows-latest
Expand Down Expand Up @@ -173,6 +180,9 @@ jobs:
env:
CARGO_INCREMENTAL: 1
RUST_BACKTRACE: 1
- name: Test native Windows service lifecycle
shell: powershell
run: scripts/test-windows-service.ps1
- name: Upload Windows binary
uses: actions/upload-artifact@v7
with:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG.md

## v0.47.0 (unreleased)
- SQLPage can now run as a native Windows service and report readiness to systemd. Service stops drain active requests, close database connections, and flush telemetry; Windows service logs appear in Event Viewer.
- **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements.
- Updated sqlx-oldapi to v0.6.57 to fix SQL Server fallback expressions such as `ISNULL($missing, 'default')` truncating defaults or failing for date values when the bound variable is `NULL`.
- Fixed MSSQL `JSON_OBJECT('key': value)` expressions being rejected by SQLPage's parser, including when used in `SET` statements or nested in `sqlpage.*` function calls.
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ handlebars = "6.2.0"
log = "0.4.17"
mime_guess = "2.0.4"
futures-util = "0.3.21"
tokio = { version = "1.24.1", features = ["macros", "rt", "process", "sync"] }
tokio = { version = "1.24.1", features = ["macros", "rt", "process", "sync", "signal"] }
tokio-stream = "0.1.9"
anyhow = "1"
serde = "1"
Expand Down Expand Up @@ -133,6 +133,13 @@ opentelemetry-http = { version = "0.32", default-features = false }
opentelemetry-semantic-conventions = { version = "0.32", features = ["semconv_experimental"] }


[target.'cfg(target_os = "linux")'.dependencies]
sd-notify = "0.4"

[target.'cfg(windows)'.dependencies]
windows-service = "0.8"
windows-sys = { version = "0.61", features = ["Win32_System_EventLog", "Win32_Security"] }

[features]
default = []
odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"]
Expand All @@ -148,4 +155,3 @@ lambda-web = [
actix-http = "3"
tempfile = "3"
tokio = { version = "1", features = ["rt", "time", "test-util"] }

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ a cheaper ARM cloud instance, using the docker image is the easiest way to do it

For managed SQLPage hosting, use [DataPage](https://datapage.app). To run SQLPage yourself on a VPS, [Hostinger](https://www.hostg.xyz/aff_c?offer_id=815&aff_id=243720&url_id=6808) is another option; this is an affiliate link, so we receive a small commission if you buy through it.

To start automatically at boot, see [running SQLPage as a Windows or systemd service](examples/official-site/your-first-sql-website/service.md), including installation, logs, and graceful shutdown.

### On macOS, with Homebrew

[SQLPage's Homebrew package](https://formulae.brew.sh/formula/sqlpage) is the recommended way to install SQLPage on macOS, including Intel Macs.
Expand Down
6 changes: 6 additions & 0 deletions configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ SQLPage can be configured through either [environment variables](https://en.wiki
or a [JSON](https://en.wikipedia.org/wiki/JSON) file placed in `sqlpage/sqlpage.json`.

You can find an example configuration file in [`sqlpage/sqlpage.json`](./sqlpage/sqlpage.json).
For automatic startup, service accounts, logging, and shutdown behavior, see
[running SQLPage as a Windows or systemd service](examples/official-site/your-first-sql-website/service.md).
Windows service mode (`--service NAME`) requires an absolute `--web-root`, which also
sets the working directory before loading `.env` and configuration files. Under
systemd, set `WorkingDirectory` in the unit file.

Here are the available configuration options and their default values:

| variable | default | description |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
INSERT INTO component(name, icon, introduced_in_version, description) VALUES
('log', 'logs', '0.37.1', 'A component that writes messages to the server logs.
When a page runs, it prints your message to the terminal/console (standard error).
When a page runs, it writes your message to the server logs.
Use it to track what happens and troubleshoot issues.

### Where do the messages appear?

- Running from a terminal (Linux, macOS, or Windows PowerShell/Command Prompt): they show up in the window.
- Docker: run `docker logs <container_name>`.
- Linux service (systemd): run `journalctl -u sqlpage`.
- This component''s output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). SQLPage request access logs are separate and are written to standard output (stdout).
- Native Windows service (`--service NAME`): open Event Viewer → Windows Logs → Application and select the SQLPage source. Logs are queued in the background; sustained overload can drop records, and messages are limited to 16 KiB. See the [service setup guide](/your-first-sql-website/service.sql).
- Outside native Windows service mode, this component''s output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). SQLPage request access logs are separate and are written to standard output (stdout).
');

INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'log', * FROM (VALUES
Expand Down
109 changes: 109 additions & 0 deletions examples/official-site/your-first-sql-website/service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Run SQLPage as a service

SQLPage can start automatically at boot and run without an open terminal. It reports
readiness after connecting to the database, applying migrations, initializing the
application, and starting its HTTP listeners and workers. A startup failure is
reported to the service manager so it can apply the configured recovery policy.

When stopped, SQLPage stops accepting new connections and gives active HTTP requests
up to 30 seconds to finish, then closes database connections and flushes telemetry.
Allow extra time for database cleanup and telemetry when configuring service stop
timeouts. Requests that exceed the drain timeout can be interrupted.

## Linux with systemd

Install the Linux executable as `/usr/local/bin/sqlpage.bin`, create a dedicated
`sqlpage` user and group, and put your application in `/var/www/sqlpage`. The account
needs read access to the application and write access to its database, uploads, and
configuration directory as appropriate.

Download the [provided systemd unit](https://github.com/sqlpage/SQLPage/blob/main/sqlpage.service)
to `/etc/systemd/system/sqlpage.service`, adjusting `User`, `Group`, `WorkingDirectory`,
`ExecStart`, and `LISTEN_ON` to match your installation. The example listens on port
80 and grants only the capability needed to bind a privileged port. For port 8080,
change `LISTEN_ON` and remove `AmbientCapabilities`.

```sh
sudo systemctl daemon-reload
sudo systemctl enable --now sqlpage
sudo systemctl status sqlpage
journalctl -u sqlpage -f
```

The unit uses `Type=notify`: `systemctl start` waits for SQLPage's readiness
notification. Startup has a five-minute timeout to allow for migrations. Change
`TimeoutStartSec` if your deployment needs more time. `systemctl stop sqlpage` sends
SIGTERM, which initiates graceful shutdown. The example allows 60 seconds for the
whole stop operation and restarts the process on failure. SIGINT and SIGQUIT also
initiate graceful shutdown when running from a terminal or another supervisor.

Logs go to the journal. Use `sqlpage/sqlpage.json`, a `.env` file in the working
directory, or systemd `Environment`/`EnvironmentFile` settings for configuration.
SQLPage stays in the foreground; no PID file or daemonization is needed.

## Windows Service Control Manager

SQLPage supports native Windows services without a wrapper. Download `sqlpage.exe`,
then prepare an application folder such as `C:\SQLPage\website`, with its
configuration in `C:\SQLPage\website\sqlpage\sqlpage.json`.

In **Windows PowerShell run as Administrator**, register the event-log source and
create the service (adjust the paths):

```powershell
New-EventLog -LogName Application -Source SQLPage
$binary = 'C:\SQLPage\sqlpage.exe'
$root = 'C:\SQLPage\website'
$command = '"{0}" --service SQLPage --web-root "{1}"' -f $binary, $root
New-Service -Name SQLPage -BinaryPathName $command -StartupType Automatic `
-DisplayName 'SQLPage website'
```

Skip `New-EventLog` if the `SQLPage` source is already registered. It is shared by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New-EventLog need admin rights to apply

all SQLPage services. This command is available in Windows PowerShell 5.1.

Before starting, open `services.msc`, find **SQLPage website**, and set its **Log On**
account to a dedicated service account with access to your application, database,
and uploads. `New-Service` defaults to LocalSystem; choose an account with only the
permissions your application needs. Configure automatic recovery on the **Recovery**
tab if desired, including recovery for non-crash failures.

```powershell
Start-Service SQLPage
Get-Service SQLPage
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'SQLPage' } -MaxEvents 20
Restart-Service SQLPage
Stop-Service SQLPage
```

`--service NAME` must match the registered service name and requires an **absolute**
`--web-root`. In service mode this directory is also the working directory, so `.env`,
relative configuration paths, SQLite files, and uploads resolve there instead of
Windows' system directory. You may also pass `--config-dir` or `--config-file` in the
service command. Use distinct names and listening ports to run multiple services.

SQLPage reports `START_PENDING` during initialization, `RUNNING` once ready,
`STOP_PENDING` while draining requests, and `STOPPED` after cleanup. Startup progress
updates carry a five-minute wait hint. Both service-stop and operating-system
shutdown controls initiate cleanup. Failures report service-specific exit code 1;
details and application logs appear in **Event Viewer → Windows Logs → Application**
under the **SQLPage** source. Existing OpenTelemetry export remains available.

Event Viewer logging uses a background writer so Windows log writes do not block
HTTP workers. The queue holds up to 256 records, each truncated to 16 KiB at a UTF-8
character boundary. If the writer cannot keep up, new records are dropped and a
warning reports the number lost when the writer progresses or during shutdown.
Accepted records are flushed before the service reports `STOPPED`. Use `LOG_LEVEL`
or `RUST_LOG` to reduce log volume if needed.

To remove the service:

```powershell
Stop-Service SQLPage
sc.exe delete SQLPage
```

Running `sqlpage.exe` without `--service` continues to run in a terminal, with normal
console logging and graceful shutdown on Ctrl+C or Ctrl+Break. To diagnose a service
configuration, open a terminal in its web root and run the same command without
`--service NAME`.
2 changes: 2 additions & 0 deletions examples/official-site/your-first-sql-website/service.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT 'dynamic' AS component, properties FROM example WHERE component = 'shell' LIMIT 1;
SELECT 'text' AS component, sqlpage.read_file_as_text('your-first-sql-website/service.md') AS contents_md;
2 changes: 1 addition & 1 deletion examples/official-site/your-first-sql-website/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Alternatively, you can use a [Hostinger VPS](https://www.hostg.xyz/aff_c?offer_i
If you prefer to host your website yourself, you can use a cloud provider or a VPS provider. You will need to:
- Configure domain name resolution to point to your server
- Open the port you are using (8080 by default) in your server's firewall
- [Setup docker](https://github.com/sqlpage/SQLPage?tab=readme-ov-file#with-docker) or another process manager such as [systemd](https://github.com/sqlpage/SQLPage/blob/main/sqlpage.service) to start SQLPage automatically when your server boots and to keep it running
- [Setup docker](https://github.com/sqlpage/SQLPage?tab=readme-ov-file#with-docker) or [run SQLPage as a Windows or systemd service](service.sql) to start SQLPage automatically when your server boots and to keep it running
- Optionally, [setup a reverse proxy](nginx.sql) to avoid exposing SQLPage directly to the internet
- Optionally, setup a TLS certificate to enable HTTPS
- Configure connection to a cloud database or a database running on your server in [`sqlpage.json`](https://github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage)
Expand Down
88 changes: 88 additions & 0 deletions scripts/test-windows-service.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Run from an elevated Windows PowerShell prompt, or on the Windows CI runner.
param([string]$Binary = "$PSScriptRoot\..\target\debug\sqlpage.exe")
$ErrorActionPreference = 'Stop'
$Binary = (Resolve-Path $Binary).Path
$name = 'SQLPageTest' + [Guid]::NewGuid().ToString('N')
$root = Join-Path ([IO.Path]::GetTempPath()) ("SQLPage service test " + $name)
$created = $false
$listener = $null
$client = $null
$startedAt = Get-Date

function Wait-State([string]$state) {
$service = Get-Service $name
$service.WaitForStatus($state, [TimeSpan]::FromSeconds(60))
}

try {
New-Item -ItemType Directory -Path (Join-Path $root 'sqlpage') -Force | Out-Null
if (-not [Diagnostics.EventLog]::SourceExists('SQLPage')) {
New-EventLog -LogName Application -Source SQLPage
}
# Reserve a port to exercise startup failure before allowing a successful start.
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$listener.Start()
$port = $listener.LocalEndpoint.Port
$configuration = @{ listen_on = "127.0.0.1:$port"; database_url = 'sqlite::memory:' } |
ConvertTo-Json
[IO.File]::WriteAllText((Join-Path $root 'sqlpage\sqlpage.json'), $configuration)
[IO.File]::WriteAllText((Join-Path $root 'index.sql'), "SELECT 'log' AS component, '$name flush marker' AS message; SELECT 'text' AS component, 'service ready' AS contents;")
$command = '"{0}" --service {1} --web-root "{2}"' -f $Binary, $name, $root
New-Service -Name $name -BinaryPathName $command -StartupType Manual | Out-Null
$created = $true
$failed = $false
try { Start-Service $name } catch { $failed = $true }
if (-not $failed) { throw 'A port conflict must fail service startup' }
Wait-State 'Stopped'
$status = Get-CimInstance Win32_Service -Filter "Name='$name'"
if ($status.ServiceSpecificExitCode -ne 1) { throw 'Startup failure was not reported to SCM' }
$listener.Stop()

Start-Service $name
Wait-State 'Running'
$response = Invoke-WebRequest "http://127.0.0.1:$port/" -UseBasicParsing
if ($response.Content -notmatch 'service ready') { throw 'The service did not use its web root' }

# Exercise STOP while a real SQL request is waiting on an HTTP fetch.
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$listener.Start()
$upstreamPort = $listener.LocalEndpoint.Port
[IO.File]::WriteAllText((Join-Path $root 'slow.sql'), "SELECT 'text' AS component, sqlpage.fetch('http://127.0.0.1:$upstreamPort/') AS contents;")
Add-Type -AssemblyName System.Net.Http
$client = [Net.Http.HttpClient]::new()
$pendingResponse = $client.GetStringAsync("http://127.0.0.1:$port/slow.sql")
$accepted = $listener.AcceptTcpClientAsync()
if (-not $accepted.Wait(20000)) { throw 'SQL request did not reach the upstream server' }
$upstream = $accepted.Result
$stream = $upstream.GetStream()
$stream.ReadTimeout = 20000
$headers = New-Object byte[] 4096
if ($stream.Read($headers, 0, $headers.Length) -eq 0) { throw 'Missing upstream request' }
sc.exe stop $name | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'SCM rejected STOP' }
Wait-State 'StopPending'
$bytes = [Text.Encoding]::ASCII.GetBytes("HTTP/1.1 200 OK`r`nContent-Length: 16`r`nConnection: close`r`n`r`nrequest finished")
$stream.Write($bytes, 0, $bytes.Length)
$upstream.Dispose()
if (-not $pendingResponse.Wait(20000)) { throw 'Active request did not finish' }
if ($pendingResponse.Result -notmatch 'request finished') { throw 'Active response was truncated' }
Wait-State 'Stopped'
$status = Get-CimInstance Win32_Service -Filter "Name='$name'"
if ($status.ExitCode -ne 0) { throw 'Clean stop reported a failure' }
$events = Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'SQLPage'; StartTime = $startedAt }
$flushed = $events | Where-Object { $_.Properties[0].Value -like "*$name flush marker*" }
if (-not $flushed) { throw 'The service stopped before its queued log record was written' }
Start-Service $name
Wait-State 'Running'
Stop-Service $name
Wait-State 'Stopped'
Write-Host 'Windows service startup failure, readiness, graceful stop, log flush, and restart passed.'
} finally {
if ($client) { $client.Dispose() }
if ($listener) { $listener.Stop() }
if ($created) {
Stop-Service $name -ErrorAction SilentlyContinue
sc.exe delete $name | Out-Null
}
Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue
}
Loading
Loading