From f706e8393c8049d810a12f08b0b23c90573b8966 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Thu, 13 Aug 2026 19:56:42 +0000 Subject: [PATCH] =?UTF-8?q?v1.0.15.3=20=E2=80=94=20non-blocking=20logger?= =?UTF-8?q?=20(fix=20install=20freeze),=20URL-encode=20pack=20file=20paths?= =?UTF-8?q?,=20news=20ordering=20by=20mtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zernmc/launcher/utils/LauncherLogger.java | 75 +++++++++++++++---- launcher/pom.xml | 2 +- server/main.py | 12 +-- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java index 2c5bf08..70013a8 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java @@ -9,6 +9,9 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class LauncherLogger { @@ -17,6 +20,28 @@ public class LauncherLogger { private static boolean initialized = false; private static final ReentrantLock lock = new ReentrantLock(); + private static final int STDOUT_QUEUE_CAPACITY = 2048; + private static final BlockingQueue stdoutQueue = new LinkedBlockingQueue<>(STDOUT_QUEUE_CAPACITY); + private static final Thread stdoutWriterThread; + + static { + stdoutWriterThread = new Thread(() -> { + while (true) { + try { + String line = stdoutQueue.take(); + System.out.println(line); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } catch (Exception ignored) { + // stdout may be a closed or full pipe; never let it kill the writer + } + } + }, "launcher-log-stdout"); + stdoutWriterThread.setDaemon(true); + stdoutWriterThread.start(); + } + public static synchronized void init() { if (initialized) return; initialized = true; @@ -65,31 +90,53 @@ public class LauncherLogger { String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); String line = "[" + ts + "] [" + level + "] " + msg; - System.out.println(line); + // stdout goes through a dedicated writer thread with a bounded queue so a + // full/hung stdout pipe (e.g. javaw piped to the parent exe) can never + // block application threads. + offerStdout(line); if (t != null) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); t.printStackTrace(pw); pw.flush(); - System.err.print(sw.toString()); + offerStdout(sw.toString()); } if (logFile != null) { - lock.lock(); + // Never block indefinitely on the file write; give up after a short wait + // so a stuck writer cannot deadlock every logging thread. + boolean acquired = false; try { - Files.writeString(logFile, line + "\n", StandardOpenOption.APPEND); - if (t != null) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - pw.flush(); - Files.writeString(logFile, sw.toString(), StandardOpenOption.APPEND); + acquired = lock.tryLock(200, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + if (acquired) { + try { + Files.writeString(logFile, line + "\n", StandardOpenOption.APPEND); + if (t != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + Files.writeString(logFile, sw.toString(), StandardOpenOption.APPEND); + } + } catch (IOException e) { + System.err.println("[LauncherLogger] write error: " + e.getMessage()); + } finally { + lock.unlock(); } - } catch (IOException e) { - System.err.println("[LauncherLogger] write error: " + e.getMessage()); - } finally { - lock.unlock(); } } } + + private static void offerStdout(String line) { + try { + if (!stdoutQueue.offer(line, 10, TimeUnit.MILLISECONDS)) { + // Queue full; drop rather than block the caller. + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } } diff --git a/launcher/pom.xml b/launcher/pom.xml index fff79fa..0afa4f5 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -19,7 +19,7 @@ 1.0.15 - 2 + 3 21 21 UTF-8 diff --git a/server/main.py b/server/main.py index 283a8ae..00c20ea 100644 --- a/server/main.py +++ b/server/main.py @@ -4,7 +4,7 @@ import os from contextlib import asynccontextmanager from datetime import datetime from pathlib import Path -from urllib.parse import urlparse +from urllib.parse import urlparse, quote from typing import Optional import httpx @@ -1128,9 +1128,9 @@ async def get_pack_diff( for path, entry in server_files.items(): client_hash = body.get(path) if client_hash is None or client_hash != entry.hash: - url = f"/pack/{pack_name}/file/{path}" + url = f"/pack/{pack_name}/file/{quote(path, safe='/')}" if preset: - url += f"?preset={preset}" + url += f"?preset={quote(preset, safe='')}" to_download.append({ "path": path, "url": url, @@ -1969,8 +1969,9 @@ async def list_news(): return {"news": []} news_list = [] - for f in sorted(NEWS_DIR.iterdir()): - if f.is_file() and f.suffix == ".txt": + files = [f for f in NEWS_DIR.iterdir() if f.is_file() and f.suffix == ".txt"] + files.sort(key=lambda f: f.stat().st_mtime, reverse=True) + for f in files: try: content = f.read_text(encoding="utf-8").strip().split("\n") if len(content) >= 4: @@ -1988,7 +1989,6 @@ async def list_news(): except Exception as e: logger.warning(f"Failed to read news file {f.name}: {e}") - news_list.reverse() return {"news": news_list}