From 3b6d22d817f4ec670d909e5aeebde651240a8ae7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 13 Jun 2026 16:17:25 +0000 Subject: [PATCH] fix: isolate personal wechat wcf host --- config/platform-infra/wechat-archive.yaml | 2 +- scripts/windows/personal-wechat/prepare.ps1 | 245 ++++++++++++++------ scripts/windows/personal-wechat/start.ps1 | 93 ++++++-- scripts/windows/personal-wechat/status.ps1 | 40 +++- scripts/windows/personal-wechat/wcf_host.py | 85 ++++++- 5 files changed, 359 insertions(+), 106 deletions(-) diff --git a/config/platform-infra/wechat-archive.yaml b/config/platform-infra/wechat-archive.yaml index 2d8bc67e..61cf4c8a 100644 --- a/config/platform-infra/wechat-archive.yaml +++ b/config/platform-infra/wechat-archive.yaml @@ -65,7 +65,7 @@ personalWechatIngress: installerAsset: WeChatSetup-3.9.12.51.exe installMode: silent-nsis-with-ui-fallback installRoot: C:/UniDesk/personal-wechat/wechat-3.9.12.51 - dataRoot: C:/UniDesk/personal-wechat/wechat-data + dataRoot: C:/UniDesk/personal-wechat/data/profile autoUpdatePolicy: disabled wcfHost: releaseTag: v39.5.2 diff --git a/scripts/windows/personal-wechat/prepare.ps1 b/scripts/windows/personal-wechat/prepare.ps1 index 4eb033b3..17029560 100644 --- a/scripts/windows/personal-wechat/prepare.ps1 +++ b/scripts/windows/personal-wechat/prepare.ps1 @@ -2,6 +2,9 @@ $ErrorActionPreference = "Stop" $Root = "C:\UniDesk\personal-wechat" $WechatRoot = Join-Path $Root "wechat-3.9.12.51" +$ExtractRoot = Join-Path $Root "wechat-3.9.12.51-extracted" +$VersionRoot = Join-Path $ExtractRoot "[3.9.12.51]" +$DataRoot = Join-Path $Root "data\profile" $WcfRoot = Join-Path $Root "wcf\v39.5.2" $StateRoot = Join-Path $Root "wcf-state" $DownloadRoot = Join-Path $Root "downloads" @@ -10,8 +13,9 @@ $ProgressLog = Join-Path $StateRoot "prepare-progress.log" $Python = "C:\ProgramData\miniconda3\python.exe" $PipIndex = "http://mirrors.aliyun.com/pypi/simple/" $ReleaseBase = "https://github.com/lich0821/WeChatFerry/releases/download/v39.5.2" +$RegistryPath = "HKCU:\Software\Tencent\WeChat" -New-Item -ItemType Directory -Force $WechatRoot,$WcfRoot,$StateRoot,$DownloadRoot | Out-Null +New-Item -ItemType Directory -Force $WechatRoot,$ExtractRoot,$VersionRoot,$DataRoot,$WcfRoot,$StateRoot,$DownloadRoot | Out-Null $StartedAt = (Get-Date).ToUniversalTime().ToString("o") function Write-PrepareProgress { @@ -32,13 +36,13 @@ trap { error = $message valuesPrinted = $false } - $failed | ConvertTo-Json -Depth 6 | Set-Content -Encoding utf8 $PrepareResult + $failed | ConvertTo-Json -Depth 8 | Set-Content -Encoding utf8 $PrepareResult exit 1 } function Download-IfMissing { param([string]$Url, [string]$Path) - if (Test-Path $Path) { return } + if (Test-Path -LiteralPath $Path) { return } Write-PrepareProgress "download-start $Url" $tmp = "$Path.part" Invoke-WebRequest -Uri $Url -OutFile $tmp -UseBasicParsing @@ -46,18 +50,155 @@ function Download-IfMissing { Write-PrepareProgress "download-done $Path" } -function Find-WeChatExe { +function Get-SevenZipPath { $candidates = @( - (Join-Path $WechatRoot "WeChat.exe"), - "C:\Program Files (x86)\Tencent\WeChat\WeChat.exe", - "C:\Program Files\Tencent\WeChat\WeChat.exe" + "C:\Program Files\7-Zip\7z.exe", + "C:\Program Files (x86)\7-Zip\7z.exe", + "7z.exe" ) foreach ($candidate in $candidates) { - if (Test-Path $candidate) { return $candidate } + $cmd = Get-Command $candidate -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + if (Test-Path -LiteralPath $candidate) { return $candidate } } return $null } +function Find-DedicatedWeChatExe { + $candidates = @( + (Join-Path $VersionRoot "WeChat.exe"), + (Join-Path $ExtractRoot "WeChat.exe"), + (Join-Path $WechatRoot "WeChat.exe") + ) + foreach ($candidate in $candidates) { + if (!(Test-Path -LiteralPath $candidate)) { continue } + $dir = Split-Path -Parent $candidate + if (Test-Path -LiteralPath (Join-Path $dir "WeChatWin.dll")) { return $candidate } + } + return $null +} + +function Ensure-WeChatExtracted { + param([string]$Installer) + $wechatExe = Find-DedicatedWeChatExe + if ($wechatExe) { return [ordered]@{ mode = "dedicated-existing"; wechatExe = $wechatExe } } + + $sevenZip = Get-SevenZipPath + if (!$sevenZip) { throw "7-Zip is required to extract the WeChat installer without administrator elevation" } + $extractLog = Join-Path $StateRoot "7z-extract.log" + Write-PrepareProgress "wechat-extract-start $Installer" + & $sevenZip x -y "-o$ExtractRoot" $Installer > $extractLog 2>&1 + if ($LASTEXITCODE -ne 0) { throw "7-Zip extract failed; see $extractLog" } + $wechatExe = Find-DedicatedWeChatExe + if (!$wechatExe) { throw "Extracted installer did not produce dedicated WeChat.exe with WeChatWin.dll" } + Write-PrepareProgress "wechat-extract-done $wechatExe" + return [ordered]@{ mode = "nsis-extracted-hkcu-registry-shim"; wechatExe = $wechatExe; extractLog = $extractLog } +} + +function Ensure-IsolatedProfile { + $appData = Join-Path $DataRoot "AppData\Roaming" + $localAppData = Join-Path $DataRoot "AppData\Local" + $documents = Join-Path $DataRoot "Documents" + $temp = Join-Path $localAppData "Temp" + New-Item -ItemType Directory -Force $appData,$localAppData,$documents,$temp | Out-Null + return [ordered]@{ + dataRoot = $DataRoot + appData = $appData + localAppData = $localAppData + documents = $documents + temp = $temp + } +} + +function Ensure-WeChatRegistryShim { + param([string]$WechatExe) + $installPath = Split-Path -Parent $WechatExe + New-Item -Path $RegistryPath -Force | Out-Null + Set-ItemProperty -Path $RegistryPath -Name InstallPath -Value ($installPath + "\") + Set-ItemProperty -Path $RegistryPath -Name Version -Value 0x03090c33 -Type DWord + return $installPath + "\" +} + +function Install-Wcferry { + if (!(Test-Path -LiteralPath $Python)) { throw "Expected Python not found: $Python" } + $pipOut = Join-Path $StateRoot "prepare-pip.stdout.log" + $pipErr = Join-Path $StateRoot "prepare-pip.stderr.log" + Write-PrepareProgress "pip-install-start" + & $Python -m pip install --trusted-host mirrors.aliyun.com --index-url $PipIndex --timeout 120 --retries 8 "wcferry==39.5.2.0" > $pipOut 2> $pipErr + if ($LASTEXITCODE -ne 0) { throw "pip install wcferry failed; see $pipErr" } + Write-PrepareProgress "pip-install-done" + + $probePy = Join-Path $StateRoot "prepare-probe.py" + @' +import importlib.metadata +import json +import os +import wcferry + +payload = { + "ok": True, + "wcferryVersion": importlib.metadata.version("wcferry"), + "packageRoot": os.path.abspath(os.path.dirname(wcferry.__file__)), +} +print(json.dumps(payload, ensure_ascii=False)) +'@ | Set-Content -Encoding utf8 $probePy + $pyProbe = (& $Python $probePy) | ConvertFrom-Json + return [ordered]@{ + ok = [bool]$pyProbe.ok + version = $pyProbe.wcferryVersion + packageRoot = $pyProbe.packageRoot + stdout = $pipOut + stderr = $pipErr + } +} + +function Sync-WcfDlls { + param([string]$PackageRoot) + $dlls = @() + foreach ($name in @("sdk.dll","spy.dll","spy_debug.dll")) { + $src = Join-Path $WcfRoot $name + $dst = Join-Path $PackageRoot $name + if (!(Test-Path -LiteralPath $src)) { throw "Missing WCF release asset: $src" } + if (!(Test-Path -LiteralPath $dst)) { throw "Missing wcferry package dll target: $dst" } + $backup = "$dst.unidesk-bak" + if (!(Test-Path -LiteralPath $backup)) { + Copy-Item -LiteralPath $dst -Destination $backup -Force + } + Copy-Item -LiteralPath $src -Destination $dst -Force + $srcItem = Get-Item -LiteralPath $src + $dstItem = Get-Item -LiteralPath $dst + $dlls += [ordered]@{ + name = $name + sourceBytes = $srcItem.Length + targetBytes = $dstItem.Length + targetVersion = $dstItem.VersionInfo.FileVersion + backup = $backup + } + } + return $dlls +} + +function Ensure-Firewall { + try { + if (-not (Get-NetFirewallRule -DisplayName "UniDesk Personal WeChat WCF 10086" -ErrorAction SilentlyContinue)) { + New-NetFirewallRule ` + -DisplayName "UniDesk Personal WeChat WCF 10086" ` + -Direction Inbound ` + -Action Allow ` + -Protocol TCP ` + -LocalPort 10086,10087 ` + -RemoteAddress 172.26.0.0/16,10.42.0.0/16,127.0.0.1 ` + -Profile Any ` + -ErrorAction Stop | Out-Null + return [ordered]@{ ok = $true; applied = $true } + } + return [ordered]@{ ok = $true; applied = $false; alreadyPresent = $true } + } catch { + Write-PrepareProgress "firewall-skip $($_.Exception.Message)" + return [ordered]@{ ok = $false; skipped = $true; reason = $_.Exception.Message } + } +} + Write-PrepareProgress "prepare-start" $installer = Join-Path $DownloadRoot "WeChatSetup-3.9.12.51.exe" Download-IfMissing "$ReleaseBase/WeChatSetup-3.9.12.51.exe" $installer @@ -65,82 +206,40 @@ Download-IfMissing "$ReleaseBase/sdk.dll" (Join-Path $WcfRoot "sdk.dll") Download-IfMissing "$ReleaseBase/spy.dll" (Join-Path $WcfRoot "spy.dll") Download-IfMissing "$ReleaseBase/spy_debug.dll" (Join-Path $WcfRoot "spy_debug.dll") -if (!(Test-Path $Python)) { - throw "Expected Python not found: $Python" -} - -$pipOut = Join-Path $StateRoot "prepare-pip.stdout.log" -$pipErr = Join-Path $StateRoot "prepare-pip.stderr.log" -Write-PrepareProgress "pip-install-start" -& $Python -m pip install --trusted-host mirrors.aliyun.com --index-url $PipIndex "wcferry==39.5.2.0" > $pipOut 2> $pipErr -if ($LASTEXITCODE -ne 0) { throw "pip install wcferry failed" } -Write-PrepareProgress "pip-install-done" - Copy-Item -Force "$PSScriptRoot\wcf_host.py" (Join-Path $WcfRoot "wcf_host.py") -if (-not (Get-NetFirewallRule -DisplayName "UniDesk Personal WeChat WCF 10086" -ErrorAction SilentlyContinue)) { - New-NetFirewallRule ` - -DisplayName "UniDesk Personal WeChat WCF 10086" ` - -Direction Inbound ` - -Action Allow ` - -Protocol TCP ` - -LocalPort 10086,10087 ` - -RemoteAddress 172.26.0.0/16,10.42.0.0/16,127.0.0.1 ` - -Profile Any ` - -ErrorAction SilentlyContinue | Out-Null -} - -$wechatExe = Find-WeChatExe -$installAttempted = $false -$installExitCode = $null -$installStdout = Join-Path $StateRoot "prepare-wechat-installer.stdout.log" -$installStderr = Join-Path $StateRoot "prepare-wechat-installer.stderr.log" -if (-not $wechatExe) { - $installAttempted = $true - $args = @("/S", "/D=$WechatRoot") - Write-PrepareProgress "wechat-install-start $installer" - $proc = Start-Process -FilePath $installer -ArgumentList $args -Wait -PassThru -RedirectStandardOutput $installStdout -RedirectStandardError $installStderr -WindowStyle Hidden - $installExitCode = $proc.ExitCode - Write-PrepareProgress "wechat-install-exit $installExitCode" - Start-Sleep -Seconds 5 - $wechatExe = Find-WeChatExe -} - -$probePy = Join-Path $StateRoot "prepare-probe.py" -@' -import importlib.metadata -import json - -payload = {"ok": True} -try: - payload["wcferryVersion"] = importlib.metadata.version("wcferry") -except Exception as exc: - payload = {"ok": False, "error": f"{type(exc).__name__}: {exc}"} -print(json.dumps(payload, ensure_ascii=False)) -'@ | Set-Content -Encoding utf8 $probePy -$pyProbeJson = & $Python $probePy -$pyProbe = $pyProbeJson | ConvertFrom-Json +$wechat = Ensure-WeChatExtracted $installer +$profile = Ensure-IsolatedProfile +$installPath = Ensure-WeChatRegistryShim $wechat.wechatExe +$wcferry = Install-Wcferry +$dlls = Sync-WcfDlls $wcferry.packageRoot +$firewall = Ensure-Firewall $summary = [ordered]@{ - ok = [bool]($pyProbe.ok -and $wechatExe) + ok = [bool]($wechat.wechatExe -and $wcferry.ok) startedAt = $StartedAt finishedAt = (Get-Date).ToUniversalTime().ToString("o") root = $Root + mode = $wechat.mode wechatInstaller = $installer wechatRoot = $WechatRoot - wechatExe = $wechatExe - installAttempted = $installAttempted - installExitCode = $installExitCode - installLogs = [ordered]@{ - stdout = $installStdout - stderr = $installStderr - } + extractRoot = $ExtractRoot + versionRoot = $VersionRoot + wechatExe = $wechat.wechatExe + installPath = $installPath + registryKey = $RegistryPath + requiredVersion = "3.9.12.51" + profile = $profile wcfRoot = $WcfRoot stateRoot = $StateRoot python = $Python - wcferryVersion = $pyProbe.wcferryVersion - next = if ($wechatExe) { "Run start.ps1 and scan the WeChat login QR." } else { "WeChat silent install did not produce WeChat.exe; run the installer UI from the interactive Windows session, then re-run prepare.ps1." } + wcferryVersion = $wcferry.version + wcferryPackageRoot = $wcferry.packageRoot + dllOverride = $dlls + firewall = $firewall + valuesPrinted = $false + next = "Run start.ps1 and scan the isolated WeChat login QR." } Write-PrepareProgress "prepare-done ok=$($summary.ok)" -$summary | ConvertTo-Json -Depth 6 | Set-Content -Encoding utf8 $PrepareResult -$summary | ConvertTo-Json -Depth 6 +$summary | ConvertTo-Json -Depth 10 | Set-Content -Encoding utf8 $PrepareResult +$summary | ConvertTo-Json -Depth 10 diff --git a/scripts/windows/personal-wechat/start.ps1 b/scripts/windows/personal-wechat/start.ps1 index 7970b379..a838567c 100644 --- a/scripts/windows/personal-wechat/start.ps1 +++ b/scripts/windows/personal-wechat/start.ps1 @@ -3,28 +3,91 @@ $ErrorActionPreference = "Stop" $Root = "C:\UniDesk\personal-wechat" $WcfRoot = Join-Path $Root "wcf\v39.5.2" $StateRoot = Join-Path $Root "wcf-state" +$DataRoot = Join-Path $Root "data\profile" +$AppData = Join-Path $DataRoot "AppData\Roaming" +$LocalAppData = Join-Path $DataRoot "AppData\Local" +$Documents = Join-Path $DataRoot "Documents" +$TempRoot = Join-Path $LocalAppData "Temp" $Python = "C:\ProgramData\miniconda3\python.exe" $Script = Join-Path $WcfRoot "wcf_host.py" $PidFile = Join-Path $StateRoot "wcf-host.pid" $Stdout = Join-Path $StateRoot "wcf-host.stdout.log" $Stderr = Join-Path $StateRoot "wcf-host.stderr.log" +$Runner = Join-Path $StateRoot "wcf-host-runner.cmd" -New-Item -ItemType Directory -Force $StateRoot | Out-Null +New-Item -ItemType Directory -Force $StateRoot,$DataRoot,$AppData,$LocalAppData,$Documents,$TempRoot | Out-Null -if (Test-Path $PidFile) { - $oldPid = Get-Content $PidFile -ErrorAction SilentlyContinue - if ($oldPid) { - $existing = Get-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue - if ($existing) { - [pscustomobject]@{ ok = $true; alreadyRunning = $true; pid = $existing.Id; stdout = $Stdout; stderr = $Stderr } | ConvertTo-Json -Depth 4 - exit 0 - } - } +function Get-WcfProcessByPidFile { + if (!(Test-Path -LiteralPath $PidFile)) { return $null } + $oldPid = (Get-Content $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1) + if (!$oldPid) { return $null } + return Get-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue } -$env:WCF_COMMAND_PORT = "10086" -$env:WCF_STATE_ROOT = $StateRoot -$proc = Start-Process -FilePath $Python -ArgumentList @($Script) -WorkingDirectory $WcfRoot -WindowStyle Normal -PassThru -RedirectStandardOutput $Stdout -RedirectStandardError $Stderr -$proc.Id | Set-Content -Encoding ascii $PidFile +$existing = Get-WcfProcessByPidFile +if ($existing) { + [pscustomobject]@{ + ok = $true + alreadyRunning = $true + pid = $existing.Id + stdout = $Stdout + stderr = $Stderr + status = (Join-Path $StateRoot "status.json") + dataRoot = $DataRoot + } | ConvertTo-Json -Depth 6 + exit 0 +} -[pscustomobject]@{ ok = $true; started = $true; pid = $proc.Id; stdout = $Stdout; stderr = $Stderr; status = (Join-Path $StateRoot "status.json") } | ConvertTo-Json -Depth 4 +# Keep this sandbox isolated from the daily Weixin/WeChat install. Only stop +# processes that were launched from the UniDesk personal-wechat tree/profile. +Get-CimInstance Win32_Process | + Where-Object { + ($_.ExecutablePath -like "$Root\*") -or + ($_.CommandLine -like "*$WcfRoot\wcf_host.py*") -or + ($_.Name -eq "WeChatAppEx.exe" -and $_.CommandLine -like "*$DataRoot*") + } | + ForEach-Object { + try { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } catch {} + } +Start-Sleep -Seconds 2 + +Remove-Item -Force (Join-Path $StateRoot "status.json") -ErrorAction SilentlyContinue +Remove-Item -Force (Join-Path $WcfRoot "injector.log") -ErrorAction SilentlyContinue + +@" +@echo off +set WCF_COMMAND_PORT=10086 +set WCF_STATE_ROOT=$StateRoot +set USERPROFILE=$DataRoot +set APPDATA=$AppData +set LOCALAPPDATA=$LocalAppData +set HOMEDRIVE=C: +set HOMEPATH=\UniDesk\personal-wechat\data\profile +set TEMP=$TempRoot +set TMP=$TempRoot +mkdir "$TempRoot" 2>nul +cd /d "$WcfRoot" +"$Python" "$Script" > "$Stdout" 2> "$Stderr" +"@ | Set-Content -Encoding ASCII $Runner + +$cmd = '/c start "" /min "' + $Runner + '"' +$launcher = Start-Process -FilePath "cmd.exe" -ArgumentList $cmd -WindowStyle Hidden -PassThru +Start-Sleep -Seconds 3 +$pidValue = $null +if (Test-Path -LiteralPath $PidFile) { + $pidValue = (Get-Content $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +[pscustomobject]@{ + ok = $true + started = $true + launcherPid = $launcher.Id + pid = $pidValue + runner = $Runner + stdout = $Stdout + stderr = $Stderr + status = (Join-Path $StateRoot "status.json") + dataRoot = $DataRoot + appData = $AppData + localAppData = $LocalAppData +} | ConvertTo-Json -Depth 6 diff --git a/scripts/windows/personal-wechat/status.ps1 b/scripts/windows/personal-wechat/status.ps1 index fcef8c06..d15936cf 100644 --- a/scripts/windows/personal-wechat/status.ps1 +++ b/scripts/windows/personal-wechat/status.ps1 @@ -1,7 +1,9 @@ $ErrorActionPreference = "Continue" $Root = "C:\UniDesk\personal-wechat" +$WcfRoot = Join-Path $Root "wcf\v39.5.2" $StateRoot = Join-Path $Root "wcf-state" +$DataRoot = Join-Path $Root "data\profile" $PidFile = Join-Path $StateRoot "wcf-host.pid" $StatusFile = Join-Path $StateRoot "status.json" $PrepareResultFile = Join-Path $StateRoot "prepare-result.json" @@ -10,34 +12,45 @@ $PrepareStderr = Join-Path $StateRoot "prepare.stderr.log" $PrepareProgress = Join-Path $StateRoot "prepare-progress.log" $Stdout = Join-Path $StateRoot "wcf-host.stdout.log" $Stderr = Join-Path $StateRoot "wcf-host.stderr.log" +$HostLog = Join-Path $StateRoot "wcf-host.log" +$InjectorLog = Join-Path $WcfRoot "injector.log" + function Read-JsonFile { param([string]$Path) - if (!(Test-Path $Path)) { return $null } + if (!(Test-Path -LiteralPath $Path)) { return $null } try { return Get-Content $Path -Raw | ConvertFrom-Json } catch { return @{ parseError = $_.Exception.Message } } } + function Tail-Text { param([string]$Path) - if (!(Test-Path $Path)) { return "" } + if (!(Test-Path -LiteralPath $Path)) { return "" } try { return (Get-Content $Path -Tail 80 -ErrorAction SilentlyContinue) -join "`n" } catch { return $_.Exception.Message } } + $pidValue = $null $running = $false -if (Test-Path $PidFile) { - $pidValue = Get-Content $PidFile -ErrorAction SilentlyContinue +if (Test-Path -LiteralPath $PidFile) { + $pidValue = (Get-Content $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1) if ($pidValue) { $running = [bool](Get-Process -Id ([int]$pidValue) -ErrorAction SilentlyContinue) } } + $status = Read-JsonFile $StatusFile $prepare = Read-JsonFile $PrepareResultFile -$prepareProc = Get-Process -Name powershell -ErrorAction SilentlyContinue | - Where-Object { $_.Path -like "*powershell*" -or $_.ProcessName -like "*powershell*" } | - Sort-Object StartTime -Descending | - Select-Object -First 5 Id,ProcessName,StartTime +$ports = Get-NetTCPConnection -LocalPort 10086,10087 -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,State,OwningProcess +$personalProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + ($_.ExecutablePath -like "$Root\*") -or + ($_.CommandLine -like "*$WcfRoot\wcf_host.py*") -or + ($_.Name -eq "WeChatAppEx.exe" -and $_.CommandLine -like "*$DataRoot*") + } | + Select-Object ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine + [pscustomobject]@{ ok = $true pid = $pidValue @@ -45,7 +58,6 @@ $prepareProc = Get-Process -Name powershell -ErrorAction SilentlyContinue | prepared = if ($prepare) { [bool]$prepare.ok } else { $false } prepare = $prepare prepareRuntime = [ordered]@{ - candidateProcesses = $prepareProc stdout = $PrepareStdout stderr = $PrepareStderr progress = $PrepareProgress @@ -54,17 +66,23 @@ $prepareProc = Get-Process -Name powershell -ErrorAction SilentlyContinue | progressTail = Tail-Text $PrepareProgress } status = $status - ports = Get-NetTCPConnection -LocalPort 10086,10087 -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,State,OwningProcess + ports = $ports + personalProcesses = $personalProcesses logs = [ordered]@{ stdout = $Stdout stderr = $Stderr + host = $HostLog + injector = $InjectorLog stdoutTail = Tail-Text $Stdout stderrTail = Tail-Text $Stderr + hostTail = Tail-Text $HostLog + injectorTail = Tail-Text $InjectorLog } paths = [ordered]@{ root = $Root + dataRoot = $DataRoot stateRoot = $StateRoot statusFile = $StatusFile prepareResultFile = $PrepareResultFile } -} | ConvertTo-Json -Depth 8 +} | ConvertTo-Json -Depth 10 diff --git a/scripts/windows/personal-wechat/wcf_host.py b/scripts/windows/personal-wechat/wcf_host.py index 806f7d51..ac2f6580 100644 --- a/scripts/windows/personal-wechat/wcf_host.py +++ b/scripts/windows/personal-wechat/wcf_host.py @@ -5,6 +5,7 @@ import sys import time from datetime import datetime, timezone +import wcferry.client as wcf_client from wcferry import Wcf @@ -15,10 +16,23 @@ DEBUG = os.environ.get("WCF_DEBUG", "false").lower() == "true" STOP = False +class WcfProcessExit(Exception): + def __init__(self, code): + super().__init__(f"wcferry requested process exit: {code}") + self.code = code + + def now_iso(): return datetime.now(timezone.utc).isoformat() +def write_json(path, payload): + tmp = f"{path}.tmp" + with open(tmp, "w", encoding="utf-8") as fp: + json.dump(payload, fp, ensure_ascii=False, indent=2, sort_keys=True) + os.replace(tmp, path) + + def log(event, **fields): os.makedirs(STATE_ROOT, exist_ok=True) payload = {"ts": now_iso(), "event": event, **fields} @@ -26,6 +40,20 @@ def log(event, **fields): print(line, flush=True) with open(os.path.join(STATE_ROOT, "wcf-host.log"), "a", encoding="utf-8") as fp: fp.write(line + "\n") + fp.flush() + + +def write_state(**fields): + os.makedirs(STATE_ROOT, exist_ok=True) + payload = { + "ts": now_iso(), + "isLogin": None, + "qrcodePresent": False, + "commandPort": PORT, + "messagePort": PORT + 1, + **fields, + } + write_json(os.path.join(STATE_ROOT, "status.json"), payload) def handle_signal(_signum, _frame): @@ -33,13 +61,58 @@ def handle_signal(_signum, _frame): STOP = True +def patch_wcferry_exit(): + def raise_exit(code=0): + raise WcfProcessExit(code) + + wcf_client.os._exit = raise_exit + + signal.signal(signal.SIGTERM, handle_signal) signal.signal(signal.SIGINT, handle_signal) +def connect_wcf(): + patch_wcferry_exit() + attempt = 0 + while not STOP: + attempt += 1 + local_init = attempt == 1 or attempt % 15 == 0 + mode = "local-init" if local_init else "remote-dial" + try: + write_state(phase="connecting", connectAttempt=attempt, connectMode=mode) + log("wcf-host-connect-attempt", attempt=attempt, mode=mode, port=PORT) + if local_init: + wcf = Wcf(port=PORT, debug=DEBUG, block=False) + else: + wcf = Wcf(host="127.0.0.1", port=PORT, debug=DEBUG, block=False) + log("wcf-host-connected", attempt=attempt, mode=mode, port=PORT) + return wcf + except WcfProcessExit as exc: + log("wcf-host-connect-retry", attempt=attempt, mode=mode, requestedExit=exc.code) + except Exception as exc: + log("wcf-host-connect-error", attempt=attempt, mode=mode, error=f"{type(exc).__name__}: {exc}"[:1000]) + time.sleep(min(2 + attempt, 10)) + return None + + def main(): - log("wcf-host-start", port=PORT, debug=DEBUG) - wcf = Wcf(port=PORT, debug=DEBUG, block=False) + os.makedirs(STATE_ROOT, exist_ok=True) + with open(os.path.join(STATE_ROOT, "wcf-host.pid"), "w", encoding="ascii") as fp: + fp.write(str(os.getpid())) + log( + "wcf-host-start", + port=PORT, + debug=DEBUG, + pid=os.getpid(), + appData=os.environ.get("APPDATA", ""), + userProfile=os.environ.get("USERPROFILE", ""), + ) + wcf = connect_wcf() + if wcf is None: + log("wcf-host-stop-before-connect") + return + while not STOP: try: logged_in = bool(wcf.is_login()) @@ -47,10 +120,11 @@ def main(): if not logged_in: try: qrcode = wcf.get_qrcode() - except Exception: + except Exception as exc: + log("wcf-host-qrcode-error", error=f"{type(exc).__name__}: {exc}"[:1000]) qrcode = "" state = { - "ts": now_iso(), + "phase": "ready", "isLogin": logged_in, "qrcodePresent": bool(qrcode), "commandPort": PORT, @@ -58,8 +132,7 @@ def main(): } if qrcode: state["qrcode"] = qrcode - with open(os.path.join(STATE_ROOT, "status.json"), "w", encoding="utf-8") as fp: - json.dump(state, fp, ensure_ascii=False, indent=2, sort_keys=True) + write_state(**state) log("wcf-host-status", isLogin=logged_in, qrcodePresent=bool(qrcode)) time.sleep(5) except Exception as exc: