feat: release 1.4.0 with isolated upgrades and device discovery
This commit is contained in:
1 parent
a6a3ee2eb0
commit
df66897ff4
41 files changed
+1564
-126
No files matched your search
@@ -0,0 +1,20 @@
|
||||
# Shared preparation/upgrade mutex. ASCII for Windows PowerShell 5.1.
|
||||
function Enter-ArtemisInstallationLock {
|
||||
param([string]$ProjectRoot)
|
||||
$normalized = [IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\').ToLowerInvariant()
|
||||
$hash = [Security.Cryptography.SHA256]::Create()
|
||||
try { $digest = [BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($normalized))).Replace('-', '') }
|
||||
finally { $hash.Dispose() }
|
||||
$mutex = New-Object Threading.Mutex($false, "Local\ArtemisInstallation-$digest")
|
||||
try {
|
||||
try { $entered = $mutex.WaitOne(0) }
|
||||
catch [Threading.AbandonedMutexException] { $entered = $true }
|
||||
if (-not $entered) { throw 'Another startup preparation or upgrade is running. Please wait and retry.' }
|
||||
return $mutex
|
||||
} catch { $mutex.Dispose(); throw }
|
||||
}
|
||||
|
||||
function Exit-ArtemisInstallationLock {
|
||||
param($Mutex)
|
||||
if ($null -ne $Mutex) { $Mutex.ReleaseMutex(); $Mutex.Dispose() }
|
||||
}
|
||||
+49
-22
@@ -17,8 +17,10 @@ param(
|
||||
[int]$Port = 8000,
|
||||
[switch]$NoOpen = $false,
|
||||
[switch]$SetupOnly = $false,
|
||||
[switch]$PrepareOnly = $false,
|
||||
[switch]$SkipUpdate = $false,
|
||||
[switch]$SkipMcpSync = $false,
|
||||
[switch]$Help = $false,
|
||||
[ValidateSet('Official', 'China')][string]$Mirror
|
||||
)
|
||||
|
||||
@@ -31,6 +33,35 @@ function Get-LaunchText {
|
||||
return $English
|
||||
}
|
||||
|
||||
# Keep UTF-8 Chinese text in this BOM-encoded script, never in CMD batch files.
|
||||
# CMD can misparse multibyte batch lines when an archive/editor converts CRLF to LF.
|
||||
if ($Help) {
|
||||
Write-Host (Get-LaunchText 'Usage: start.bat [options]' '用法:start-cn.bat [选项]')
|
||||
Write-Host ''
|
||||
Write-Host (Get-LaunchText ' -SetupOnly Prepare the environment without launching the UI' ' -SetupOnly 安装或检查环境,不启动网页')
|
||||
Write-Host (Get-LaunchText ' -Port 8001 Use the specified port (default: 8000)' ' -Port 8001 使用指定端口,默认 8000')
|
||||
Write-Host (Get-LaunchText ' -NoOpen Do not open the browser automatically' ' -NoOpen 启动服务后不自动打开浏览器')
|
||||
Write-Host (Get-LaunchText ' -SkipUpdate Skip the source update notification' ' -SkipUpdate 跳过源码更新提示(启动不会拉取源码)')
|
||||
Write-Host (Get-LaunchText ' -SkipMcpSync Skip Codex MCP installation and updates' ' -SkipMcpSync 仅启动网页,跳过 Codex MCP 安装和更新')
|
||||
Write-Host (Get-LaunchText ' -Mirror China Use China download mirrors' ' -Mirror China 使用国内下载镜像')
|
||||
Write-Host (Get-LaunchText ' -Help Show this help' ' -Help 显示本说明')
|
||||
Write-Host ''
|
||||
Write-Host (Get-LaunchText 'Download sources: config\downloads.json' '国内镜像设置:config\downloads.json')
|
||||
Write-Host (Get-LaunchText 'First launch requires Internet access. Upgrade with update.bat; startup only checks for updates.' '首次运行需联网;启动仅提示源码更新,升级请运行 update-cn.bat。')
|
||||
exit 0
|
||||
}
|
||||
Write-Host '======================================================'
|
||||
if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN' -and $Mirror -eq 'China') {
|
||||
Write-Host ' ARTEMIS 中文启动 - 使用国内镜像'
|
||||
Write-Host '======================================================'
|
||||
Write-Host '正在检查已有环境、更新提示及依赖;首次启动可能需要下载组件。'
|
||||
Write-Host '镜像地址和下载超时可在 config\downloads.json 中调整。'
|
||||
} else {
|
||||
Write-Host (Get-LaunchText ' Artemis Autonomous Mobile Agent UI' ' ARTEMIS 手机自动化测试')
|
||||
Write-Host '======================================================'
|
||||
}
|
||||
Write-Host ''
|
||||
|
||||
# Ensure modern TLS protocols are enabled for downloads (GitHub releases, nodejs.org, Google repositories)
|
||||
try {
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
|
||||
@@ -40,28 +71,18 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$RootDir = Split-Path -Parent $ScriptDir
|
||||
Set-Location $RootDir
|
||||
|
||||
# Existing installations check source BEFORE importing bootstrap helpers.
|
||||
# Startup never mutates source; upgrades run from a staged, independent process.
|
||||
. (Join-Path $ScriptDir 'installation_lock.ps1')
|
||||
$installationLock = $null
|
||||
if ($env:ARTEMIS_UPGRADE_CHILD -ne '1') {
|
||||
try { $installationLock = Enter-ArtemisInstallationLock -ProjectRoot $RootDir }
|
||||
catch { Write-Host (Get-LaunchText "[ERROR] $_" '[错误] 另一个环境准备或升级正在运行,请稍后重试。') -ForegroundColor Red; exit 1 }
|
||||
}
|
||||
$existingPython = Join-Path $RootDir '.venv\Scripts\python.exe'
|
||||
if ((-not $SkipUpdate) -and (Test-Path -LiteralPath $existingPython)) {
|
||||
& $existingPython (Join-Path $ScriptDir 'update_installation.py') source --port $Port
|
||||
$updateExit = $LASTEXITCODE
|
||||
if ($updateExit -eq 20) { exit 1 }
|
||||
if ($updateExit -eq 10) {
|
||||
# Reparse the freshly updated launcher; do not continue running stale functions.
|
||||
$reloadArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $ScriptDir 'start.ps1'), '-Port', $Port, '-SkipUpdate')
|
||||
if ($NoOpen) { $reloadArgs += '-NoOpen' }
|
||||
if ($SetupOnly) { $reloadArgs += '-SetupOnly' }
|
||||
if ($SkipMcpSync) { $reloadArgs += '-SkipMcpSync' }
|
||||
if ($Mirror) { $reloadArgs += @('-Mirror', $Mirror) }
|
||||
& powershell.exe @reloadArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if ($updateExit -ne 0) {
|
||||
Write-Host (Get-LaunchText ' [ERROR] Source update check failed. Resolve the error above, or use -SkipUpdate for local startup.' ' [错误] 源码更新检查失败。请处理上方错误;要运行当前源码可传入 -SkipUpdate。') -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
& $existingPython (Join-Path $ScriptDir 'update_installation.py') source-check
|
||||
# Network/authentication failures are warnings, never a reason to prevent startup.
|
||||
}
|
||||
|
||||
. (Join-Path $ScriptDir 'download_config.ps1')
|
||||
try {
|
||||
Initialize-ArtemisDownloads -ProjectRoot $RootDir -Mode $Mirror
|
||||
@@ -376,7 +397,7 @@ if (-not $SkipMcpSync) {
|
||||
} else {
|
||||
Write-Host (Get-LaunchText ' [SKIP] MCP synchronization was skipped.' ' [跳过] 已跳过 MCP 同步。') -ForegroundColor Yellow
|
||||
}
|
||||
if ($SetupOnly) {
|
||||
if ($SetupOnly -and -not $PrepareOnly) {
|
||||
Write-Host (Get-LaunchText ' [OK] Setup complete. Run start.bat to launch Artemis.' ' [完成] 环境已准备好。运行 start-cn.bat 启动 ARTEMIS。') -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
@@ -431,7 +452,7 @@ if ($missingCore.Count -gt 0) {
|
||||
}
|
||||
|
||||
# Ensure local ADB daemon is warm & listening so probes do not hit 'Connection refused'
|
||||
Start-LocalAdbServer
|
||||
if (-not $PrepareOnly) { Start-LocalAdbServer }
|
||||
|
||||
# 6. Check and build Showcase UI if not already compiled
|
||||
$ShowcaseIndex = "$RootDir\apps\showcase_ui\dist\frontend\browser\index.html"
|
||||
@@ -523,9 +544,15 @@ if ($frontendChanged -or ((-not (Test-Path $ShowcaseIndex)) -and (-not (Test-Pat
|
||||
|
||||
# MCP clients were refreshed after Python dependency sync above.
|
||||
|
||||
Exit-ArtemisInstallationLock -Mutex $installationLock
|
||||
$installationLock = $null
|
||||
if ($PrepareOnly) {
|
||||
Write-Host (Get-LaunchText '[OK] Python, frontend and Codex MCP preparation complete.' '[完成] Python、网页构建和 Codex MCP 已准备好。') -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
# 8. Launch unified Showcase UI & open browser
|
||||
# Ensure local ADB daemon is warm & listening
|
||||
Start-LocalAdbServer
|
||||
if (-not $PrepareOnly) { Start-LocalAdbServer }
|
||||
|
||||
# Launch via `python -m artemis` (not the `artemis.exe` console-script shim) so the
|
||||
# long-running server never locks .venv\Scripts\artemis.exe against `uv sync` reinstalls.
|
||||
|
||||
@@ -13,7 +13,6 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import urllib.request
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -42,34 +41,16 @@ def git(root: Path, *args: str, timeout: int = 15) -> str:
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def update_source(root: Path, port: int = 8000) -> int:
|
||||
"""Return 10 after a fast-forward (caller reloads launcher); never overwrite local work."""
|
||||
try:
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with opener.open(f"http://127.0.0.1:{port}/api/status", timeout=2) as response:
|
||||
status = json.load(response)
|
||||
if (
|
||||
status.get("active_tasks")
|
||||
or status.get("queue")
|
||||
or (status.get("status") == "running" and status.get("pid"))
|
||||
):
|
||||
print(
|
||||
launch_text(
|
||||
"[WAIT] Tasks are active. Finish/stop them before updating dependencies or restarting.",
|
||||
"[等待] 有任务正在执行或排队。请先处理任务,再更新依赖或重启。",
|
||||
)
|
||||
)
|
||||
return 20
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
def update_source(root: Path, *, check_only: bool = False) -> int:
|
||||
"""Return 10 for an available/applied fast-forward, 1 on failure; preserve local work."""
|
||||
if not shutil.which("git"):
|
||||
print(
|
||||
launch_text(
|
||||
"[SKIP] Git is not installed. Continuing with local source; install Git for automatic source updates.",
|
||||
"[跳过] 未安装 Git,继续使用当前源码;安装 Git 后可自动检查更新。",
|
||||
"[ERROR] Install Git before upgrading; local source is preserved.",
|
||||
"[提示] 未安装 Git,无法升级;当前源码保持不变。",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
return 1
|
||||
if not (root / ".git").exists():
|
||||
print(
|
||||
launch_text(
|
||||
@@ -77,8 +58,16 @@ def update_source(root: Path, port: int = 8000) -> int:
|
||||
"[跳过] 旧版源码包不含 Git 信息。请使用新版源码包或 Git 克隆;当前文件保持不变。",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
return 1
|
||||
try:
|
||||
if not check_only and git(root, "status", "--porcelain"):
|
||||
print(
|
||||
launch_text(
|
||||
"[ERROR] Local changes exist; commit or back up first.",
|
||||
"[错误] 发现本地修改;请先提交或备份后再升级。",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
branch = git(root, "symbolic-ref", "--quiet", "--short", "HEAD")
|
||||
remote = git(root, "config", "--get", f"branch.{branch}.remote")
|
||||
merge_ref = git(root, "config", "--get", f"branch.{branch}.merge")
|
||||
@@ -98,45 +87,53 @@ def update_source(root: Path, port: int = 8000) -> int:
|
||||
"[检查] 正在检查当前分支的远程更新...",
|
||||
)
|
||||
)
|
||||
git(root, "fetch", "--no-tags", remote, merge_ref, timeout=45)
|
||||
git(root, "fetch", "--no-tags", remote, merge_ref, timeout=5 if check_only else 45)
|
||||
target = git(root, "rev-parse", "FETCH_HEAD")
|
||||
current = git(root, "rev-parse", "HEAD")
|
||||
if current == target:
|
||||
print(launch_text("[OK] Source is up to date.", "[完成] 源码已是最新版。"))
|
||||
return 0
|
||||
if check_only:
|
||||
print(
|
||||
launch_text(
|
||||
"[UPDATE] A different upstream revision is available. Run update.bat (update-cn.bat for China mirrors).",
|
||||
"[更新] 远程版本有变化;请运行 update-cn.bat 或 update.bat 升级。",
|
||||
)
|
||||
)
|
||||
return 10
|
||||
if git(root, "status", "--porcelain"):
|
||||
print(
|
||||
launch_text(
|
||||
"[SKIP] Upstream differs, but local changes exist. Commit/back up your work before updating; continuing locally.",
|
||||
"[跳过] 发现本地修改;请先提交或备份。此次继续使用本地源码。",
|
||||
"[ERROR] Local changes appeared during the check. Upgrade stopped; files preserved.",
|
||||
"[错误] 检查期间出现本地修改;已停止升级,保留现有文件。",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
return 1
|
||||
ancestor = git(root, "merge-base", "HEAD", target)
|
||||
if ancestor != current:
|
||||
print(
|
||||
launch_text(
|
||||
"[SKIP] Local branch is ahead or diverged. No automatic merge/rebase; continuing locally.",
|
||||
"[跳过] 本地分支领先或分叉,不自动合并;继续使用本地源码。",
|
||||
"[ERROR] Local branch is ahead or diverged. Upgrade stopped; no merge/rebase performed.",
|
||||
"[错误] 本地分支领先或分叉;已停止升级,不自动合并。",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
return 1
|
||||
git(root, "merge", "--ff-only", target, timeout=30)
|
||||
print(
|
||||
launch_text(
|
||||
f"[UPDATED] Source {current[:12]} -> {target[:12]}. Reloading launcher and checking dependencies.",
|
||||
f"[已更新] 源码 {current[:12]} -> {target[:12]},重新载入启动脚本并检查依赖。",
|
||||
f"[UPDATED] Source {current[:12]} -> {target[:12]}. Preparing the updated installation.",
|
||||
f"[已更新] 源码 {current[:12]} -> {target[:12]},准备同步新版环境。",
|
||||
)
|
||||
)
|
||||
return 10
|
||||
except (RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
||||
print(
|
||||
launch_text(
|
||||
f"[WARN] {exc if isinstance(exc, RuntimeError) else 'Git update timed out.'} Continuing with local source.",
|
||||
"[提示] Git 更新失败或超时;请检查网络、凭据和仓库状态。继续使用当前源码。",
|
||||
f"[WARN] {exc if isinstance(exc, RuntimeError) else 'Git check failed or timed out.'} Local source is preserved.",
|
||||
"[提示] Git 检查/更新失败或超时;请检查网络、凭据和仓库状态。当前源码保持不变。",
|
||||
)
|
||||
)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def _atomic_write(path: Path, text: str) -> None:
|
||||
@@ -406,12 +403,13 @@ def frontend_fingerprint(root: Path) -> str:
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=["source", "mcp", "frontend-check", "frontend-mark"])
|
||||
parser.add_argument(
|
||||
"action", choices=["source", "source-check", "mcp", "frontend-check", "frontend-mark"]
|
||||
)
|
||||
parser.add_argument("--root", type=Path, default=ROOT)
|
||||
parser.add_argument("--port", type=int, default=8000)
|
||||
args = parser.parse_args()
|
||||
if args.action == "source":
|
||||
sys.exit(update_source(args.root, args.port))
|
||||
if args.action in {"source", "source-check"}:
|
||||
sys.exit(update_source(args.root, check_only=args.action == "source-check"))
|
||||
elif args.action == "mcp":
|
||||
sys.exit(refresh_mcp(args.root))
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ProjectRoot,
|
||||
[ValidateSet('Official', 'China')][string]$Mirror,
|
||||
[switch]$Help,
|
||||
[switch]$Staged
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
||||
$env:PYTHONUTF8 = '1'
|
||||
$env:PYTHONIOENCODING = 'utf-8'
|
||||
if ($Help) {
|
||||
Write-Host '用法:update.bat 或 update-cn.bat [-Help]'
|
||||
Write-Host '升级会终止本项目的 ARTEMIS 服务、MCP 和任务进程;保留配置、密钥与历史资料。'
|
||||
Write-Host '随后快进更新源码,同步依赖、网页构建和 Codex MCP;完成后请重新启动并重载 MCP。'
|
||||
exit 0
|
||||
}
|
||||
if (-not $ProjectRoot) { $ProjectRoot = Split-Path $PSScriptRoot -Parent }
|
||||
$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path
|
||||
. (Join-Path $PSScriptRoot 'installation_lock.ps1')
|
||||
|
||||
if (-not $Staged) {
|
||||
$lock = $null
|
||||
$stage = $null
|
||||
$stageCreated = $false
|
||||
$code = 1
|
||||
try {
|
||||
$lock = Enter-ArtemisInstallationLock -ProjectRoot $ProjectRoot
|
||||
# Every program used before the merge is frozen outside the checkout.
|
||||
$stage = Join-Path ([IO.Path]::GetTempPath()) ('artemis-upgrade-' + [Guid]::NewGuid().ToString('N'))
|
||||
$fullStage = [IO.Path]::GetFullPath($stage)
|
||||
$rootPrefix = [IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\') + '\'
|
||||
if ($fullStage.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'TEMP must be outside the project directory.' }
|
||||
New-Item -ItemType Directory -Path $stage | Out-Null
|
||||
$stageCreated = $true
|
||||
foreach ($name in @('upgrade.ps1', 'installation_lock.ps1', 'upgrade_processes.ps1', 'update_installation.py')) {
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination (Join-Path $stage $name)
|
||||
}
|
||||
$forward = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $stage 'upgrade.ps1'), '-Staged', '-ProjectRoot', $ProjectRoot)
|
||||
if ($Mirror) { $forward += @('-Mirror', $Mirror) }
|
||||
& powershell.exe @forward
|
||||
$code = $LASTEXITCODE
|
||||
} catch {
|
||||
Write-Host "[升级失败] $_" -ForegroundColor Red
|
||||
} finally {
|
||||
# Only remove the unique, validated staging directory created above.
|
||||
if ($stageCreated -and (Test-Path -LiteralPath $stage)) { Remove-Item -LiteralPath $stage -Recurse -Force }
|
||||
Exit-ArtemisInstallationLock -Mutex $lock
|
||||
}
|
||||
exit $code
|
||||
}
|
||||
|
||||
try {
|
||||
Set-Location -LiteralPath $PSScriptRoot
|
||||
$python = Join-Path $ProjectRoot '.venv\Scripts\python.exe'
|
||||
if (-not (Test-Path -LiteralPath $python)) { throw '尚未安装项目 Python 环境;请先运行 start.bat 或 start-cn.bat。' }
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { throw '未安装 Git,无法升级。' }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot '.git'))) { throw '此目录不含 Git 信息;请改用 Git 克隆或新版发布源码包。' }
|
||||
# Refuse dirty checkouts before terminating the user's running processes.
|
||||
$localChanges = & git -C $ProjectRoot status --porcelain 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw '无法检查 Git 工作区。' }
|
||||
if ($localChanges) { throw '发现本地修改;请先提交或备份,未终止任何任务。' }
|
||||
Write-Host '======================================================'
|
||||
Write-Host 'ARTEMIS 独立升级:将终止当前项目的服务、MCP 及任务。' -ForegroundColor Yellow
|
||||
. (Join-Path $PSScriptRoot 'upgrade_processes.ps1')
|
||||
Stop-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot
|
||||
& $python (Join-Path $PSScriptRoot 'update_installation.py') source --root $ProjectRoot
|
||||
if ($LASTEXITCODE -notin @(0, 10)) { throw '源码升级未完成;配置和历史资料已保留。' }
|
||||
Stop-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot
|
||||
# Load the new launcher only AFTER the frozen Git updater has exited.
|
||||
$prepare = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $ProjectRoot 'scripts\start.ps1'), '-PrepareOnly', '-SkipUpdate', '-NoOpen')
|
||||
if ($Mirror) { $prepare += @('-Mirror', $Mirror) }
|
||||
$previousChild = $env:ARTEMIS_UPGRADE_CHILD
|
||||
$env:ARTEMIS_UPGRADE_CHILD = '1'
|
||||
try { & powershell.exe @prepare; $prepareCode = $LASTEXITCODE }
|
||||
finally { $env:ARTEMIS_UPGRADE_CHILD = $previousChild }
|
||||
if ($prepareCode -ne 0) { throw '新版源码已保留,但环境同步失败;请处理上方错误后重新运行升级脚本。' }
|
||||
Write-Host '[升级完成] 源码、依赖、网页及 Codex MCP 已检查并同步。' -ForegroundColor Green
|
||||
Write-Host '[下一步] 运行启动脚本,并在 Codex 中重载 MCP 或重启 Codex。'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "[升级失败] $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Process ownership is determined by checkout paths, never by a port or name alone.
|
||||
function Get-ArtemisUpgradeProcesses {
|
||||
param([string]$ProjectRoot, [object[]]$Processes, [int[]]$ProtectedIds = @())
|
||||
$root = [IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\') + '\'
|
||||
$venv = $root + '.venv\'
|
||||
$selected = @{}
|
||||
foreach ($process in $Processes) {
|
||||
if ($ProtectedIds -contains [int]$process.ProcessId) { continue }
|
||||
$executable = [string]$process.ExecutablePath
|
||||
$command = [string]$process.CommandLine
|
||||
$python = $process.Name -match '^(python(w)?|artemis(-admin)?)\.exe$'
|
||||
$inVenv = $executable.StartsWith($venv, [StringComparison]::OrdinalIgnoreCase)
|
||||
$explicitRoot = $command.Replace('/', '\').IndexOf($root, [StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
$entrypoint = $command -match '(?i)(?:-m\s+(?:artemis(?:\s|\.|$)|apps\.admin_console(?:\.|\s)|mcp_server(?:\.|\s|$))|[\\/]artemis(?:\.exe|[\\/]__main__\.py)|[\\/]mcp_server[\\/])'
|
||||
if (($python -and $inVenv) -or ($explicitRoot -and $entrypoint -and ($python -or $process.Name -eq 'uv.exe'))) {
|
||||
$selected[[int]$process.ProcessId] = $process
|
||||
}
|
||||
}
|
||||
do {
|
||||
$added = $false
|
||||
foreach ($process in $Processes) {
|
||||
$id = [int]$process.ProcessId
|
||||
if ($selected.ContainsKey($id) -or $ProtectedIds -contains $id) { continue }
|
||||
# Shared device services and desktop applications can outlive a task.
|
||||
if ($process.Name -match '^(adb|codex|chrome|msedge|firefox|emulator|MuMu.*|Nemu.*|Nox.*|MEmu.*|LdVBox.*|HD-Player)\.exe$') { continue }
|
||||
if ($selected.ContainsKey([int]$process.ParentProcessId)) {
|
||||
$selected[$id] = $process
|
||||
$added = $true
|
||||
}
|
||||
}
|
||||
} while ($added)
|
||||
return @($selected.Values)
|
||||
}
|
||||
|
||||
function Stop-ArtemisUpgradeProcesses {
|
||||
param([string]$ProjectRoot)
|
||||
$snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop)
|
||||
$protected = @($PID)
|
||||
$parentId = $PID
|
||||
do {
|
||||
$parent = $snapshot | Where-Object { [int]$_.ProcessId -eq $parentId } | Select-Object -First 1
|
||||
$parentId = if ($parent) { [int]$parent.ParentProcessId } else { 0 }
|
||||
if ($parentId -and $protected -notcontains $parentId) { $protected += $parentId } else { break }
|
||||
} while ($true)
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(10)
|
||||
do {
|
||||
$targets = @(Get-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot -Processes $snapshot -ProtectedIds $protected)
|
||||
if (-not $targets.Count) { return }
|
||||
foreach ($target in $targets) {
|
||||
$id = [int]$target.ProcessId
|
||||
$current = Get-CimInstance Win32_Process -Filter "ProcessId = $id" -ErrorAction Stop
|
||||
if (-not $current -or $current.CreationDate -ne $target.CreationDate) { continue }
|
||||
Write-Host "[STOP] ARTEMIS PID $id ($($target.Name))"
|
||||
try { Stop-Process -Id $id -Force -ErrorAction Stop }
|
||||
catch {
|
||||
if (Get-CimInstance Win32_Process -Filter "ProcessId = $id" -ErrorAction Stop) { throw }
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 200
|
||||
$snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop)
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
if (@(Get-ArtemisUpgradeProcesses -ProjectRoot $ProjectRoot -Processes $snapshot -ProtectedIds $protected).Count) {
|
||||
throw 'ARTEMIS processes are still running. Source/dependency updates were not started.'
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user