feat: 建立 Sense Windows 交付包 (#70)

This commit is contained in:
QiuSW
2026-08-15 10:56:47 +08:00
parent cf50c285f4
commit 6b79478414
33 changed files with 1159 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\backup-sense.ps1" %*
exit /b %errorlevel%
+22
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\check-sense.ps1" %*
exit /b %errorlevel%
+19
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\migrate-sense.ps1" %*
exit /b %errorlevel%
+19
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\restore-sense.ps1" %*
exit /b %errorlevel%
+27
View File
@@ -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
}
+281
View File
@@ -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')
}
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\start-sense.ps1" %*
exit /b %errorlevel%
+31
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\runtime\stop-sense.ps1" %*
exit /b %errorlevel%
+24
View File
@@ -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
}