Repository navigation
Support native Windows services and systemd lifecycle notifications #1503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lovasoa
wants to merge
4
commits into
main
Choose a base branch
from
codex/native-service-lifecycle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,214
−52
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
50ce0a4
Support native Windows services and systemd lifecycle notifications
lovasoa 130967e
Fix Windows event log SID pointer type
lovasoa d6da4c6
Keep Windows validation scoped to service integration
lovasoa e705907
Avoid server specialization and queue Windows service logs
lovasoa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 3 additions & 2 deletions
5
examples/official-site/sqlpage/migrations/66_log_component.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
examples/official-site/your-first-sql-website/service.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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