실제 설치에서 드러난 것들이다. 셋 다 증상이 원인과 전혀 안 닮아서 기록해 둔다. 1) provision.ps1 이 1행부터 파싱 실패 - 'Missing closing }' Windows PowerShell 5.1 은 BOM 없는 .ps1 을 ANSI(CP1252)로 읽는다. 한글 주석의 UTF-8 바이트가 CP1252 에서 스마트따옴표(0x93/0x94 = " ")로 변하고, 파서가 그걸 문자열 시작으로 보고 뒤를 전부 삼킨다. -> build.sh 가 ISO 에 넣을 때 UTF-8 BOM 을 붙인다. 2) stage2.ps1 의 1행이 주석으로 인식되지 않음 agent 가 받은 본문에 BOM 이 있으면 Set-Content -Encoding UTF8 이 BOM 을 한 번 더 붙여 BOM 이 두 개가 된다. PowerShell 은 첫 개만 벗겨내고 남은 U+FEFF 때문에 '#' 이 줄 맨 앞이 아니게 된다. -> agent 가 TrimStart(U+FEFF), 서버는 charset=utf-8 을 명시한다. 3) Git 설치가 'Cannot convert System.Object[] to System.String' 로 실패 Log 가 Write-Output 이라 함수 안에서 부르면 그 함수의 반환값에 로그가 섞인다. Get-File 이 배열을 돌려주니 Start-Process 가 죽었다. -> Log 를 Write-Host 로 바꿨다. 덤으로 시리얼/HTTP 로그의 한글이 '?' 로 깨지던 것도 고쳤다 (SerialPort 기본 인코딩이 ASCII). 그리고 러너 토큰이 없어도 워크플로와 같은 단계를 VM 안에서 직접 돌리는 검증 절차를 stage2 에 붙였다.
111 lines
5.4 KiB
PowerShell
111 lines
5.4 KiB
PowerShell
# Windows CI VM 최초 부팅 프로비저닝 (무인)
|
|
#
|
|
# 이 스크립트는 autounattend.xml 의 FirstLogonCommands 로 한 번만 실행된다.
|
|
# 네트워크가 아직 없는 상태에서 시작하므로, 순서가 중요하다.
|
|
# 1) virtio 드라이버 설치 (ISO 에서 오프라인) -> 이걸 해야 네트워크가 생긴다
|
|
# 2) 화면 꺼짐/잠금 끄기 (GUI 테스트가 잠금화면에서 실패하지 않게)
|
|
# 3) agent.ps1 을 C:\win-ci 에 심고 로그온 예약 작업으로 등록
|
|
# 4) agent 를 바로 실행 -> 이후 모든 설치는 호스트가 주는 stage2.ps1 이 한다
|
|
#
|
|
# 로그는 COM1(호스트의 serial.log)과 C:\win-ci\provision.log 양쪽에 남긴다.
|
|
# 네트워크가 없는 1) 단계를 호스트에서 관찰할 수 있어야 하기 때문이다.
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
New-Item -ItemType Directory -Force -Path 'C:\win-ci' | Out-Null
|
|
|
|
$script:Port = $null
|
|
try {
|
|
$script:Port = New-Object System.IO.Ports.SerialPort 'COM1', 115200, 'None', 8, 'One'
|
|
# 기본 인코딩이 ASCII 라 한글이 전부 '?' 가 된다
|
|
$script:Port.Encoding = [Text.Encoding]::UTF8
|
|
$script:Port.Open()
|
|
} catch { $script:Port = $null }
|
|
|
|
function Say([string]$m) {
|
|
$line = "[provision $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $m"
|
|
try { Add-Content -Path 'C:\win-ci\provision.log' -Value $line -Encoding UTF8 } catch {}
|
|
if ($script:Port) { try { $script:Port.WriteLine($line) } catch {} }
|
|
}
|
|
|
|
Say "start. user=$env:USERNAME computer=$env:COMPUTERNAME"
|
|
|
|
# ---------------------------------------------------------------- 1) virtio
|
|
# virtio-win ISO 가 몇 번째 드라이브로 잡힐지 모른다. 전부 훑어 찾는다.
|
|
$virtio = $null
|
|
foreach ($d in [char[]](68..77)) { # D..M
|
|
$p = "${d}:\virtio-win-guest-tools.exe"
|
|
if (Test-Path $p) { $virtio = "${d}:"; break }
|
|
}
|
|
|
|
if (-not $virtio) {
|
|
Say "ERROR virtio-win ISO 를 못 찾음. 네트워크/화면 드라이버 없이 계속한다."
|
|
} else {
|
|
Say "virtio ISO = $virtio"
|
|
|
|
# 드라이버 스토어에 직접 넣는 쪽을 먼저 한다. 설치 프로그램보다 확실하다.
|
|
$infs = @(
|
|
"$virtio\NetKVM\w11\amd64\netkvm.inf", # 네트워크 - 가장 중요
|
|
"$virtio\viogpudo\w11\amd64\viogpudo.inf", # 화면 - 모니터 2개에 필요
|
|
"$virtio\Balloon\w11\amd64\balloon.inf",
|
|
"$virtio\vioserial\w11\amd64\vioser.inf",
|
|
"$virtio\viostor\w11\amd64\viostor.inf",
|
|
"$virtio\vioscsi\w11\amd64\vioscsi.inf",
|
|
"$virtio\vioinput\w11\amd64\vioinput.inf"
|
|
)
|
|
foreach ($inf in $infs) {
|
|
if (Test-Path $inf) {
|
|
$out = & pnputil.exe /add-driver $inf /install 2>&1 | Out-String
|
|
Say "pnputil $(Split-Path $inf -Leaf) -> $($out.Trim() -replace '\s+', ' ')"
|
|
} else {
|
|
Say "skip (없음) $inf"
|
|
}
|
|
}
|
|
|
|
# guest-tools 는 balloon 서비스/qemu-ga 까지 붙여준다. 실패해도 위에서 드라이버는 들어갔다.
|
|
try {
|
|
Say "virtio-win-guest-tools 설치 시작"
|
|
$p = Start-Process -FilePath "$virtio\virtio-win-guest-tools.exe" `
|
|
-ArgumentList '/install', '/quiet', '/norestart' -Wait -PassThru
|
|
Say "virtio-win-guest-tools exit=$($p.ExitCode)"
|
|
} catch {
|
|
Say "virtio-win-guest-tools 실패: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------- 2) 화면 꺼짐 / 잠금 끄기
|
|
# 러너가 GUI 테스트와 스크린샷을 찍으므로 데스크톱이 살아 있어야 한다.
|
|
& powercfg.exe /change monitor-timeout-ac 0
|
|
& powercfg.exe /change standby-timeout-ac 0
|
|
& powercfg.exe /change disk-timeout-ac 0
|
|
& powercfg.exe /change hibernate-timeout-ac 0
|
|
Say "powercfg 적용 (모니터/대기/디스크/최대절전 전부 끔)"
|
|
|
|
# 화면 보호기 + 잠금화면
|
|
reg add "HKCU\Control Panel\Desktop" /v ScreenSaveActive /t REG_SZ /d 0 /f | Out-Null
|
|
reg add "HKCU\Control Panel\Desktop" /v ScreenSaverIsSecure /t REG_SZ /d 0 /f | Out-Null
|
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Personalization" /v NoLockScreen /t REG_DWORD /d 1 /f | Out-Null
|
|
# 절전 시 암호 요구 끔
|
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Power\PowerSettings\0e796bdb-100d-47d6-a2d5-f7d2daa51f51" /v ACSettingIndex /t REG_DWORD /d 0 /f | Out-Null
|
|
Say "잠금화면/화면보호기 끔"
|
|
|
|
# 첫 부팅 때 뜨는 방해 요소들
|
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" /v NoDriveTypeAutoRun /t REG_DWORD /d 255 /f | Out-Null
|
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoRebootWithLoggedOnUsers /t REG_DWORD /d 1 /f | Out-Null
|
|
Say "자동실행/업데이트 자동재부팅 끔"
|
|
|
|
# ------------------------------------------- 3) agent 설치 + 로그온 예약 작업
|
|
$src = $PSScriptRoot
|
|
Copy-Item -Path (Join-Path $src 'agent.ps1') -Destination 'C:\win-ci\agent.ps1' -Force
|
|
Say "agent.ps1 복사 완료"
|
|
|
|
$taskCmd = 'powershell.exe'
|
|
$taskArg = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\win-ci\agent.ps1'
|
|
& schtasks.exe /Create /TN 'win-ci-agent' /SC ONLOGON /RL HIGHEST /TR "$taskCmd $taskArg" /F 2>&1 | Out-String | ForEach-Object { Say "schtasks -> $($_.Trim())" }
|
|
|
|
# ------------------------------------------------------------- 4) agent 기동
|
|
Say "agent 시작 - 이후 로그는 [agent] 접두어로 나온다"
|
|
Start-Process -FilePath $taskCmd -ArgumentList $taskArg -WindowStyle Hidden
|
|
|
|
Say "provision 끝"
|
|
if ($script:Port) { try { $script:Port.Close() } catch {} }
|