ci: win-ci 검증 출력을 호스트 파일로 회수한다 (증거 기준선)
Some checks failed
Windows / verify (push) Has been cancelled
Some checks failed
Windows / verify (push) Has been cancelled
VM 을 만든 이유가 "리눅스에서 통과한 게 Windows 에서 깨지는 것을 재현
가능하게 본다" 인데, 정작 실패 트레이스백이 호스트에 한 줄도 안 남았다.
로그 채널은 마지막 몇 줄만 흘렸고(Select-Object -Last 30), serial.log 는
VM 이 뜰 때마다 truncate 되고, guest.log 는 stage2 가 다시 돌 때만 채워진다.
그래서 실패 내용을 화면에서 읽어 옮겨 적는 상태였고 — 고친 뒤에 고쳐졌다는
것을 증명할 기준선이 없었다.
- serve-host.py: POST /artifact/<name> 추가. results/<name> 에 파일로 저장
(경로는 basename 으로만 취하고 32MB 상한)
- stage2.ps1: Step 이 각 단계의 출력 전문을 StreamWriter(AutoFlush)로 받아
호스트로 업로드한다. 중간에 예외로 죽어도 거기까지는 남는다.
출력 자르기를 없애고 pytest 는 --tb=long, 검증 리비전을 박은
summary.log 도 함께 올린다
- sync.sh: 스크립트만 /home/claude/win-ci 로 반영한다. build.sh 를 다시
돌리면 지워둔 Windows ISO 4.8GB 를 다시 내려받는다
첫 수집 결과(rev 162540b)로 실제 원인 두 개를 특정했다.
This commit is contained in:
@@ -4,23 +4,32 @@
|
||||
게스트(Windows)는 QEMU user networking 안에 있어서 호스트를 10.0.2.2 로 본다.
|
||||
이 서버는 그 반대편이다.
|
||||
|
||||
GET /ping 게스트가 네트워크 올라왔는지 확인용
|
||||
GET /stage20.ps1 게스트가 실행할 설치 스크립트 (serve/ 안의 파일 그대로)
|
||||
GET /<file> serve/ 안의 아무 파일 (러너 등록 토큰 등)
|
||||
POST /log 게스트 로그 -> guest.log 에 append
|
||||
GET /ping 게스트가 네트워크 올라왔는지 확인용
|
||||
GET /stage2.ps1 게스트가 실행할 설치 스크립트 (serve/ 안의 파일 그대로)
|
||||
GET /<file> serve/ 안의 아무 파일 (러너 등록 토큰 등)
|
||||
POST /log 게스트 로그 -> guest.log 에 append
|
||||
POST /artifact/<name> 게스트 명령 출력 원본 -> results/<name> 에 파일로 저장
|
||||
|
||||
/artifact 가 필요한 이유: 게스트 검증 결과를 사람이 화면에서 읽고 옮겨 적으면
|
||||
"고쳤다"를 증명할 기준선이 안 남는다. serial.log 는 VM 이 뜰 때마다 truncate 되고
|
||||
로그 채널은 마지막 몇 줄만 흘린다. pytest 트레이스백처럼 전문이 필요한 출력은
|
||||
반드시 이 엔드포인트로 호스트 파일에 떨어뜨린다.
|
||||
|
||||
127.0.0.1 만 바인딩한다. slirp 의 10.0.2.2 는 호스트 루프백으로 들어오므로
|
||||
외부에 열 필요가 없다.
|
||||
"""
|
||||
import http.server
|
||||
import os
|
||||
import posixpath
|
||||
import socketserver
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
ROOT = "/home/claude/win-ci/serve"
|
||||
LOG = "/home/claude/win-ci/guest.log"
|
||||
RESULTS = "/home/claude/win-ci/results"
|
||||
PORT = 8099
|
||||
MAX_ARTIFACT = 32 * 1024 * 1024
|
||||
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
@@ -47,9 +56,15 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path.rstrip("/") != "/log":
|
||||
path = self.path.rstrip("/")
|
||||
if path == "/log":
|
||||
self._recv_log()
|
||||
elif path.startswith("/artifact/"):
|
||||
self._recv_artifact(path[len("/artifact/") :])
|
||||
else:
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
def _recv_log(self):
|
||||
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:
|
||||
@@ -57,9 +72,27 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def _recv_artifact(self, name):
|
||||
# 게스트가 주는 이름을 그대로 경로에 쓰면 안 된다. 파일명만 취한다.
|
||||
name = posixpath.basename(name.replace("\\", "/"))
|
||||
if not name or name.startswith("."):
|
||||
self.send_error(400, "bad artifact name")
|
||||
return
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
if n > MAX_ARTIFACT:
|
||||
self.send_error(413, "artifact too large")
|
||||
return
|
||||
data = self.rfile.read(n)
|
||||
os.makedirs(RESULTS, exist_ok=True)
|
||||
with open(os.path.join(RESULTS, name), "wb") as f:
|
||||
f.write(data)
|
||||
print(f"artifact {name} {len(data)} bytes", flush=True)
|
||||
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.write(f"{self.address_string()} {fmt % args}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@@ -70,6 +103,7 @@ class Server(socketserver.ThreadingTCPServer):
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs(ROOT, exist_ok=True)
|
||||
os.makedirs(RESULTS, 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()
|
||||
|
||||
Reference in New Issue
Block a user