package com.mes.util; import java.io.File; import java.io.PrintStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** Lightweight local logging for the standalone MES desktop client. */ public final class AppLog { private static final String APP_NAME = "mesclient-op060B-P02"; private static final Object LOCK = new Object(); private static final SimpleDateFormat TIME = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private static PrintStream out; private static File stateFile; private static ScheduledExecutorService heartbeat; private static volatile boolean userClose; private static volatile String exitReason; private static volatile String uncaughtThread; private static volatile Throwable uncaughtException; private AppLog() { } public static void init() { synchronized (LOCK) { if (out != null) return; try { File code = new File(AppLog.class.getProtectionDomain().getCodeSource().getLocation().toURI()); File base = code.isFile() ? code.getParentFile() : new File(System.getProperty("user.dir")); File dir = new File(base, "logs"); if (!dir.exists() && !dir.mkdirs()) return; stateFile = new File(dir, APP_NAME + ".running"); String day = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); out = new PrintStream(new FileOutputStream(new File(dir, APP_NAME + "-" + day + ".log"), true), true, "UTF-8"); reportPreviousRun(); writeState(); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread thread, Throwable exception) { synchronized (LOCK) { exitReason = "UNCAUGHT_EXCEPTION"; uncaughtThread = thread.getName(); uncaughtException = exception; log("错误", "未捕获异常,线程=" + clean(uncaughtThread), exception); } } }); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { synchronized (LOCK) { if (userClose) { log("信息", "客户端退出,原因=用户主动正常关闭,状态文件将删除", null); deleteState(); } else if ("UNCAUGHT_EXCEPTION".equals(exitReason)) { log("错误", "客户端退出,原因=未捕获异常,线程=" + clean(uncaughtThread), uncaughtException); } else { log("错误", "客户端退出,原因=未收到正常关闭标记(可能被任务管理器终止、进程崩溃或系统终止)", null); } } } }, APP_NAME + "-log-shutdown")); heartbeat = Executors.newSingleThreadScheduledExecutor(r -> { Thread thread = new Thread(r, APP_NAME + "-heartbeat"); thread.setDaemon(true); return thread; }); heartbeat.scheduleAtFixedRate(() -> { synchronized (LOCK) { writeState(); } }, 5, 5, TimeUnit.SECONDS); } catch (Exception ignored) { // Logging is best-effort and must not stop client startup. } } } public static void login(String userId, String method, boolean success) { log(success ? "信息" : "警告", "登录" + (success ? "成功" : "失败") + ",方式=" + clean(method) + ",用户=" + clean(userId), null); } public static void logout(String userId) { log("信息", "用户退出,用户=" + clean(userId), null); } public static void scan(String type, String code) { log("信息", "扫码,类型=" + clean(type) + ",内容=" + clean(code), null); } public static void work(String action, String code, boolean success) { log(success ? "信息" : "警告", "操作" + (success ? "成功" : "失败") + ",动作=" + clean(action) + ",内容=" + clean(code), null); } public static void markUserClose() { synchronized (LOCK) { if (userClose) return; userClose = true; exitReason = "USER_CLOSE"; log("信息", "收到用户主动关闭标记", null); writeState(); } } public static void error(String message, Throwable exception) { log("错误", message, exception); } private static void reportPreviousRun() { if (stateFile == null || !stateFile.isFile()) return; Properties state = new Properties(); try (FileInputStream input = new FileInputStream(stateFile)) { state.load(input); log("错误", "检测到上次进程未正常关闭(可能被任务管理器结束),进程号=" + clean(state.getProperty("pid")) + ",最后心跳=" + clean(state.getProperty("heartbeat")) + ",状态=" + clean(state.getProperty("status")), null); } catch (Exception exception) { log("警告", "无法读取上次运行状态", exception); } } private static void writeState() { if (stateFile == null) return; Properties state = new Properties(); state.setProperty("pid", getPid()); state.setProperty("heartbeat", Long.toString(System.currentTimeMillis())); state.setProperty("status", "RUNNING"); File temporary = new File(stateFile.getParentFile(), stateFile.getName() + ".tmp"); try (FileOutputStream output = new FileOutputStream(temporary)) { state.store(output, "MES client running state"); } catch (IOException ignored) { return; } if (!temporary.renameTo(stateFile)) { temporary.delete(); } } private static void deleteState() { if (heartbeat != null) heartbeat.shutdownNow(); if (stateFile != null && stateFile.isFile() && !stateFile.delete()) { log("警告", "无法删除运行状态文件", null); } } private static String getPid() { String runtime = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); int separator = runtime.indexOf('@'); return separator > 0 ? runtime.substring(0, separator) : runtime; } private static String clean(String value) { return value == null ? "" : value.replace('\r', ' ').replace('\n', ' '); } private static void log(String level, String message, Throwable exception) { synchronized (LOCK) { if (out == null) return; if (exception != null) { message += ",异常类型=" + exception.getClass().getName() + ",详情=" + clean(exception.getMessage()); } out.println("[" + TIME.format(new Date()) + "][" + level + "][" + Thread.currentThread().getName() + "] " + message); out.flush(); } } }