mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 02:16:13 +00:00
fix(dev): 启动器输出改文件重定向, 修复长时间运行内存叠加
dev.ps1 用 Start-Job + 每 150ms Receive-Job 中转子进程输出。Start-Job 会把 每行原生输出包装成带 job/runspace 元数据的 PSObject 存进 job 输出集合, 生产快于消费 (窗口最小化/RDP 断开/日志量突增) 时持续堆积且不及时释放 —— 实测开盘跑到中午, 启动器单个 powershell.exe 可占十几 GB。 - dev.ps1: 改用 Start-Process + 重定向到 data/logs/*.log, PowerShell 只持 PID; 控制台改为有界 StreamReader tail, 内存占用与日志总量无关 - dev.ps1: 新增单实例守卫 (默认拒绝启动, -Force 才结束) 与 -NoReload; 退出时 taskkill /T 结束整棵树, 覆盖 reloader 父进程与 server 子进程 - dev.sh: free_port 只杀监听者会让 reloader 父进程重生子进程 —— 改为先按 仓库 venv 路径结束同端口整组后端; cleanup 改递归 kill_tree, 不再留孤儿 - dev.sh: 只处理同端口实例, 换端口调试的实例只提示不误杀 验证: dev.sh bash -n 通过; kill_repo_backends / kill_tree 用真实进程做了 隔离测试 (8888/9999 端口不误杀、三级进程树无孤儿); dev.ps1 花括号平衡与 无残留 Start-Job 调用, 本机无 PowerShell 未做实际运行验证。
This commit is contained in:
@@ -3,9 +3,12 @@
|
||||
# Usage:
|
||||
# .\dev.ps1
|
||||
# .\dev.ps1 -BackendPort 8000 -FrontendPort 5173
|
||||
# .\dev.ps1 -NoReload # 内测/长时间运行: 不启用 uvicorn --reload
|
||||
# .\dev.ps1 -Force # 已有实例时先结束它再启动
|
||||
# $env:BACKEND_PORT='8000'; .\dev.ps1
|
||||
#
|
||||
# Ctrl-C closes both processes.
|
||||
# 进程输出直接重定向到 data\logs\*.log, 启动器只做有界 tail, 内存占用恒定。
|
||||
# 停止请用 Ctrl-C (会结束整棵进程树); 直接点窗口 X 不会走到清理逻辑。
|
||||
#
|
||||
# If you see "running scripts is disabled":
|
||||
# Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||
@@ -13,7 +16,12 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$BackendPort = 0,
|
||||
[int]$FrontendPort = 0
|
||||
[int]$FrontendPort = 0,
|
||||
# 检测到已有后端实例时强制结束它再启动。默认拒绝启动, 避免多实例叠加。
|
||||
[switch]$Force,
|
||||
# 不启用 uvicorn --reload。内测/长时间运行时建议开启: 改文件不再触发
|
||||
# 进程重启, 也就不会反复做 1GB+ 的 enriched 全量预热。
|
||||
[switch]$NoReload
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -56,6 +64,38 @@ if ($BackendPort -le 0) {
|
||||
}
|
||||
if ($FrontendPort -le 0) { $FrontendPort = if ($env:FRONTEND_PORT) { [int]$env:FRONTEND_PORT } else { 3011 } }
|
||||
|
||||
# 单实例守卫: 重复运行 dev.ps1 会各自拉起一套后端, 每个实例常驻 1GB+;
|
||||
# 靠端口检测杀不干净 (--reload 的父进程持有 socket 并会重生子进程), 因此改为
|
||||
# 启动前显式识别已有实例: 默认拒绝启动, -Force 时才整树结束。
|
||||
# 只匹配本仓库的 venv 解释器, 避免误伤其他项目里同名模块 app.main:app 的进程。
|
||||
function Stop-ExistingInstance {
|
||||
$venvPython = Join-Path $BackendDir '.venv\Scripts\python.exe'
|
||||
$existing = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$_.ProcessId -ne $PID -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -like '*uvicorn*app.main:app*' -and
|
||||
($_.CommandLine -like "*$venvPython*" -or
|
||||
($_.CommandLine -like '*.venv\Scripts\python*' -and
|
||||
$_.ExecutablePath -like "$BackendDir*"))
|
||||
})
|
||||
if ($existing.Count -eq 0) { return $true }
|
||||
$ids = @($existing | ForEach-Object { $_.ProcessId })
|
||||
if (-not $Force) {
|
||||
Log-Warn "检测到已有后端实例 (PID: $($ids -join ', ')), 本次启动取消"
|
||||
Log-Warn '如需重启, 重新运行并加 -Force, 或先执行:'
|
||||
Log-Warn " Stop-Process -Id $($ids -join ',') -Force"
|
||||
return $false
|
||||
}
|
||||
Log-Warn "结束已有后端实例 (PID: $($ids -join ', '))"
|
||||
foreach ($p in $ids) {
|
||||
$null = & cmd /c "taskkill /F /T /PID $p 2>nul"
|
||||
try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
return $true
|
||||
}
|
||||
|
||||
# Force UTF-8 console output so child process logs aren't garbled
|
||||
try {
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
@@ -135,6 +175,8 @@ function Free-Port($name, $port) {
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Stop-ExistingInstance)) { exit 1 }
|
||||
|
||||
Free-Port 'backend' $BackendPort
|
||||
Free-Port 'frontend' $FrontendPort
|
||||
|
||||
@@ -185,51 +227,46 @@ Write-Host '| Ctrl-C closes both |' -ForegroundColor B
|
||||
Write-Host '+----------------------------------------------+' -ForegroundColor Blue
|
||||
Write-Host ''
|
||||
|
||||
# ===== 5. Launch jobs =====
|
||||
# Each job writes its $PID to a temp file so the main thread can find the
|
||||
# child powershell.exe and taskkill /T the whole process tree on exit.
|
||||
$backendPidFile = [System.IO.Path]::GetTempFileName()
|
||||
$frontendPidFile = [System.IO.Path]::GetTempFileName()
|
||||
# ===== 5. Launch processes =====
|
||||
# 输出由操作系统直接重定向到日志文件, PowerShell 只持有 PID, 不参与数据搬运。
|
||||
#
|
||||
# 旧实现用 Start-Job + 每 150ms Receive-Job 轮询: Start-Job 会把每一行原生输出
|
||||
# 包装成带 job/runspace 元数据的 PSObject 存进 job 输出集合。当生产快于消费
|
||||
# (控制台滚动缓冲、窗口最小化、RDP 断开、日志音量突增) 时对象持续堆积且不及时
|
||||
# 释放 —— 实测开盘跑到中午, 启动器单个 powershell.exe 可占十几 GB。改为文件
|
||||
# 重定向后, 启动器内存与日志总量无关; 控制台只做「读新增字节」的有界 tail。
|
||||
$LogDir = Join-Path $Root 'data\logs'
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$BackendOutLog = Join-Path $LogDir 'backend.out.log'
|
||||
$BackendErrLog = Join-Path $LogDir 'backend.err.log'
|
||||
$FrontendOutLog = Join-Path $LogDir 'frontend.out.log'
|
||||
$FrontendErrLog = Join-Path $LogDir 'frontend.err.log'
|
||||
|
||||
$backendJob = Start-Job -Name 'backend' -ScriptBlock {
|
||||
param($pidFile, $dir, $envFile, $bindAddress, $port)
|
||||
# Start-Job 开的是全新 powershell.exe 子进程, 不继承主进程的 UTF-8 设置,
|
||||
# 默认用系统 ANSI (中文 Windows = GBK/cp936) 解码后端 UTF-8 输出 → 中文乱码。
|
||||
# 这里强制子进程用 UTF-8, 与 app/__init__.py 的 stdout/stderr 编码对齐。
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$PID | Out-File -FilePath $pidFile -Encoding ascii -Force
|
||||
$env:PYTHONUNBUFFERED = '1'
|
||||
Set-Location $dir
|
||||
$envArgs = if (Test-Path $envFile) { @('--env-file', $envFile) } else { @() }
|
||||
& .\.venv\Scripts\python.exe -m uvicorn app.main:app @envArgs --reload --host $bindAddress --port $port 2>&1
|
||||
} -ArgumentList $backendPidFile, $BackendDir, $EnvFile, $BindAddress, $BackendPort
|
||||
# 每次启动重置本会话日志 (后端自身的 data/backend.log 已有轮转, 这里只放转发副本)
|
||||
foreach ($f in @($BackendOutLog, $BackendErrLog, $FrontendOutLog, $FrontendErrLog)) {
|
||||
Remove-Item $f -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$frontendJob = Start-Job -Name 'frontend' -ScriptBlock {
|
||||
param($pidFile, $dir, $bindAddress, $backendPort, $port)
|
||||
# 同上: job 子进程默认 GBK, pnpm/前端工具链也是 UTF-8 输出, 需对齐。
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||
$PID | Out-File -FilePath $pidFile -Encoding ascii -Force
|
||||
Set-Location $dir
|
||||
$env:BACKEND_HOST = $bindAddress
|
||||
$env:BACKEND_PORT = [string]$backendPort
|
||||
& pnpm dev --host $bindAddress --port $port 2>&1
|
||||
} -ArgumentList $frontendPidFile, $FrontendDir, $BindAddress, $BackendPort, $FrontendPort
|
||||
$env:PYTHONUNBUFFERED = '1'
|
||||
# Start-Process 的 -ArgumentList 按空格拼接, 含空格的路径必须自带引号
|
||||
function Quote-Arg($value) {
|
||||
if ($value -match '[\s"]') { return '"' + ($value -replace '"', '\"') + '"' }
|
||||
return $value
|
||||
}
|
||||
|
||||
# Wait up to 5 seconds for the PID files to materialise
|
||||
function Read-JobPid($file) {
|
||||
for ($i = 0; $i -lt 50; $i++) {
|
||||
try {
|
||||
$c = (Get-Content $file -ErrorAction SilentlyContinue) -as [string]
|
||||
if ($c -and $c.Trim()) { return [int]$c.Trim() }
|
||||
} catch {}
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
return $null
|
||||
$BackendArgs = @('-m', 'uvicorn', 'app.main:app')
|
||||
if (Test-Path $EnvFile) { $BackendArgs += @('--env-file', (Quote-Arg $EnvFile)) }
|
||||
if (-not $NoReload) { $BackendArgs += '--reload' }
|
||||
$BackendArgs += @('--host', $BindAddress, '--port', [string]$BackendPort)
|
||||
|
||||
$env:BACKEND_HOST = $BindAddress
|
||||
$env:BACKEND_PORT = [string]$BackendPort
|
||||
$pnpmExe = (Get-Command 'pnpm.cmd' -ErrorAction SilentlyContinue).Source
|
||||
if (-not $pnpmExe) { $pnpmExe = (Get-Command 'pnpm' -ErrorAction SilentlyContinue).Source }
|
||||
if (-not $pnpmExe) {
|
||||
Log-Err 'pnpm not found'
|
||||
exit 1
|
||||
}
|
||||
$backendChildPid = Read-JobPid $backendPidFile
|
||||
$frontendChildPid = Read-JobPid $frontendPidFile
|
||||
|
||||
# ===== 6. Cleanup =====
|
||||
$script:cleaning = $false
|
||||
@@ -239,28 +276,110 @@ function Cleanup-All {
|
||||
Write-Host ''
|
||||
Log-Info 'shutting down...'
|
||||
|
||||
foreach ($p in @($backendChildPid, $frontendChildPid)) {
|
||||
if ($p) {
|
||||
# /T kills the whole process tree (the job's powershell + uvicorn/vite)
|
||||
$null = & cmd /c "taskkill /F /T /PID $p 2>nul"
|
||||
# /T 结束整棵树 (uvicorn --reload 的父进程 + server 子进程 / pnpm + vite + esbuild)
|
||||
foreach ($proc in @($backendProc, $frontendProc)) {
|
||||
if ($proc -and -not $proc.HasExited) {
|
||||
$null = & cmd /c "taskkill /F /T /PID $($proc.Id) 2>nul"
|
||||
}
|
||||
}
|
||||
foreach ($j in @($backendJob, $frontendJob)) {
|
||||
if ($j) {
|
||||
Stop-Job $j -ErrorAction SilentlyContinue
|
||||
Remove-Job $j -Force -ErrorAction SilentlyContinue
|
||||
if ($script:tails) {
|
||||
foreach ($state in @($script:tails.Values)) {
|
||||
if ($state -and $state.Reader) {
|
||||
try { $state.Reader.Dispose() } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($f in @($backendPidFile, $frontendPidFile)) {
|
||||
Remove-Item $f -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Log-Ok 'bye'
|
||||
Log-Ok "bye (logs: $LogDir)"
|
||||
}
|
||||
|
||||
# ===== 7. Main loop - pump output, handle Ctrl-C =====
|
||||
# Treat Ctrl-C as input so try/finally is guaranteed to run.
|
||||
# ===== 7. Bounded log tail =====
|
||||
# 每个日志文件一个 StreamReader, 每轮只消费「上次之后新增的内容」。
|
||||
# 内存占用 = StreamReader 解码缓冲 + 一行待拼接文本, 与日志总量无关。
|
||||
# StreamReader 负责 UTF-8 解码, 多字节字符跨读边界也不会被截断成乱码。
|
||||
$script:tails = @{}
|
||||
foreach ($p in @($BackendOutLog, $BackendErrLog, $FrontendOutLog, $FrontendErrLog)) {
|
||||
$script:tails[$p] = @{ Reader = $null; LastLength = 0L; Pending = '' }
|
||||
}
|
||||
$MaxPending = 64KB
|
||||
|
||||
function Open-LogTail($path) {
|
||||
$fs = [System.IO.File]::Open($path, [System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Read,
|
||||
[System.IO.FileShare]::ReadWrite -bor [System.IO.FileShare]::Delete)
|
||||
return New-Object System.IO.StreamReader(
|
||||
$fs, (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
function Read-NewLogLines($path, $prefix, $color) {
|
||||
$state = $script:tails[$path]
|
||||
if (-not $state) { return }
|
||||
try {
|
||||
if ($null -eq $state.Reader) {
|
||||
if (-not (Test-Path $path)) { return }
|
||||
$state.Reader = Open-LogTail $path
|
||||
} elseif ($state.Reader.BaseStream.Length -lt $state.LastLength) {
|
||||
# 文件被重置/替换 → 从头重新跟随
|
||||
$state.Reader.Dispose()
|
||||
$state.Reader = Open-LogTail $path
|
||||
$state.Pending = ''
|
||||
}
|
||||
$state.LastLength = $state.Reader.BaseStream.Length
|
||||
|
||||
# ReadToEnd: 从当前位置读到末尾, 只取新增部分
|
||||
$chunk = $state.Reader.ReadToEnd()
|
||||
if ([string]::IsNullOrEmpty($chunk)) { return }
|
||||
|
||||
$text = $state.Pending + $chunk
|
||||
$complete = $text.EndsWith("`n")
|
||||
$lines = $text -split "`n"
|
||||
if ($complete) {
|
||||
$state.Pending = ''
|
||||
if ($lines.Count -gt 0) { $lines = $lines[0..($lines.Count - 2)] }
|
||||
} else {
|
||||
# 末尾没有换行 = 这行可能还没写完, 留到下一轮
|
||||
$state.Pending = $lines[-1]
|
||||
$lines = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
|
||||
if ($state.Pending.Length -gt $MaxPending) {
|
||||
Write-Host $prefix -NoNewline -ForegroundColor $color
|
||||
Write-Host ($state.Pending.Substring(0, $MaxPending) + ' ...[truncated]')
|
||||
$state.Pending = ''
|
||||
}
|
||||
}
|
||||
foreach ($line in $lines) {
|
||||
Write-Host $prefix -NoNewline -ForegroundColor $color
|
||||
Write-Host $line.TrimEnd("`r")
|
||||
}
|
||||
} catch {
|
||||
# 日志读取失败绝不能拖垮启动器
|
||||
}
|
||||
}
|
||||
|
||||
# ===== 8. Launch + main loop =====
|
||||
# 启动与主循环同处一个 try/finally: 任一 Start-Process 失败或 Ctrl-C 都会走
|
||||
# Cleanup-All, 不留下半套进程。
|
||||
$prevCtrlC = [Console]::TreatControlCAsInput
|
||||
try {
|
||||
$backendProc = Start-Process `
|
||||
-FilePath (Join-Path $BackendDir '.venv\Scripts\python.exe') `
|
||||
-ArgumentList $BackendArgs `
|
||||
-WorkingDirectory $BackendDir `
|
||||
-RedirectStandardOutput $BackendOutLog `
|
||||
-RedirectStandardError $BackendErrLog `
|
||||
-NoNewWindow -PassThru
|
||||
|
||||
# .cmd 不是可执行文件: 关闭 shell 执行 (重定向输出时必须) 后要走 cmd.exe /c。
|
||||
# 整条命令作为一个参数交给 cmd 解析, 避免 Start-Process 自行拼接时加错引号。
|
||||
$cmdExe = if ($env:ComSpec) { $env:ComSpec } else { 'cmd.exe' }
|
||||
$frontendCmd = '"{0}" dev --host {1} --port {2}' -f $pnpmExe, $BindAddress, $FrontendPort
|
||||
$frontendProc = Start-Process `
|
||||
-FilePath $cmdExe `
|
||||
-ArgumentList @('/c', $frontendCmd) `
|
||||
-WorkingDirectory $FrontendDir `
|
||||
-RedirectStandardOutput $FrontendOutLog `
|
||||
-RedirectStandardError $FrontendErrLog `
|
||||
-NoNewWindow -PassThru
|
||||
|
||||
# Treat Ctrl-C as input so try/finally is guaranteed to run.
|
||||
[Console]::TreatControlCAsInput = $true
|
||||
|
||||
while ($true) {
|
||||
@@ -271,28 +390,17 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
$bOut = Receive-Job $backendJob -ErrorAction SilentlyContinue
|
||||
if ($bOut) {
|
||||
foreach ($line in $bOut) {
|
||||
Write-Host '[backend ] ' -NoNewline -ForegroundColor Blue
|
||||
Write-Host $line
|
||||
}
|
||||
}
|
||||
Read-NewLogLines $BackendOutLog '[backend ] ' Blue
|
||||
Read-NewLogLines $BackendErrLog '[backend ] ' Blue
|
||||
Read-NewLogLines $FrontendOutLog '[frontend] ' Green
|
||||
Read-NewLogLines $FrontendErrLog '[frontend] ' Green
|
||||
|
||||
$fOut = Receive-Job $frontendJob -ErrorAction SilentlyContinue
|
||||
if ($fOut) {
|
||||
foreach ($line in $fOut) {
|
||||
Write-Host '[frontend] ' -NoNewline -ForegroundColor Green
|
||||
Write-Host $line
|
||||
}
|
||||
}
|
||||
|
||||
if ($backendJob.State -ne 'Running' -or $frontendJob.State -ne 'Running') {
|
||||
if ($backendProc.HasExited -or $frontendProc.HasExited) {
|
||||
Log-Warn 'one of the processes exited; closing the other...'
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 150
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -117,6 +117,51 @@ free_port() {
|
||||
fi
|
||||
ok "端口 $port 已释放"
|
||||
}
|
||||
# 按本仓库 venv 的绝对路径识别整组后端进程。
|
||||
# uvicorn --reload 的监听 socket 由 reloader 父进程创建并传给 server 子进程:
|
||||
# 只 kill 监听者会让父进程立刻重生一个新子进程, 旧实例根本没退出 —— 每次重启
|
||||
# 就多叠一套常驻 1GB+ 的后端。这里把父、子一起结束。
|
||||
# 只处理同一端口的实例: 换端口再起一个 (如临时开 8000 调试) 不该被顺手杀掉。
|
||||
kill_repo_backends() {
|
||||
local pattern="$BACKEND_DIR/.venv/bin/python -m uvicorn app.main:app"
|
||||
local pids others=""
|
||||
pids=$(pgrep -f "$pattern" 2>/dev/null || true)
|
||||
if [ -z "$pids" ]; then
|
||||
return 0
|
||||
fi
|
||||
local to_kill=""
|
||||
local pid cmd
|
||||
for pid in $pids; do
|
||||
cmd=$(ps -o command= -p "$pid" 2>/dev/null || true)
|
||||
if [[ "$cmd" == *"--port $BACKEND_PORT"* ]]; then
|
||||
to_kill="$to_kill $pid"
|
||||
else
|
||||
others="$others $pid"
|
||||
fi
|
||||
done
|
||||
if [ -n "$others" ]; then
|
||||
warn "检测到其他端口上的本项目后端实例 (PID:$others), 未处理。"
|
||||
warn "如需一并结束: kill$others"
|
||||
fi
|
||||
if [ -z "$to_kill" ]; then
|
||||
return 0
|
||||
fi
|
||||
warn "结束本项目同端口后端实例 PID:$(echo "$to_kill" | xargs)"
|
||||
echo "$to_kill" | xargs kill 2>/dev/null || true
|
||||
sleep 1
|
||||
local left
|
||||
left=$(echo "$to_kill" | xargs -n1 sh -c 'kill -0 "$0" 2>/dev/null && echo "$0"' || true)
|
||||
if [ -n "$left" ]; then
|
||||
echo "$left" | xargs kill -9 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 先按仓库路径结束整组后端, 再查端口:
|
||||
# 反过来的话 free_port 会先杀掉监听者 (reload 的子进程), 父进程随即重生一个,
|
||||
# 端口检查看到的又是新进程。
|
||||
kill_repo_backends
|
||||
free_port backend "$BACKEND_PORT"
|
||||
free_port frontend "$FRONTEND_PORT"
|
||||
|
||||
@@ -142,12 +187,22 @@ fi
|
||||
# ===== 4. 启动 + 日志前缀 =====
|
||||
PIDS=()
|
||||
|
||||
# 结束整棵子树: 只 kill 子 shell 自己会把 uvicorn/vite 留在原地变成孤儿
|
||||
# (实测存在存活数天的残留 vite)。递归先杀后代再杀父进程。
|
||||
kill_tree() {
|
||||
local pid="$1" child
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
||||
kill_tree "$child"
|
||||
done
|
||||
kill "$pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo
|
||||
info "关闭服务..."
|
||||
for pid in "${PIDS[@]:-}"; do
|
||||
if [ -n "$pid" ]; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
kill_tree "$pid"
|
||||
fi
|
||||
done
|
||||
# 等子进程退出,避免孤儿
|
||||
|
||||
Reference in New Issue
Block a user