316 lines
12 KiB
PowerShell
316 lines
12 KiB
PowerShell
# 只读:拉服务器新系统 8910 的业务/错误日志,并落到本机 logs/(UTF-8 BOM,记事本可直接打开)。
|
|
# 用法:双击 Watch-Agent8910.bat / Watch-Agent8910-Once.bat
|
|
# 本机:logs/yyyyMMdd_info.log 、 logs/yyyyMMdd_error.log
|
|
# 服务器旧日志可能是 GBK、新日志是 UTF-8;远程按「逐行 UTF-8,失败则 GBK」解码,避免乱码。
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$Once,
|
|
[switch]$All,
|
|
[switch]$Access,
|
|
[switch]$Gbk,
|
|
[switch]$NoLocalLog,
|
|
[int]$Tail = 120
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$SshKey = if ($env:YTD_SSH_KEY) { $env:YTD_SSH_KEY } else { Join-Path $env:USERPROFILE '.ssh\id_ed25519_ytd' }
|
|
$SshHost = if ($env:YTD_SSH_HOST) { $env:YTD_SSH_HOST } else { 'ytd_extdev_0903@129.211.181.80' }
|
|
$LogOut = 'E:\wwwroot\logs\agent-http.out.log'
|
|
$LogErr = 'E:\wwwroot\logs\agent-http.err.log'
|
|
# once / follow 用不同远程脚本名,避免互相覆盖导致「持续监听」其实跑成 Once
|
|
$WatchTag = if ($Once) { 'once' } else { 'follow' }
|
|
$IncomingRemotePath = "C:/Users/ytd_extdev_0903/incoming/_watch_agent8910_$WatchTag.ps1"
|
|
$IncomingLocalRun = "C:\Users\ytd_extdev_0903\incoming\_watch_agent8910_$WatchTag.ps1"
|
|
|
|
$Pattern = '回调|inject-text|inject:|outbox|inbox|errcode|errmsg|60020|ERROR|WARNING|Traceback|Exception|ModuleNotFoundError|sender=|touser=|crypto|ChannelRuntime|dispatch intent|企微发送|验签|解密|reply='
|
|
|
|
if (-not (Test-Path $SshKey)) {
|
|
throw "缺少 SSH 密钥: $SshKey"
|
|
}
|
|
|
|
try {
|
|
chcp 65001 | Out-Null
|
|
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
} catch {
|
|
Write-Host 'warn: console UTF-8 setup failed'
|
|
}
|
|
|
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
|
# 在仓库 deploy/ 下:写到仓库根 logs/;单独解压给同事:写到本工具目录 logs/
|
|
if ((Split-Path -Leaf $PSScriptRoot) -eq 'deploy') {
|
|
$LocalLogDir = Join-Path $RepoRoot 'logs'
|
|
} else {
|
|
$LocalLogDir = Join-Path $PSScriptRoot 'logs'
|
|
}
|
|
$Day = Get-Date -Format 'yyyyMMdd'
|
|
$LocalInfoLog = Join-Path $LocalLogDir ($Day + '_info.log')
|
|
$LocalErrorLog = Join-Path $LocalLogDir ($Day + '_error.log')
|
|
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
$Utf8Bom = New-Object System.Text.UTF8Encoding $true
|
|
|
|
function Ensure-Utf8BomFile([string]$path) {
|
|
if (-not (Test-Path $path) -or ((Get-Item $path).Length -eq 0)) {
|
|
[System.IO.File]::WriteAllText($path, '', $Utf8Bom)
|
|
return
|
|
}
|
|
$fs = [System.IO.File]::OpenRead($path)
|
|
try {
|
|
$b0 = $fs.ReadByte(); $b1 = $fs.ReadByte(); $b2 = $fs.ReadByte()
|
|
} finally { $fs.Close() }
|
|
if ($b0 -eq 0xEF -and $b1 -eq 0xBB -and $b2 -eq 0xBF) { return }
|
|
$raw = [System.IO.File]::ReadAllBytes($path)
|
|
$bom = [byte[]](0xEF, 0xBB, 0xBF)
|
|
$merged = New-Object byte[] ($bom.Length + $raw.Length)
|
|
[Array]::Copy($bom, 0, $merged, 0, 3)
|
|
[Array]::Copy($raw, 0, $merged, 3, $raw.Length)
|
|
[System.IO.File]::WriteAllBytes($path, $merged)
|
|
}
|
|
|
|
if (-not $NoLocalLog) {
|
|
New-Item -ItemType Directory -Force -Path $LocalLogDir | Out-Null
|
|
Ensure-Utf8BomFile $LocalInfoLog
|
|
Ensure-Utf8BomFile $LocalErrorLog
|
|
$sessionHdr = '===== watch start ' + (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + ' host=' + $SshHost + ' once=' + $Once.IsPresent + ' ====='
|
|
[System.IO.File]::AppendAllText($LocalInfoLog, $sessionHdr + [Environment]::NewLine, $Utf8NoBom)
|
|
[System.IO.File]::AppendAllText($LocalErrorLog, $sessionHdr + [Environment]::NewLine, $Utf8NoBom)
|
|
}
|
|
|
|
function Write-LocalLine([string]$line) {
|
|
Write-Host $line
|
|
if ($NoLocalLog) { return }
|
|
if ($line -match '^(目标:|主日志|访问日志|日志编码|过滤:|本机落盘:|说明:|---+|=====)') {
|
|
$isErr = $false
|
|
} else {
|
|
$isErr = ($line -cmatch '\] WARNING |\] ERROR |Traceback|Exception|ModuleNotFoundError') -or
|
|
($line -match 'errcode=\d+|errmsg=.+|health_fail|MISSING |解密失败|验签/解密失败|relation "agent_inbox"|not allow to access from your ip')
|
|
}
|
|
$target = if ($isErr) { $LocalErrorLog } else { $LocalInfoLog }
|
|
try {
|
|
[System.IO.File]::AppendAllText($target, $line + [Environment]::NewLine, $Utf8NoBom)
|
|
} catch {
|
|
Write-Host ('warn: write local log failed: ' + $_.Exception.Message)
|
|
}
|
|
}
|
|
|
|
Write-LocalLine ("目标: $SshHost")
|
|
Write-LocalLine ("主日志(业务/错误): $LogErr")
|
|
if ($Access -or $All) { Write-LocalLine ("访问日志: $LogOut") }
|
|
Write-LocalLine ("日志编码: smart UTF-8/GBK" + $(if ($Gbk) { ' (force GBK)' } else { '' }))
|
|
if ($All) { Write-LocalLine '过滤: 关闭' } else { Write-LocalLine ("过滤: $Pattern") }
|
|
if (-not $NoLocalLog) {
|
|
Write-LocalLine ("本机落盘: $LocalInfoLog")
|
|
Write-LocalLine ("本机落盘: $LocalErrorLog")
|
|
}
|
|
Write-LocalLine '说明: 本地日志为 UTF-8 BOM;请重新跑 Once 覆盖旧乱码内容(可先删当天 log 再跑)'
|
|
Write-LocalLine '---'
|
|
|
|
# follow=1 / once=0,避免 $true/$false 在远程被覆盖或解析歧义
|
|
$waitFlag = if ($Once) { '0' } else { '1' }
|
|
$allLiteral = if ($All) { '$true' } else { '$false' }
|
|
$accessLiteral = if ($Access) { '$true' } else { '$false' }
|
|
$forceGbkLiteral = if ($Gbk) { '$true' } else { '$false' }
|
|
|
|
# 远程:逐行按 UTF-8 严格解码,失败再用 GBK(936);输出强制 Flush,避免持续监听被缓冲卡住
|
|
$remoteScript = @"
|
|
`$ErrorActionPreference = 'Continue'
|
|
try {
|
|
chcp 65001 | Out-Null
|
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(`$false)
|
|
`$OutputEncoding = [System.Text.UTF8Encoding]::new(`$false)
|
|
} catch {}
|
|
`$out = '$LogOut'
|
|
`$err = '$LogErr'
|
|
`$tail = $Tail
|
|
`$waitFlag = '$waitFlag'
|
|
`$wait = (`$waitFlag -eq '1')
|
|
`$all = $allLiteral
|
|
`$wantAccess = $accessLiteral
|
|
`$pat = '$Pattern'
|
|
`$forceGbk = $forceGbkLiteral
|
|
`$utf8Strict = New-Object System.Text.UTF8Encoding `$false, `$true
|
|
`$gbk = [System.Text.Encoding]::GetEncoding(936)
|
|
|
|
function Emit([string]`$s) {
|
|
[Console]::Out.WriteLine(`$s)
|
|
[Console]::Out.Flush()
|
|
}
|
|
|
|
function Decode-LogBytes([byte[]]`$chunk) {
|
|
if (`$null -eq `$chunk -or `$chunk.Length -eq 0) { return '' }
|
|
if (`$forceGbk) { return `$gbk.GetString(`$chunk) }
|
|
try { return `$utf8Strict.GetString(`$chunk) } catch { return `$gbk.GetString(`$chunk) }
|
|
}
|
|
|
|
function Get-SmartTailLines([string]`$path, [int]`$n) {
|
|
if (-not (Test-Path `$path)) { return @() }
|
|
`$fs = [System.IO.File]::Open(`$path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
try {
|
|
`$ms = New-Object System.IO.MemoryStream
|
|
`$fs.CopyTo(`$ms)
|
|
`$bytes = `$ms.ToArray()
|
|
} finally {
|
|
`$fs.Close()
|
|
}
|
|
`$lines = New-Object System.Collections.Generic.List[string]
|
|
`$start = 0
|
|
for (`$i = 0; `$i -lt `$bytes.Length; `$i++) {
|
|
if (`$bytes[`$i] -ne 10) { continue }
|
|
`$end = `$i
|
|
if (`$end -gt `$start -and `$bytes[`$end - 1] -eq 13) { `$end = `$end - 1 }
|
|
`$len = `$end - `$start
|
|
if (`$len -lt 0) { `$len = 0 }
|
|
`$chunk = New-Object byte[] `$len
|
|
if (`$len -gt 0) { [Array]::Copy(`$bytes, `$start, `$chunk, 0, `$len) }
|
|
`$lines.Add((Decode-LogBytes `$chunk))
|
|
`$start = `$i + 1
|
|
}
|
|
if (`$start -lt `$bytes.Length) {
|
|
`$len = `$bytes.Length - `$start
|
|
`$chunk = New-Object byte[] `$len
|
|
[Array]::Copy(`$bytes, `$start, `$chunk, 0, `$len)
|
|
`$lines.Add((Decode-LogBytes `$chunk))
|
|
}
|
|
if (`$lines.Count -le `$n) { return `$lines.ToArray() }
|
|
return `$lines.GetRange(`$lines.Count - `$n, `$n).ToArray()
|
|
}
|
|
|
|
Emit ('mode=' + `$waitFlag + ' follow=' + `$wait)
|
|
Emit '=== remote health 8910 ==='
|
|
try {
|
|
`$h = Invoke-WebRequest -Uri 'http://127.0.0.1:8910/health' -UseBasicParsing -TimeoutSec 5
|
|
Emit `$h.Content
|
|
} catch {
|
|
Emit ('health_fail: ' + `$_.Exception.Message)
|
|
}
|
|
Emit '=== business/error log (err) ==='
|
|
|
|
function Show-Err([string]`$line) {
|
|
if (`$all) { Emit ('[err] ' + `$line); return }
|
|
if (`$line -match `$pat) { Emit ('[err] ' + `$line) }
|
|
}
|
|
|
|
function Show-Out([string]`$line) {
|
|
if (-not `$wantAccess -and -not `$all) { return }
|
|
if (`$all) { Emit ('[out] ' + `$line); return }
|
|
if (`$line -match '/wecom/callback|inject-text') { Emit ('[out] ' + `$line) }
|
|
}
|
|
|
|
if (-not (Test-Path `$err)) { Emit ('MISSING ' + `$err) }
|
|
|
|
if (-not `$wait) {
|
|
if (Test-Path `$err) {
|
|
Get-SmartTailLines `$err `$tail | ForEach-Object { Show-Err `$_ }
|
|
}
|
|
if (`$wantAccess -or `$all) {
|
|
Emit '=== access log (out) ==='
|
|
if (Test-Path `$out) {
|
|
Get-SmartTailLines `$out `$tail | ForEach-Object { Show-Out `$_ }
|
|
}
|
|
}
|
|
exit 0
|
|
}
|
|
|
|
Emit 'following err.log ... (Ctrl+C to stop)'
|
|
`$lastLen = @{}
|
|
`$carry = @{}
|
|
foreach (`$f in @(`$err, `$out)) {
|
|
if (Test-Path `$f) { `$lastLen[`$f] = (Get-Item `$f).Length } else { `$lastLen[`$f] = 0 }
|
|
`$carry[`$f] = ''
|
|
}
|
|
if (Test-Path `$err) { Get-SmartTailLines `$err `$tail | ForEach-Object { Show-Err `$_ } }
|
|
# 尾部打完后,从当前文件末尾继续跟,避免重复刷旧行
|
|
foreach (`$f in @(`$err, `$out)) {
|
|
if (Test-Path `$f) { `$lastLen[`$f] = (Get-Item `$f).Length }
|
|
}
|
|
`$heartbeatAt = [DateTime]::UtcNow
|
|
while (`$true) {
|
|
Start-Sleep -Milliseconds 500
|
|
if (([DateTime]::UtcNow - `$heartbeatAt).TotalSeconds -ge 30) {
|
|
Emit ('... still watching ' + (Get-Date).ToString('HH:mm:ss'))
|
|
`$heartbeatAt = [DateTime]::UtcNow
|
|
}
|
|
`$targets = @()
|
|
if (Test-Path `$err) { `$targets += `$err }
|
|
if ((`$wantAccess -or `$all) -and (Test-Path `$out)) { `$targets += `$out }
|
|
foreach (`$f in `$targets) {
|
|
if (-not `$lastLen.ContainsKey(`$f)) { `$lastLen[`$f] = 0; `$carry[`$f] = '' }
|
|
`$len = (Get-Item `$f).Length
|
|
if (`$len -lt `$lastLen[`$f]) { `$lastLen[`$f] = 0; `$carry[`$f] = '' }
|
|
if (`$len -le `$lastLen[`$f]) { continue }
|
|
`$fs = [System.IO.File]::Open(`$f, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
try {
|
|
[void]`$fs.Seek(`$lastLen[`$f], [System.IO.SeekOrigin]::Begin)
|
|
`$buf = New-Object byte[] (`$len - `$lastLen[`$f])
|
|
`$read = `$fs.Read(`$buf, 0, `$buf.Length)
|
|
} finally { `$fs.Close() }
|
|
`$lastLen[`$f] = `$len
|
|
if (`$read -le 0) { continue }
|
|
`$chunk = New-Object byte[] `$read
|
|
[Array]::Copy(`$buf, 0, `$chunk, 0, `$read)
|
|
`$text = `$carry[`$f] + (Decode-LogBytes `$chunk)
|
|
`$parts = `$text -split "`n"
|
|
if (-not (`$text.EndsWith("`n") -or `$text.EndsWith("`r"))) {
|
|
if (`$parts.Count -ge 1) {
|
|
`$carry[`$f] = `$parts[`$parts.Count - 1].TrimEnd("`r")
|
|
`$limit = `$parts.Count - 1
|
|
} else {
|
|
`$carry[`$f] = `$text
|
|
`$limit = 0
|
|
}
|
|
} else {
|
|
`$carry[`$f] = ''
|
|
`$limit = `$parts.Count
|
|
}
|
|
for (`$pi = 0; `$pi -lt `$limit; `$pi++) {
|
|
`$line = `$parts[`$pi].TrimEnd("`r")
|
|
if (-not `$line) { continue }
|
|
if (`$f -eq `$out) { Show-Out `$line } else { Show-Err `$line }
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
|
|
$localTemp = Join-Path $env:TEMP 'ytd-watch-agent8910-remote.ps1'
|
|
[System.IO.File]::WriteAllText($localTemp, $remoteScript, $Utf8NoBom)
|
|
|
|
& scp.exe -o BatchMode=yes -o ConnectTimeout=12 -i $SshKey $localTemp "${SshHost}:${IncomingRemotePath}"
|
|
if ($LASTEXITCODE -ne 0) { throw "scp failed: $LASTEXITCODE" }
|
|
|
|
$sshArgs = @(
|
|
'-o', 'BatchMode=yes',
|
|
'-o', 'ConnectTimeout=12',
|
|
'-i', $SshKey,
|
|
$SshHost,
|
|
"powershell -NoProfile -ExecutionPolicy Bypass -File `"$IncomingLocalRun`""
|
|
)
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = 'ssh.exe'
|
|
$psi.Arguments = ($sshArgs | ForEach-Object {
|
|
if ($_ -match '\s') { '"' + $_ + '"' } else { $_ }
|
|
}) -join ' '
|
|
$psi.UseShellExecute = $false
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.CreateNoWindow = $true
|
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
$proc = New-Object System.Diagnostics.Process
|
|
$proc.StartInfo = $psi
|
|
[void]$proc.Start()
|
|
|
|
while (-not $proc.StandardOutput.EndOfStream) {
|
|
$line = $proc.StandardOutput.ReadLine()
|
|
if ($null -ne $line) { Write-LocalLine $line }
|
|
}
|
|
$errTail = $proc.StandardError.ReadToEnd()
|
|
if ($errTail) {
|
|
foreach ($eline in ($errTail -split "`r?`n")) {
|
|
if ($eline) { Write-LocalLine $eline }
|
|
}
|
|
}
|
|
$proc.WaitForExit()
|
|
exit $proc.ExitCode
|