112 lines
4.6 KiB
PowerShell
112 lines
4.6 KiB
PowerShell
# 本地预览服务器 (零依赖 TcpListener + Runspace 池并发): 服务在线站点静态目录
|
|
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web.ps1 [-Root release\web_2026.08] [-Port 8080]
|
|
# 并发: 每个请求一个 Runspace (池上限 16), 大文件 (wasm/图片) 不再阻塞小文件 (step 下载/页面)
|
|
param(
|
|
[string]$Root = (Join-Path $PSScriptRoot '..\release\web_2026.08'),
|
|
[int]$Port = 8080,
|
|
[string]$ViewerRoot = (Join-Path $PSScriptRoot '..\viewer') # /viewer/ 前缀 → 新迪查看器本地副本 (可选; 原项目 Z:\...\上海新迪3D\dev 只读)
|
|
)
|
|
if (-not (Test-Path $Root)) { Write-Output "目录不存在: $Root"; exit 1 }
|
|
$viewerRootFull = ''
|
|
if ($ViewerRoot -and (Test-Path $ViewerRoot)) {
|
|
$viewerRootFull = [System.IO.Path]::GetFullPath($ViewerRoot)
|
|
Write-Output ("查看器目录: " + $viewerRootFull)
|
|
}
|
|
|
|
$mime = @{
|
|
'.html' = 'text/html; charset=utf-8'; '.js' = 'application/javascript; charset=utf-8'
|
|
'.css' = 'text/css; charset=utf-8'; '.json' = 'application/json; charset=utf-8'
|
|
'.png' = 'image/png'; '.jpg' = 'image/jpeg'; '.pdf' = 'application/pdf'
|
|
'.step' = 'application/octet-stream'; '.ico' = 'image/x-icon'; '.txt' = 'text/plain; charset=utf-8'
|
|
'.wasm' = 'application/wasm'; '.stp' = 'application/octet-stream'
|
|
}
|
|
$rootFull = [System.IO.Path]::GetFullPath($Root)
|
|
|
|
# 单请求处理逻辑 (自包含, 在 Runspace 中执行)
|
|
$handlerText = @'
|
|
param($client, $rootFull, $viewerRootFull, $mime)
|
|
try {
|
|
$stream = $client.GetStream()
|
|
$stream.ReadTimeout = 2000 # 预连接/空连接快速失败, 不阻塞后续请求
|
|
$stream.WriteTimeout = 30000
|
|
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII, $false, 4096, $true)
|
|
$reqLine = $reader.ReadLine()
|
|
if ($null -eq $reqLine) { $client.Close(); return } # 客户端连上不发数据, 直接断开
|
|
# 读掉请求头 (EOF 即 null 时必须退出, 否则死循环)
|
|
$line = $reader.ReadLine()
|
|
$guard = 0
|
|
while ($line -ne $null -and $line -ne '') {
|
|
$line = $reader.ReadLine()
|
|
$guard++
|
|
if ($guard -gt 100) { break }
|
|
}
|
|
|
|
$path = '/'
|
|
if ($reqLine) {
|
|
$parts = $reqLine -split ' '
|
|
if ($parts.Count -ge 2) { $path = $parts[1] }
|
|
}
|
|
# 去掉查询串 (?v= / ?file= 等), 否则会进文件路径导致非法字符异常
|
|
$qi = $path.IndexOf('?')
|
|
if ($qi -ge 0) { $path = $path.Substring(0, $qi) }
|
|
if ($path -eq '/') { $path = '/index.html' }
|
|
|
|
$full = [System.IO.Path]::GetFullPath((Join-Path $rootFull ($path -replace '/', '\').TrimStart('\')))
|
|
$bytes = $null; $status = '200 OK'; $ctype = 'application/octet-stream'
|
|
# /viewer/ 前缀 → 新迪 3D 查看器根目录 (各自防目录穿越)
|
|
$rootNow = $rootFull
|
|
$rel = ($path -replace '/', '\').TrimStart('\')
|
|
if ($viewerRootFull -and $rel.StartsWith('viewer\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
$rootNow = $viewerRootFull
|
|
$rel = $rel.Substring('viewer\'.Length)
|
|
$full = [System.IO.Path]::GetFullPath((Join-Path $rootNow $rel))
|
|
}
|
|
if ($full.StartsWith($rootNow, [System.StringComparison]::OrdinalIgnoreCase) -and (Test-Path $full -PathType Leaf)) {
|
|
$bytes = [System.IO.File]::ReadAllBytes($full)
|
|
$ext = [System.IO.Path]::GetExtension($full).ToLowerInvariant()
|
|
if ($mime.ContainsKey($ext)) { $ctype = $mime[$ext] }
|
|
} else {
|
|
$status = '404 Not Found'
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes('404 Not Found')
|
|
$ctype = 'text/plain; charset=utf-8'
|
|
}
|
|
$head = "HTTP/1.1 $status`r`nContent-Type: $ctype`r`nCache-Control: no-cache`r`nContent-Length: $($bytes.Length)`r`nConnection: close`r`n`r`n"
|
|
$headBytes = [System.Text.Encoding]::ASCII.GetBytes($head)
|
|
$stream.Write($headBytes, 0, $headBytes.Length)
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
$stream.Flush()
|
|
} catch { }
|
|
$client.Close()
|
|
'@
|
|
|
|
# Runspace 池 (并发上限 16)
|
|
$pool = [runspacefactory]::CreateRunspacePool(1, 16)
|
|
$pool.Open()
|
|
|
|
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
|
|
$listener.Start()
|
|
Write-Output ("服务已启动: http://localhost:" + $Port + " (停止: 关闭本窗口)")
|
|
|
|
while ($true) {
|
|
$client = $null
|
|
try {
|
|
$client = $listener.AcceptTcpClient()
|
|
} catch {
|
|
# 监听异常不退出进程 (端口占用等), 2 秒后重试
|
|
Start-Sleep -Seconds 2
|
|
continue
|
|
}
|
|
try {
|
|
$ps = [powershell]::Create()
|
|
$ps.RunspacePool = $pool
|
|
[void]$ps.AddScript($handlerText)
|
|
[void]$ps.AddArgument($client)
|
|
[void]$ps.AddArgument($rootFull)
|
|
[void]$ps.AddArgument($viewerRootFull)
|
|
[void]$ps.AddArgument($mime)
|
|
[void]$ps.BeginInvoke()
|
|
} catch {
|
|
$client.Close()
|
|
}
|
|
}
|