feat: 建立 Sense Windows 交付包 (#70)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\backup-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,22 @@
|
||||
param(
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production',
|
||||
[string]$OutputDirectory = ''
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode -AllowOccupiedPort
|
||||
$pgDump = Get-SensePostgresTool -Name 'pg_dump'
|
||||
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) { $OutputDirectory = Join-Path $root 'backups' }
|
||||
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$output = Join-Path $OutputDirectory "sense-$($state.Database.Database)-$stamp.dump"
|
||||
Invoke-SensePostgresTool -Tool $pgDump -Database $state.Database -Arguments @('--dbname', $state.Database.Sanitized, '--format=custom', '--no-owner', '--file', $output)
|
||||
Write-Host "Sense backup created: $output"
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\check-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,19 @@
|
||||
param(
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production',
|
||||
[switch]$Running
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode -AllowOccupiedPort:$Running
|
||||
if ($Running -and -not (Test-SenseTcpEndpoint -HostName $state.Host -Port $state.Port)) {
|
||||
throw "Sense is not accepting TCP connections at $($state.Host):$($state.Port)."
|
||||
}
|
||||
Write-Host "Sense $Mode configuration check passed."
|
||||
Write-Host "PostgreSQL endpoint: reachable; MediaMTX mode: $($state.MediaMode); HTTP port: $($state.Port)."
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\initialize-admin.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,29 @@
|
||||
param(
|
||||
[string]$Username = '',
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production'
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
$passwordPointer = [IntPtr]::Zero
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode -AllowOccupiedPort
|
||||
$token = Get-SenseEnvironmentValue -Name 'SENSE_BOOTSTRAP_TOKEN'
|
||||
if ($token.Length -lt 32) { throw 'Set a temporary random SENSE_BOOTSTRAP_TOKEN of at least 32 characters, then restart Sense.' }
|
||||
if ([string]::IsNullOrWhiteSpace($Username)) { $Username = Read-Host 'Administrator username' }
|
||||
$securePassword = Read-Host 'Administrator password (at least 6 characters)' -AsSecureString
|
||||
$passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
|
||||
$password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($passwordPointer)
|
||||
$body = @{ username = $Username; password = $password; nickName = $Username } | ConvertTo-Json -Compress
|
||||
$headers = @{ 'X-Sense-Bootstrap-Token' = $token }
|
||||
$uri = "http://$($state.Host):$($state.Port)/api/v1/public/bootstrap"
|
||||
Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $body | Out-Null
|
||||
Write-Host 'Sense administrator created. Remove SENSE_BOOTSTRAP_TOKEN from config/sense.env and restart Sense now.'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
} finally {
|
||||
if ($passwordPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer) }
|
||||
Remove-Variable password -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\migrate-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,19 @@
|
||||
param([ValidateSet('production', 'demo')][string]$Mode = 'production')
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode -AllowOccupiedPort
|
||||
$sense = Join-Path $root 'sense.exe'
|
||||
Write-Host 'Applying pending Sense database migrations...'
|
||||
Push-Location $root
|
||||
try {
|
||||
& $sense migrate -c $state.SettingsPath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Sense database migration failed.' }
|
||||
} finally { Pop-Location }
|
||||
Write-Host 'Sense database migration completed.'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\restore-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,27 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$BackupFile,
|
||||
[Parameter(Mandatory = $true)][string]$ConfirmDatabaseName,
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production',
|
||||
[string]$Confirmation = ''
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode -AllowOccupiedPort
|
||||
$backup = [IO.Path]::GetFullPath($BackupFile)
|
||||
if (-not (Test-Path -LiteralPath $backup -PathType Leaf)) { throw "Backup file not found: $backup" }
|
||||
if ($ConfirmDatabaseName -cne $state.Database.Database) {
|
||||
throw 'Restore confirmation does not exactly match the configured database name.'
|
||||
}
|
||||
$pgRestore = Get-SensePostgresTool -Name 'pg_restore'
|
||||
Write-Warning "Restoring will replace objects in database '$ConfirmDatabaseName'. Stop Sense before continuing."
|
||||
$answer = if ([string]::IsNullOrWhiteSpace($Confirmation)) { Read-Host "Type RESTORE-$ConfirmDatabaseName to continue" } else { $Confirmation }
|
||||
if ($answer -cne "RESTORE-$ConfirmDatabaseName") { throw 'Restore cancelled.' }
|
||||
Invoke-SensePostgresTool -Tool $pgRestore -Database $state.Database -Arguments @('--dbname', $state.Database.Sanitized, '--clean', '--if-exists', '--no-owner', '--exit-on-error', $backup)
|
||||
Write-Host 'Sense database restore completed. Run migrate-sense.bat before starting Sense.'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$script:SenseAllowedEnvironment = @(
|
||||
'SENSE_MODE', 'SENSE_HOST', 'SENSE_PORT', 'SENSE_DATABASE_URL',
|
||||
'SENSE_DEMO_DATABASE_URL', 'SENSE_JWT_SECRET', 'SENSE_BOOTSTRAP_TOKEN',
|
||||
'SENSE_CREDENTIAL_KEY', 'SENSE_ONVIF_DISCOVERY_IP',
|
||||
'SENSE_ONVIF_ALLOWED_CIDRS', 'SENSE_MEDIAMTX_MODE',
|
||||
'SENSE_MEDIAMTX_BINARY', 'SENSE_MEDIAMTX_CONFIG',
|
||||
'SENSE_MEDIAMTX_API', 'SENSE_WEB_ROOT', 'SENSE_AUTO_MIGRATE',
|
||||
'SENSE_POSTGRES_BIN'
|
||||
)
|
||||
|
||||
function Get-SensePackageRoot {
|
||||
param([string]$ScriptDirectory = $PSScriptRoot)
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $ScriptDirectory '..\..'))
|
||||
}
|
||||
|
||||
function Import-SenseEnvironment {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "Sense configuration file not found: $Path"
|
||||
}
|
||||
$lineNumber = 0
|
||||
foreach ($rawLine in Get-Content -LiteralPath $Path -Encoding UTF8) {
|
||||
$lineNumber++
|
||||
$line = $rawLine.Trim()
|
||||
if ($line.Length -eq 0 -or $line.StartsWith('#')) { continue }
|
||||
$separator = $line.IndexOf('=')
|
||||
if ($separator -lt 1) {
|
||||
throw "Invalid Sense configuration at line $lineNumber. Expected NAME=value."
|
||||
}
|
||||
$name = $line.Substring(0, $separator).Trim()
|
||||
if ($script:SenseAllowedEnvironment -notcontains $name) {
|
||||
throw "Unsupported Sense configuration key at line ${lineNumber}: $name"
|
||||
}
|
||||
$value = $line.Substring($separator + 1)
|
||||
if ($value.Length -ge 2) {
|
||||
$first, $last = $value[0], $value[$value.Length - 1]
|
||||
if (($first -eq '"' -and $last -eq '"') -or ($first -eq "'" -and $last -eq "'")) {
|
||||
$value = $value.Substring(1, $value.Length - 2)
|
||||
}
|
||||
}
|
||||
$existing = [Environment]::GetEnvironmentVariable($name, 'Process')
|
||||
if ([string]::IsNullOrWhiteSpace($existing)) {
|
||||
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SenseEnvironmentValue {
|
||||
param([Parameter(Mandatory = $true)][string]$Name, [string]$Default = '')
|
||||
$value = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
if ([string]::IsNullOrWhiteSpace($value)) { return $Default }
|
||||
return $value
|
||||
}
|
||||
|
||||
function ConvertTo-SenseYamlString {
|
||||
param([AllowEmptyString()][string]$Value)
|
||||
return ($Value | ConvertTo-Json -Compress)
|
||||
}
|
||||
|
||||
function Resolve-SenseConfiguredPath {
|
||||
param([Parameter(Mandatory = $true)][string]$PackageRoot, [AllowEmptyString()][string]$Value)
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return '' }
|
||||
if ([System.IO.Path]::IsPathRooted($Value)) {
|
||||
return [System.IO.Path]::GetFullPath($Value)
|
||||
}
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $PackageRoot $Value))
|
||||
}
|
||||
|
||||
function Get-SenseDatabaseInfo {
|
||||
param([Parameter(Mandatory = $true)][string]$Connection)
|
||||
$result = @{ Host = '127.0.0.1'; Port = 5432; Database = ''; Sanitized = $Connection; Password = '' }
|
||||
if ($Connection -match '^postgres(?:ql)?://') {
|
||||
$uri = [Uri]$Connection
|
||||
$result.Host = $uri.Host
|
||||
if (-not $uri.IsDefaultPort) { $result.Port = $uri.Port }
|
||||
$result.Database = $uri.AbsolutePath.TrimStart('/')
|
||||
if ($uri.UserInfo) {
|
||||
$parts = $uri.UserInfo.Split(':', 2)
|
||||
$user = [Uri]::UnescapeDataString($parts[0])
|
||||
if ($parts.Count -eq 2) { $result.Password = [Uri]::UnescapeDataString($parts[1]) }
|
||||
$builder = [UriBuilder]$uri
|
||||
$builder.UserName = $user
|
||||
$builder.Password = ''
|
||||
$result.Sanitized = $builder.Uri.AbsoluteUri
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
$matches = [regex]::Matches($Connection, '(?:^|\s)(?<key>[A-Za-z_][A-Za-z0-9_]*)=(?<value>''(?:[^'']|'''')*''|"(?:[^"]|"")*"|[^\s]+)')
|
||||
$sanitized = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($match in $matches) {
|
||||
$key = $match.Groups['key'].Value
|
||||
$raw = $match.Groups['value'].Value
|
||||
$value = $raw
|
||||
if ($raw.Length -ge 2 -and (($raw[0] -eq "'" -and $raw[$raw.Length - 1] -eq "'") -or ($raw[0] -eq '"' -and $raw[$raw.Length - 1] -eq '"'))) {
|
||||
$value = $raw.Substring(1, $raw.Length - 2)
|
||||
}
|
||||
if ($key.ToLowerInvariant() -eq 'password') {
|
||||
$result.Password = $value
|
||||
continue
|
||||
}
|
||||
switch ($key.ToLowerInvariant()) {
|
||||
'host' { $result.Host = $value }
|
||||
'port' { $result.Port = [int]$value }
|
||||
'dbname' { $result.Database = $value }
|
||||
}
|
||||
$sanitized.Add("$key=$raw")
|
||||
}
|
||||
if ($matches.Count -eq 0) { throw 'SENSE_DATABASE_URL must be a PostgreSQL URI or keyword connection string.' }
|
||||
$result.Sanitized = $sanitized -join ' '
|
||||
return $result
|
||||
}
|
||||
|
||||
function Test-SenseTcpEndpoint {
|
||||
param([Parameter(Mandatory = $true)][string]$HostName, [Parameter(Mandatory = $true)][int]$Port, [int]$TimeoutMilliseconds = 2000)
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
try {
|
||||
$task = $client.ConnectAsync($HostName, $Port)
|
||||
if (-not $task.Wait($TimeoutMilliseconds)) { return $false }
|
||||
return $client.Connected
|
||||
} catch {
|
||||
return $false
|
||||
} finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Test-SenseListenPortAvailable {
|
||||
param([Parameter(Mandatory = $true)][string]$HostName, [Parameter(Mandatory = $true)][int]$Port)
|
||||
$ip = if ($HostName -eq '0.0.0.0') { [Net.IPAddress]::Any } elseif ($HostName -eq 'localhost') { [Net.IPAddress]::Loopback } else { [Net.IPAddress]::Parse($HostName) }
|
||||
$listener = New-Object Net.Sockets.TcpListener($ip, $Port)
|
||||
try { $listener.Start(); return $true } catch { return $false } finally { try { $listener.Stop() } catch {} }
|
||||
}
|
||||
|
||||
function Initialize-SenseRuntime {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PackageRoot,
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production',
|
||||
[switch]$AllowOccupiedPort
|
||||
)
|
||||
$configName = if ($Mode -eq 'demo') { 'sense.demo.env' } else { 'sense.env' }
|
||||
Import-SenseEnvironment -Path (Join-Path $PackageRoot "config\$configName")
|
||||
|
||||
$hostName = Get-SenseEnvironmentValue -Name 'SENSE_HOST' -Default '127.0.0.1'
|
||||
$portText = Get-SenseEnvironmentValue -Name 'SENSE_PORT' -Default '18080'
|
||||
$port = 0
|
||||
if (-not [int]::TryParse($portText, [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
|
||||
throw 'SENSE_PORT must be an integer between 1 and 65535.'
|
||||
}
|
||||
if ($hostName -notin @('127.0.0.1', '0.0.0.0', 'localhost')) {
|
||||
throw 'SENSE_HOST must be 127.0.0.1, localhost, or 0.0.0.0.'
|
||||
}
|
||||
if (-not $AllowOccupiedPort -and -not (Test-SenseListenPortAvailable -HostName $hostName -Port $port)) {
|
||||
throw "Sense HTTP port $hostName`:$port is already in use. Stop the existing process or change SENSE_PORT."
|
||||
}
|
||||
|
||||
$databaseVariable = if ($Mode -eq 'demo') { 'SENSE_DEMO_DATABASE_URL' } else { 'SENSE_DATABASE_URL' }
|
||||
$databaseURL = Get-SenseEnvironmentValue -Name $databaseVariable
|
||||
if ([string]::IsNullOrWhiteSpace($databaseURL)) { throw "$databaseVariable is required." }
|
||||
$database = Get-SenseDatabaseInfo -Connection $databaseURL
|
||||
if ([string]::IsNullOrWhiteSpace($database.Database)) { throw "$databaseVariable must name a database." }
|
||||
if ($Mode -eq 'demo' -and $database.Database -notmatch '(?i)demo|test') {
|
||||
throw 'Demo mode requires a database name containing demo or test; production data must never be reused as demo data.'
|
||||
}
|
||||
if (-not (Test-SenseTcpEndpoint -HostName $database.Host -Port $database.Port)) {
|
||||
throw "PostgreSQL is unreachable at $($database.Host):$($database.Port). Start PostgreSQL and verify the database connection."
|
||||
}
|
||||
|
||||
$jwtSecret = Get-SenseEnvironmentValue -Name 'SENSE_JWT_SECRET'
|
||||
if ($Mode -eq 'production' -and $jwtSecret.Trim().Length -lt 32) {
|
||||
throw 'SENSE_JWT_SECRET must contain at least 32 characters in production.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($jwtSecret)) { throw 'SENSE_JWT_SECRET is required.' }
|
||||
|
||||
$mediaMode = (Get-SenseEnvironmentValue -Name 'SENSE_MEDIAMTX_MODE' -Default $(if ($Mode -eq 'demo') { 'disabled' } else { 'managed' })).ToLowerInvariant()
|
||||
if ($mediaMode -notin @('managed', 'external', 'disabled')) { throw 'SENSE_MEDIAMTX_MODE must be managed, external, or disabled.' }
|
||||
if ($Mode -eq 'production' -and $mediaMode -eq 'disabled') { throw 'MediaMTX cannot be disabled in production.' }
|
||||
$mediaAPI = Get-SenseEnvironmentValue -Name 'SENSE_MEDIAMTX_API' -Default 'http://127.0.0.1:9997'
|
||||
$apiUri = [Uri]$mediaAPI
|
||||
if ($apiUri.Scheme -ne 'http' -or $apiUri.Host -notin @('127.0.0.1', 'localhost', '::1')) {
|
||||
throw 'SENSE_MEDIAMTX_API must be an HTTP loopback URL.'
|
||||
}
|
||||
$mediaBinary = Resolve-SenseConfiguredPath -PackageRoot $PackageRoot -Value (Get-SenseEnvironmentValue -Name 'SENSE_MEDIAMTX_BINARY')
|
||||
$mediaConfig = Resolve-SenseConfiguredPath -PackageRoot $PackageRoot -Value (Get-SenseEnvironmentValue -Name 'SENSE_MEDIAMTX_CONFIG')
|
||||
if ($mediaMode -eq 'managed') {
|
||||
if (-not (Test-Path -LiteralPath $mediaBinary -PathType Leaf)) { throw 'Managed MediaMTX binary not found. Set SENSE_MEDIAMTX_BINARY to mediamtx.exe.' }
|
||||
if (-not (Test-Path -LiteralPath $mediaConfig -PathType Leaf)) { throw 'Managed MediaMTX configuration not found. Set SENSE_MEDIAMTX_CONFIG.' }
|
||||
}
|
||||
if ($mediaMode -eq 'external' -and -not (Test-SenseTcpEndpoint -HostName $apiUri.Host -Port $apiUri.Port)) {
|
||||
throw "External MediaMTX Control API is unreachable at $($apiUri.Host):$($apiUri.Port)."
|
||||
}
|
||||
|
||||
$webRoot = Resolve-SenseConfiguredPath -PackageRoot $PackageRoot -Value (Get-SenseEnvironmentValue -Name 'SENSE_WEB_ROOT' -Default 'web')
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $webRoot 'index.html') -PathType Leaf)) { throw 'Sense web assets are missing. Rebuild or replace the delivery package.' }
|
||||
[Environment]::SetEnvironmentVariable('SENSE_WEB_ROOT', $webRoot, 'Process')
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_MODE', $mediaMode, 'Process')
|
||||
if ($mediaMode -eq 'managed') {
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_BINARY', $mediaBinary, 'Process')
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_CONFIG', $mediaConfig, 'Process')
|
||||
} else {
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_BINARY', '', 'Process')
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_CONFIG', '', 'Process')
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable('SENSE_MEDIAMTX_API', $mediaAPI, 'Process')
|
||||
|
||||
$runtimeDir = Join-Path $PackageRoot 'data\runtime'
|
||||
$logDir = Join-Path $PackageRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $runtimeDir, $logDir | Out-Null
|
||||
$settingsPath = Join-Path $runtimeDir 'settings.yml'
|
||||
$applicationMode = if ($Mode -eq 'production') { 'prod' } else { 'test' }
|
||||
$lines = @(
|
||||
'settings:',
|
||||
' application:',
|
||||
" mode: $applicationMode",
|
||||
" host: $(ConvertTo-SenseYamlString $hostName)",
|
||||
' name: sense',
|
||||
" port: $port",
|
||||
' readtimeout: 10',
|
||||
' writertimeout: 20',
|
||||
' enabledp: false',
|
||||
' logger:',
|
||||
" path: $(ConvertTo-SenseYamlString $logDir)",
|
||||
" stdout: ''",
|
||||
' level: info',
|
||||
' enableddb: false',
|
||||
' jwt:',
|
||||
" secret: $(ConvertTo-SenseYamlString $jwtSecret)",
|
||||
' timeout: 3600',
|
||||
' database:',
|
||||
' driver: postgres',
|
||||
" source: $(ConvertTo-SenseYamlString $databaseURL)",
|
||||
' gen:',
|
||||
" dbname: $(ConvertTo-SenseYamlString $database.Database)",
|
||||
" frontpath: ''",
|
||||
' extend:',
|
||||
' demo:',
|
||||
' name: data',
|
||||
' cache:',
|
||||
" memory: ''",
|
||||
' queue:',
|
||||
' memory:',
|
||||
' poolSize: 100',
|
||||
' locker:',
|
||||
' redis:'
|
||||
)
|
||||
[IO.File]::WriteAllLines($settingsPath, $lines, (New-Object Text.UTF8Encoding($false)))
|
||||
return @{ PackageRoot = $PackageRoot; SettingsPath = $settingsPath; Host = $hostName; Port = $port; Database = $database; Mode = $Mode; MediaMode = $mediaMode }
|
||||
}
|
||||
|
||||
function Get-SensePostgresTool {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
$configured = Get-SenseEnvironmentValue -Name 'SENSE_POSTGRES_BIN'
|
||||
if (-not [string]::IsNullOrWhiteSpace($configured)) {
|
||||
$candidate = Join-Path $configured "$Name.exe"
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
|
||||
}
|
||||
$command = Get-Command "$Name.exe" -ErrorAction SilentlyContinue
|
||||
if ($command) { return $command.Source }
|
||||
throw "$Name.exe was not found. Install PostgreSQL client tools or set SENSE_POSTGRES_BIN."
|
||||
}
|
||||
|
||||
function Invoke-SensePostgresTool {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Tool,
|
||||
[Parameter(Mandatory = $true)][hashtable]$Database,
|
||||
[Parameter(Mandatory = $true)][string[]]$Arguments
|
||||
)
|
||||
$oldPassword = [Environment]::GetEnvironmentVariable('PGPASSWORD', 'Process')
|
||||
try {
|
||||
if (-not [string]::IsNullOrEmpty($Database.Password)) {
|
||||
[Environment]::SetEnvironmentVariable('PGPASSWORD', $Database.Password, 'Process')
|
||||
}
|
||||
& $Tool @Arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "PostgreSQL tool failed with exit code $LASTEXITCODE." }
|
||||
} finally {
|
||||
[Environment]::SetEnvironmentVariable('PGPASSWORD', $oldPassword, 'Process')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\start-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,31 @@
|
||||
param(
|
||||
[ValidateSet('production', 'demo')][string]$Mode = 'production',
|
||||
[switch]$SkipMigration
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$state = Initialize-SenseRuntime -PackageRoot $root -Mode $Mode
|
||||
$sense = Join-Path $root 'sense.exe'
|
||||
if (-not (Test-Path -LiteralPath $sense -PathType Leaf)) { throw "Sense executable not found: $sense" }
|
||||
$autoMigrate = (Get-SenseEnvironmentValue -Name 'SENSE_AUTO_MIGRATE' -Default 'true').ToLowerInvariant()
|
||||
Push-Location $root
|
||||
try {
|
||||
if (-not $SkipMigration -and $autoMigrate -notin @('false', '0', 'no')) {
|
||||
Write-Host 'Applying pending Sense database migrations...'
|
||||
& $sense migrate -c $state.SettingsPath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Sense database migration failed. Review the error above and the PostgreSQL connection.' }
|
||||
}
|
||||
if ($Mode -eq 'demo') {
|
||||
Write-Warning 'Sense is running in isolated demo mode. Demo data must not be used as production data.'
|
||||
}
|
||||
Write-Host "Starting Sense at http://$($state.Host):$($state.Port)/ ..."
|
||||
Write-Host 'Press Ctrl+C in this window to stop Sense and its managed MediaMTX process.'
|
||||
& $sense server -c $state.SettingsPath
|
||||
exit $LASTEXITCODE
|
||||
} finally { Pop-Location }
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\stop-sense.ps1" %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,24 @@
|
||||
param([ValidateSet('production', 'demo')][string]$Mode = 'production')
|
||||
. (Join-Path $PSScriptRoot 'sense-common.ps1')
|
||||
|
||||
try {
|
||||
$root = Get-SensePackageRoot
|
||||
$configName = if ($Mode -eq 'demo') { 'sense.demo.env' } else { 'sense.env' }
|
||||
$config = Join-Path $root "config\$configName"
|
||||
Import-SenseEnvironment -Path $config
|
||||
$port = [int](Get-SenseEnvironmentValue -Name 'SENSE_PORT' -Default '18080')
|
||||
$connection = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $connection) { Write-Host "Sense is not listening on port $port."; exit 0 }
|
||||
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $($connection.OwningProcess)"
|
||||
$expected = [IO.Path]::GetFullPath((Join-Path $root 'sense.exe'))
|
||||
if (-not $process -or [IO.Path]::GetFullPath($process.ExecutablePath) -ne $expected) {
|
||||
throw "Port $port belongs to another process; it was not stopped."
|
||||
}
|
||||
& taskkill.exe /PID $process.ProcessId /T /F | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Failed to stop the Sense process tree.' }
|
||||
Write-Host 'Sense and its managed child processes were stopped.'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user