실제 설치에서 드러난 것들이다. 셋 다 증상이 원인과 전혀 안 닮아서 기록해 둔다. 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 에 붙였다.
76 lines
2.7 KiB
Python
Executable File
76 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""win-ci VM 과의 통신 채널.
|
|
|
|
게스트(Windows)는 QEMU user networking 안에 있어서 호스트를 10.0.2.2 로 본다.
|
|
이 서버는 그 반대편이다.
|
|
|
|
GET /ping 게스트가 네트워크 올라왔는지 확인용
|
|
GET /stage20.ps1 게스트가 실행할 설치 스크립트 (serve/ 안의 파일 그대로)
|
|
GET /<file> serve/ 안의 아무 파일 (러너 등록 토큰 등)
|
|
POST /log 게스트 로그 -> guest.log 에 append
|
|
|
|
127.0.0.1 만 바인딩한다. slirp 의 10.0.2.2 는 호스트 루프백으로 들어오므로
|
|
외부에 열 필요가 없다.
|
|
"""
|
|
import http.server
|
|
import os
|
|
import socketserver
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
ROOT = "/home/claude/win-ci/serve"
|
|
LOG = "/home/claude/win-ci/guest.log"
|
|
PORT = 8099
|
|
|
|
|
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
def __init__(self, *a, **kw):
|
|
super().__init__(*a, directory=ROOT, **kw)
|
|
|
|
def guess_type(self, path):
|
|
# .ps1 은 charset 을 반드시 명시한다. 없으면 PowerShell 쪽에서
|
|
# ISO-8859-1 로 디코딩해 한글 주석이 깨지고, 깨진 바이트가 CP1252 의
|
|
# 스마트따옴표가 되어 파서가 스크립트를 통째로 삼킨다.
|
|
if str(path).endswith(".ps1"):
|
|
return "text/plain; charset=utf-8"
|
|
return super().guess_type(path)
|
|
|
|
def do_GET(self):
|
|
if self.path.rstrip("/") == "/ping":
|
|
body = b"pong\n"
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/plain")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
return
|
|
super().do_GET()
|
|
|
|
def do_POST(self):
|
|
if self.path.rstrip("/") != "/log":
|
|
self.send_error(404)
|
|
return
|
|
n = int(self.headers.get("Content-Length") or 0)
|
|
data = self.rfile.read(n).decode("utf-8", "replace").strip()
|
|
with open(LOG, "a", encoding="utf-8") as f:
|
|
f.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} {data}\n")
|
|
self.send_response(204)
|
|
self.end_headers()
|
|
|
|
def log_message(self, fmt, *args):
|
|
# 접근 로그는 stdout 으로만. journald 가 받는다.
|
|
sys.stdout.write("%s %s\n" % (self.address_string(), fmt % args))
|
|
sys.stdout.flush()
|
|
|
|
|
|
class Server(socketserver.ThreadingTCPServer):
|
|
allow_reuse_address = True
|
|
daemon_threads = True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs(ROOT, exist_ok=True)
|
|
with Server(("127.0.0.1", PORT), Handler) as httpd:
|
|
print(f"win-ci control server on 127.0.0.1:{PORT}, root={ROOT}", flush=True)
|
|
httpd.serve_forever()
|