From 85254af4ae9816c630b66e255f44539e2d807980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B2=94=EC=88=98?= Date: Tue, 7 Jul 2026 00:13:12 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=B4=EC=8A=88=20#9=20-=20JCEF=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=9E=98=ED=95=91=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=EB=A5=BC=20=EC=98=A4=ED=94=84=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=B0=20=EB=A0=8C=EB=8D=94=EB=A7=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드포스/SWEA 문제 가져오기 폴백이 사용하던 '화면 밖 좌표(-3000,-3000) 숨김 프레임' 방식은 Linux(Wayland 등)에서 WM이 창 위치 지정을 무시해 제목줄 없는 플로팅 창으로 노출됐고, 사용자가 이를 닫으면 폴러가 응답을 받지 못해 'JCEF internal timeout' 오류가 발생했음. 오프스크린 렌더링(OSR)으로 브라우저를 생성하면 OS 창 자체가 없어 어떤 플랫폼에서도 노출될 수 없음. OSR 초기화가 실패하는 환경에서는 기존 숨김 프레임 방식으로 폴백. --- .../service/CodeforcesJcefFetcher.kt | 41 +++++++++++++++---- .../codingtestkit/service/SweaJcefFetcher.kt | 35 +++++++++++++--- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt b/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt index 9aa72e0..be24a19 100644 --- a/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt +++ b/src/main/kotlin/com/codingtestkit/service/CodeforcesJcefFetcher.kt @@ -81,24 +81,49 @@ object CodeforcesJcefFetcher { private val future: CompletableFuture ) { private val done = AtomicBoolean(false) - private var frame: JFrame? = null + private var frame: JFrame? = null // OSR 미지원 환경의 숨김 프레임 폴백용 private var browser: JBCefBrowser? = null private var timeoutTimer: Timer? = null - fun start() { - val (contestId, letter) = CodeforcesCrawler.parseProblemId(problemId) - val url = "https://codeforces.com/problemset/problem/$contestId/$letter?locale=en" - + /** + * 사용자에게 보이지 않는 스크래핑용 브라우저 생성. + * + * 1순위는 오프스크린 렌더링(OSR): OS 창 자체가 없어 어떤 플랫폼에서도 + * 노출될 수 없다. 화면 밖 좌표(-3000,-3000)로 숨기는 기존 방식은 + * Linux(Wayland 등)에서 WM이 창 위치 지정을 무시해 제목줄 없는 + * 플로팅 창으로 그대로 노출됐다 (이슈 #9). + */ + private fun createHiddenBrowser(url: String): JBCefBrowser { + try { + val osr = JBCefBrowser.createBuilder() + .setUrl(url) + .setOffScreenRendering(true) + .setCreateImmediately(true) + .build() + // OSR는 컴포넌트 크기를 뷰포트로 사용하므로 명시적으로 지정 + osr.component.setBounds(0, 0, 1024, 768) + return osr + } catch (e: Exception) { + LOG.info("[CodingTestKit] JCEF OSR unavailable, falling back to hidden frame: ${e.message}") + } + // 폴백: 화면 밖 숨김 프레임 (일부 Linux WM에서는 노출될 수 있음) + val windowed = JBCefBrowser(url) frame = JFrame().apply { type = Window.Type.UTILITY isUndecorated = true size = Dimension(1024, 768) setLocation(-3000, -3000) + contentPane.add(windowed.component) + isVisible = true } + return windowed + } + + fun start() { + val (contestId, letter) = CodeforcesCrawler.parseProblemId(problemId) + val url = "https://codeforces.com/problemset/problem/$contestId/$letter?locale=en" - browser = JBCefBrowser(url) - frame!!.contentPane.add(browser!!.component) - frame!!.isVisible = true + browser = createHiddenBrowser(url) LOG.info("[CodingTestKit] Codeforces JCEF fetch started: $problemId") diff --git a/src/main/kotlin/com/codingtestkit/service/SweaJcefFetcher.kt b/src/main/kotlin/com/codingtestkit/service/SweaJcefFetcher.kt index c17a00b..2f79d2b 100644 --- a/src/main/kotlin/com/codingtestkit/service/SweaJcefFetcher.kt +++ b/src/main/kotlin/com/codingtestkit/service/SweaJcefFetcher.kt @@ -89,21 +89,46 @@ object SweaJcefFetcher { private val future: CompletableFuture ) { private val done = AtomicBoolean(false) - private var frame: JFrame? = null + private var frame: JFrame? = null // OSR 미지원 환경의 숨김 프레임 폴백용 private var browser: JBCefBrowser? = null private var timeoutTimer: Timer? = null - fun start() { + /** + * 사용자에게 보이지 않는 스크래핑용 브라우저 생성. + * + * 1순위는 오프스크린 렌더링(OSR): OS 창 자체가 없어 어떤 플랫폼에서도 + * 노출될 수 없다. 화면 밖 좌표(-3000,-3000)로 숨기는 기존 방식은 + * Linux(Wayland 등)에서 WM이 창 위치 지정을 무시해 제목줄 없는 + * 플로팅 창으로 그대로 노출됐다 (이슈 #9). + */ + private fun createHiddenBrowser(url: String): JBCefBrowser { + try { + val osr = JBCefBrowser.createBuilder() + .setUrl(url) + .setOffScreenRendering(true) + .setCreateImmediately(true) + .build() + // OSR는 컴포넌트 크기를 뷰포트로 사용하므로 명시적으로 지정 + osr.component.setBounds(0, 0, 1024, 768) + return osr + } catch (e: Exception) { + LOG.info("[CodingTestKit] JCEF OSR unavailable, falling back to hidden frame: ${e.message}") + } + // 폴백: 화면 밖 숨김 프레임 (일부 Linux WM에서는 노출될 수 있음) + val windowed = JBCefBrowser(url) frame = JFrame().apply { type = Window.Type.UTILITY isUndecorated = true size = Dimension(1024, 768) setLocation(-3000, -3000) + contentPane.add(windowed.component) + isVisible = true } + return windowed + } - browser = JBCefBrowser(LIST_URL) - frame!!.contentPane.add(browser!!.component) - frame!!.isVisible = true + fun start() { + browser = createHiddenBrowser(LIST_URL) LOG.info("[CodingTestKit] JCEF fetch session started: $contestProbId")