|
|
@@ -1,10 +1,16 @@
|
|
|
package com.mes.util;
|
|
|
|
|
|
import java.io.File;
|
|
|
-import java.io.FileOutputStream;
|
|
|
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 {
|
|
|
@@ -12,6 +18,9 @@ public final class AppLog {
|
|
|
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;
|
|
|
@@ -27,24 +36,43 @@ public final class AppLog {
|
|
|
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) {
|
|
|
- exitReason = "UNCAUGHT_EXCEPTION";
|
|
|
- uncaughtThread = thread.getName();
|
|
|
- uncaughtException = 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() {
|
|
|
- if ("USER_CLOSE".equals(exitReason)) {
|
|
|
- log("INFO", "Client exited, reason=user-close", null);
|
|
|
- } else if ("UNCAUGHT_EXCEPTION".equals(exitReason)) {
|
|
|
- log("ERROR", "Client exited, reason=uncaught-exception, thread=" + clean(uncaughtThread), uncaughtException);
|
|
|
+ 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.
|
|
|
}
|
|
|
@@ -52,21 +80,79 @@ public final class AppLog {
|
|
|
}
|
|
|
|
|
|
public static void login(String userId, String method, boolean success) {
|
|
|
- log(success ? "INFO" : "WARN", "Login " + (success ? "succeeded" : "failed")
|
|
|
- + ", method=" + clean(method) + ", user=" + clean(userId), null);
|
|
|
+ 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("INFO", "Scan, type=" + clean(type) + ", code=" + clean(code), null);
|
|
|
+ log("信息", "扫码,类型=" + clean(type) + ",内容=" + clean(code), null);
|
|
|
}
|
|
|
|
|
|
public static void work(String action, String code, boolean success) {
|
|
|
- log(success ? "INFO" : "WARN", "Operation " + (success ? "succeeded" : "failed")
|
|
|
- + ", action=" + clean(action) + ", code=" + clean(code), null);
|
|
|
+ log(success ? "信息" : "警告", "操作" + (success ? "成功" : "失败")
|
|
|
+ + ",动作=" + clean(action) + ",内容=" + clean(code), null);
|
|
|
}
|
|
|
|
|
|
public static void markUserClose() {
|
|
|
- exitReason = "USER_CLOSE";
|
|
|
+ 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) {
|
|
|
@@ -76,9 +162,12 @@ public final class AppLog {
|
|
|
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);
|
|
|
- if (exception != null) exception.printStackTrace(out);
|
|
|
out.flush();
|
|
|
}
|
|
|
}
|