Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,106 @@ jobs:
with:
python-version: "3.12"
- run: uv sync --no-install-project --group all
# * 실험 patch: macOS family lookup 에 시스템 폰트 사전 필터링 적용 — match_family_style 호출 전에 멤버십 체크해 missing family 를 CoreText 에 던지지 않음. external/rhwp 자체에는 commit 하지 않고 runtime 에만 apply.
- name: Apply experimental CoreText fix (macOS only)
if: runner.os == 'macOS'
run: |
cd external/rhwp
git apply ../../ci/patches/macos-coretext-fix.patch
echo "=== patch applied ==="
git diff --stat
- run: uv run maturin develop --release
- run: uv run pytest tests/ -m "not slow" -v
# * macOS PNG 렌더 hang — 3차 진단으로 CoreText downloadable-font IPC hang 확인 (TDownloadableFontManager::Download → FontRegistryUI). chain 첫 폰트 부재 시 macOS 가 다운로드 시도 → daemon 응답 없음 → 무한 IPC wait.
# mitigation: 상류 fallback chain 의 모든 한글 폰트 사전 설치로 다운로드 lookup 자체를 막음.
- name: Install Korean font chain (macOS mitigation)
if: runner.os == 'macOS'
run: |
brew install --cask \
font-noto-sans-kr \
font-noto-serif-kr \
font-noto-sans-cjk \
font-nanum-gothic
echo "=== fc-list 한글 폰트 ==="
fc-list :lang=ko 2>&1 | head -20 || echo "(fc-list 미인식)"
- name: Run pytest (non-macOS)
if: runner.os != 'macOS'
run: uv run pytest tests/ -m "not slow" -v
- name: Run pytest (macOS) — sample native stack on hang
if: runner.os == 'macOS'
env:
RUST_BACKTRACE: full
run: |
set +e
mkdir -p /tmp/diag
uv run pytest tests/test_render_png.py::TestRenderPng::test_returns_png_magic -v -s &
PYTEST_PID=$!
# ^ 정상이면 분 단위 안에 끝남. 90초 wait 후 살아있으면 hang 으로 판정.
for i in $(seq 1 9); do
sleep 10
if ! kill -0 "$PYTEST_PID" 2>/dev/null; then
wait "$PYTEST_PID"; EXIT=$?
echo "pytest 종료 in ~$((i*10))s exit=$EXIT"
if [ "$EXIT" = "0" ]; then
exec uv run pytest tests/ -m "not slow" -v
fi
exit $EXIT
fi
done
echo "=== HANG DETECTED — sampling native stacks ==="
# ^ 자식까지 포함 (uv → python pytest). sample 출력을 파일로 받아 artifact 업로드.
PIDS="$PYTEST_PID $(pgrep -P "$PYTEST_PID" || true)"
for PID in $PIDS; do
kill -0 "$PID" 2>/dev/null || continue
OUT="/tmp/diag/sample_${PID}.txt"
echo "----- sampling PID=$PID → $OUT -----"
sample "$PID" 15 -file "$OUT" 2>&1 | head -3
echo ">>> Call graph head (PID=$PID) <<<"
awk '/^Call graph:/,/^Binary Images:/' "$OUT" 2>/dev/null | head -120 || echo "(파일 미생성)"
echo
done
ls -la /tmp/diag/
kill -9 "$PYTEST_PID" 2>/dev/null
exit 1
- name: Upload sample diagnostic artifacts
if: always() && runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: macos-png-hang-samples
path: /tmp/diag/
if-no-files-found: warn
# * 상류 CLI 자체도 hang 하는지 확인 — rhwp export-svg 가 같은 FontMgr 진입점을 거침. external/rhwp 디렉토리에서 빌드하되 산출물은 workspace target 으로 떨어질 수 있어 두 path 다 시도.
- name: Upstream rhwp CLI 재현 (macOS, export-svg)
if: always() && runner.os == 'macOS'
working-directory: external/rhwp
run: |
set +e
cargo build --release --bin rhwp 2>&1 | tail -3
BIN=../../target/release/rhwp
[ -x "$BIN" ] || BIN=./target/release/rhwp
[ -x "$BIN" ] || { echo "(빌드 산출물 없음)"; find ../.. -name "rhwp" -type f -maxdepth 5 2>/dev/null | head -5; exit 0; }
mkdir -p /tmp/svg_diag
echo "=== export-svg page 0, 90s soft timeout · using $BIN ==="
# macOS 는 GNU `timeout` 미포함 — background + sleep loop 로 구현
"$BIN" export-svg samples/aift.hwp -o /tmp/svg_diag -p 0 &
CLI_PID=$!
START=$(date +%s)
while kill -0 "$CLI_PID" 2>/dev/null; do
sleep 5
ELAPSED=$(( $(date +%s) - START ))
if [ "$ELAPSED" -ge 90 ]; then
echo "⚠️ TIMEOUT ${ELAPSED}s — 상류 CLI 도 동일하게 hang"
kill -9 "$CLI_PID" 2>/dev/null
wait "$CLI_PID" 2>/dev/null
exit 0
fi
done
wait "$CLI_PID"; RC=$?
ELAPSED=$(( $(date +%s) - START ))
if [ "$RC" = "0" ]; then
echo "✓ 상류 CLI export-svg ${ELAPSED}s 에 정상 종료 — PNG path 특이성"
else
echo "exit=$RC after ${ELAPSED}s (timeout 도 정상도 아님)"
fi

# * PDF 렌더링 — 느려서 별도 잡, Linux wheel 재사용
test-slow:
Expand Down
47 changes: 47 additions & 0 deletions ci/patches/macos-coretext-fix.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
diff --git a/src/renderer/skia/renderer.rs b/src/renderer/skia/renderer.rs
index eea1f807..5193fedf 100644
--- a/src/renderer/skia/renderer.rs
+++ b/src/renderer/skia/renderer.rs
@@ -2,7 +2,7 @@ use skia_safe::{
font, paint, surfaces, Canvas, Color, EncodedImageFormat, Font, FontMgr, FontStyle, Paint,
PathBuilder, PathEffect, Rect, Typeface,
};
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};

use crate::error::HwpError;
use crate::model::image::ImageEffect;
@@ -22,13 +22,20 @@ pub struct SkiaLayerRenderer {
/// key = primary face name (Typeface::family_name), value = Typeface.
/// SVG 의 `--font-path` 와 같은 패턴으로 ttfs 디렉토리의 한컴 전용 폰트 (HY견명조 등) 도 사용 가능.
custom_typefaces: HashMap<String, Typeface>,
+ /// [diagnostic patch] 시스템 폰트 family list 사전 캐시 — macOS 의
+ /// CoreText downloadable-font lookup IPC hang 회피용. match_family_style
+ /// 호출 전에 멤버십 체크해 missing family 를 CoreText 에 던지지 않음.
+ system_families: HashSet<String>,
}

impl SkiaLayerRenderer {
pub fn new() -> Self {
+ let font_mgr = FontMgr::default();
+ let system_families: HashSet<String> = font_mgr.family_names().collect();
Self {
- font_mgr: FontMgr::default(),
+ font_mgr,
custom_typefaces: HashMap::new(),
+ system_families,
}
}

@@ -326,6 +333,11 @@ impl SkiaLayerRenderer {
}
}
for family in &families {
+ // [diagnostic patch] macOS 의 CoreText downloadable lookup IPC
+ // hang 회피 — 시스템에 없는 family 는 match_family_style 에 던지지 않음.
+ if !self.system_families.contains(AsRef::<str>::as_ref(family)) {
+ continue;
+ }
if let Some(tf) = self.font_mgr.match_family_style(family, font_style) {
push(&mut chain, &mut seen, tf);
}
Loading