mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
Compare commits
3 Commits
@@ -14,6 +14,34 @@
|
||||
|
||||
Var WDA_InstallDirPage
|
||||
|
||||
!macro customInit
|
||||
; Safety: older versions created an `output` junction inside the install directory that points to the
|
||||
; per-user AppData `output` folder. Some uninstall/update flows may traverse that junction and delete
|
||||
; real user data. Remove it as early as possible during install/update.
|
||||
Call WDA_RemoveLegacyOutputLink
|
||||
!macroend
|
||||
|
||||
!macro customInstall
|
||||
; Provide a safe, non-junction way for users to locate the real per-user output directory.
|
||||
; The actual data is NOT stored inside $INSTDIR (it is wiped on update/reinstall).
|
||||
; `open-output.cmd` uses %APPDATA% so it works for the current user.
|
||||
FileOpen $0 "$INSTDIR\output-location.txt" w
|
||||
FileWrite $0 "WeChatDataAnalysis output folder (per user):$\r$\n%APPDATA%\\${APP_PACKAGE_NAME}\\output$\r$\n"
|
||||
FileClose $0
|
||||
|
||||
FileOpen $1 "$INSTDIR\open-output.cmd" w
|
||||
; NSIS escaping: use $\" to output a literal quote character into the .cmd file.
|
||||
FileWrite $1 "@echo off$\r$\nexplorer $\"%APPDATA%\\${APP_PACKAGE_NAME}\\output$\"$\r$\n"
|
||||
FileClose $1
|
||||
!macroend
|
||||
|
||||
Function WDA_RemoveLegacyOutputLink
|
||||
; $INSTDIR is usually the full install directory. Be defensive and also try the nested path
|
||||
; in case the installer is running before electron-builder appends "\${APP_FILENAME}".
|
||||
RMDir "$INSTDIR\output"
|
||||
RMDir "$INSTDIR\${APP_FILENAME}\output"
|
||||
FunctionEnd
|
||||
|
||||
!macro customPageAfterChangeDir
|
||||
; Add a confirmation page after the directory picker so users clearly see
|
||||
; the final install location (includes the app sub-folder).
|
||||
@@ -90,6 +118,10 @@ Var /GLOBAL WDA_DeleteUserData
|
||||
!macro customUnInit
|
||||
; Default: keep user data (also applies to silent uninstall / update uninstall).
|
||||
StrCpy $WDA_DeleteUserData "0"
|
||||
|
||||
; Safety: if an older build created an `output` junction inside the install dir, remove it early so
|
||||
; directory cleanup can't traverse it and delete the real per-user output folder.
|
||||
RMDir "$INSTDIR\output"
|
||||
!macroend
|
||||
|
||||
!macro customUnWelcomePage
|
||||
|
||||
+174
-13
@@ -123,6 +123,68 @@ function isPortAvailable(port, host) {
|
||||
});
|
||||
}
|
||||
|
||||
function getEphemeralPort(host) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const srv = net.createServer();
|
||||
srv.unref();
|
||||
srv.once("error", () => resolve(null));
|
||||
srv.listen({ port: 0, host }, () => {
|
||||
const addr = srv.address();
|
||||
const p = addr && typeof addr === "object" ? Number(addr.port) : null;
|
||||
srv.close(() => resolve(Number.isInteger(p) ? p : null));
|
||||
});
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function chooseAvailablePort(preferredPort, host) {
|
||||
const preferred = parsePort(preferredPort);
|
||||
if (preferred != null && (await isPortAvailable(preferred, host))) return preferred;
|
||||
|
||||
// Keep the port close to the user's expectation when possible.
|
||||
if (preferred != null) {
|
||||
for (let i = 1; i <= 50; i += 1) {
|
||||
const cand = preferred + i;
|
||||
if (cand > 65535) break;
|
||||
if (await isPortAvailable(cand, host)) return cand;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to an OS-chosen ephemeral port.
|
||||
const random = await getEphemeralPort(host);
|
||||
if (random != null && (await isPortAvailable(random, host))) return random;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ensureBackendPortAvailableOnStartup() {
|
||||
// Avoid surprising behavior in dev: the frontend dev server expects a stable backend port.
|
||||
if (!app.isPackaged) return getBackendPort();
|
||||
|
||||
const bindHost = getBackendBindHost();
|
||||
const currentPort = getBackendPort();
|
||||
const ok = await isPortAvailable(currentPort, bindHost);
|
||||
if (ok) return currentPort;
|
||||
|
||||
const chosen = await chooseAvailablePort(currentPort, bindHost);
|
||||
if (chosen == null) {
|
||||
logMain(`[main] backend port unavailable: ${currentPort} host=${bindHost}; failed to find a free port`);
|
||||
return currentPort;
|
||||
}
|
||||
|
||||
try {
|
||||
setBackendPortSetting(chosen);
|
||||
logMain(`[main] backend port ${currentPort} unavailable; switched to ${chosen}`);
|
||||
} catch (err) {
|
||||
logMain(`[main] failed to persist backend port ${chosen}: ${err?.message || err}`);
|
||||
}
|
||||
|
||||
return getBackendPort();
|
||||
}
|
||||
|
||||
function resolveDataDir() {
|
||||
if (resolvedDataDir) return resolvedDataDir;
|
||||
|
||||
@@ -163,7 +225,11 @@ function getExeDir() {
|
||||
|
||||
function ensureOutputLink() {
|
||||
// Users often expect an `output/` folder near the installed exe. We keep the real data
|
||||
// in the per-user data dir, and (when possible) create a Windows junction next to the exe.
|
||||
// in the per-user data dir.
|
||||
//
|
||||
// NOTE: We intentionally avoid creating a junction/symlink inside the install directory.
|
||||
// Some uninstall/update flows may traverse reparse points and delete the target directory,
|
||||
// causing data loss (the install dir is removed on every update/reinstall).
|
||||
if (!app.isPackaged) return;
|
||||
|
||||
const exeDir = getExeDir();
|
||||
@@ -171,26 +237,56 @@ function ensureOutputLink() {
|
||||
if (!exeDir || !dataDir) return;
|
||||
|
||||
const target = path.join(dataDir, "output");
|
||||
const linkPath = path.join(exeDir, "output");
|
||||
const legacyLinkPath = path.join(exeDir, "output");
|
||||
|
||||
// If the target doesn't exist yet, create it so the link points somewhere real.
|
||||
// Ensure the real output dir exists.
|
||||
try {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
} catch {}
|
||||
|
||||
// If something already exists at linkPath, do not overwrite it.
|
||||
// Best-effort: remove a legacy junction/symlink at `exeDir/output` so uninstallers can't
|
||||
// accidentally traverse it and delete the real per-user output directory.
|
||||
try {
|
||||
if (fs.existsSync(linkPath)) return;
|
||||
const st = fs.lstatSync(legacyLinkPath);
|
||||
if (st.isSymbolicLink()) {
|
||||
try {
|
||||
fs.unlinkSync(legacyLinkPath);
|
||||
logMain(`[main] removed legacy output link: ${legacyLinkPath}`);
|
||||
} catch (err) {
|
||||
logMain(`[main] failed to remove legacy output link: ${err?.message || err}`);
|
||||
}
|
||||
} else if (st.isDirectory()) {
|
||||
const entries = fs.readdirSync(legacyLinkPath);
|
||||
if (Array.isArray(entries) && entries.length === 0) {
|
||||
// Remove an empty real directory to reduce confusion (it will be recreated by the backend if needed).
|
||||
fs.rmdirSync(legacyLinkPath);
|
||||
} else {
|
||||
// Do not overwrite non-empty directories to avoid data loss.
|
||||
// Note: data stored here will be wiped on update/reinstall.
|
||||
logMain(
|
||||
`[main] output dir exists in install dir (not a link): ${legacyLinkPath}. real data dir output: ${target}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logMain(`[main] output path exists and is not a directory/link: ${legacyLinkPath}`);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
// Doesn't exist yet.
|
||||
}
|
||||
|
||||
// Best-effort: drop a helper file next to the exe so users can find the real data.
|
||||
// This avoids the data-loss risks of using junctions/symlinks under the install directory.
|
||||
try {
|
||||
fs.symlinkSync(target, linkPath, "junction");
|
||||
logMain(`[main] created output link: ${linkPath} -> ${target}`);
|
||||
} catch (err) {
|
||||
logMain(`[main] failed to create output link: ${err?.message || err}`);
|
||||
}
|
||||
const p = path.join(exeDir, "output-location.txt");
|
||||
const text = `WeChatDataAnalysis data directory\n\nOutput folder:\n${target}\n`;
|
||||
fs.writeFileSync(p, text, { encoding: "utf8" });
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const p = path.join(exeDir, "open-output.cmd");
|
||||
const text = `@echo off\r\nexplorer \"${target}\"\r\n`;
|
||||
fs.writeFileSync(p, text, { encoding: "utf8" });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getMainLogPath() {
|
||||
@@ -990,6 +1086,16 @@ async function waitForBackend({ timeoutMs, healthUrl } = {}) {
|
||||
const startedAt = Date.now();
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// If the backend process died, fail fast (otherwise we'd wait for the full timeout).
|
||||
if (!backendProc) {
|
||||
throw new Error(`Backend process exited before becoming ready: ${url}`);
|
||||
}
|
||||
if (backendProc.exitCode != null) {
|
||||
throw new Error(
|
||||
`Backend process exited (code=${backendProc.exitCode} signal=${backendProc.signalCode || "null"}): ${url}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const code = await httpGet(url);
|
||||
if (code >= 200 && code < 500) return;
|
||||
@@ -1254,6 +1360,30 @@ function registerWindowIpc() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("app:getOutputDir", () => {
|
||||
const dir = resolveDataDir();
|
||||
if (!dir) return "";
|
||||
return path.join(dir, "output");
|
||||
});
|
||||
|
||||
ipcMain.handle("app:openOutputDir", async () => {
|
||||
const dir = resolveDataDir();
|
||||
if (!dir) throw new Error("无法定位数据目录");
|
||||
const outDir = path.join(dir, "output");
|
||||
try {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
} catch {}
|
||||
try {
|
||||
const err = await shell.openPath(outDir);
|
||||
if (err) throw new Error(err);
|
||||
return { success: true, path: outDir };
|
||||
} catch (e) {
|
||||
const message = e?.message || String(e);
|
||||
logMain(`[main] openOutputDir failed: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("app:checkForUpdates", async () => {
|
||||
return await checkForUpdatesInternal();
|
||||
});
|
||||
@@ -1272,6 +1402,11 @@ function registerWindowIpc() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Safety: remove legacy `output` junctions in the install dir before triggering the NSIS update/uninstall.
|
||||
// Some uninstall flows may traverse reparse points and delete the real per-user output directory.
|
||||
try {
|
||||
ensureOutputLink();
|
||||
} catch {}
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
@@ -1312,15 +1447,41 @@ async function main() {
|
||||
registerWindowIpc();
|
||||
registerDebugShortcuts();
|
||||
|
||||
// Resolve/create the data dir early so we can log reliably and (optionally) place a link
|
||||
// Resolve/create the data dir early so we can log reliably and place helper files
|
||||
// next to the installed exe for easier access.
|
||||
resolveDataDir();
|
||||
ensureOutputLink();
|
||||
await ensureBackendPortAvailableOnStartup();
|
||||
|
||||
logMain(`[main] app.isPackaged=${app.isPackaged} argv=${JSON.stringify(process.argv)}`);
|
||||
|
||||
startBackend();
|
||||
await waitForBackend({ timeoutMs: 30_000 });
|
||||
try {
|
||||
await waitForBackend({ timeoutMs: 30_000 });
|
||||
} catch (err) {
|
||||
// In some environments a specific port may be blocked/reserved (WSAEACCES) or taken.
|
||||
// Best-effort: pick a new port and retry once so the app can still start.
|
||||
if (app.isPackaged) {
|
||||
const prevPort = getBackendPort();
|
||||
const bindHost = getBackendBindHost();
|
||||
const nextPort = await chooseAvailablePort(prevPort + 1, bindHost);
|
||||
if (nextPort != null && nextPort !== prevPort) {
|
||||
logMain(`[main] backend not ready on port ${prevPort}; retrying on ${nextPort}`);
|
||||
try {
|
||||
setBackendPortSetting(nextPort);
|
||||
await restartBackend({ timeoutMs: 30_000 });
|
||||
logMain(`[main] backend retry succeeded on port ${nextPort}`);
|
||||
} catch (retryErr) {
|
||||
logMain(`[main] backend retry failed: ${retryErr?.stack || String(retryErr)}`);
|
||||
throw retryErr;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const win = createMainWindow();
|
||||
mainWindow = win;
|
||||
|
||||
@@ -19,6 +19,10 @@ contextBridge.exposeInMainWorld("wechatDesktop", {
|
||||
|
||||
chooseDirectory: (options = {}) => ipcRenderer.invoke("dialog:chooseDirectory", options),
|
||||
|
||||
// Data/output folder helpers
|
||||
getOutputDir: () => ipcRenderer.invoke("app:getOutputDir"),
|
||||
openOutputDir: () => ipcRenderer.invoke("app:openOutputDir"),
|
||||
|
||||
// Auto update
|
||||
getVersion: () => ipcRenderer.invoke("app:getVersion"),
|
||||
checkForUpdates: () => ipcRenderer.invoke("app:checkForUpdates"),
|
||||
|
||||
@@ -142,6 +142,24 @@
|
||||
<div v-if="desktopBackendPortError" class="text-xs text-red-600 whitespace-pre-wrap -mt-1.5">
|
||||
{{ desktopBackendPortError }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-[13px] font-medium text-[#222]">output 目录</div>
|
||||
<div class="mt-0.5 text-[11px] text-[#909090] break-words">{{ desktopOutputDirText }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-[6px] border border-[#e2e2e2] bg-white px-2 py-1 text-[12px] text-[#222] transition hover:bg-[#f9f9f9] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!isDesktopEnv || desktopOutputDirLoading"
|
||||
@click="onDesktopOpenOutputDir"
|
||||
>
|
||||
打开 output
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="desktopOutputDirError" class="text-xs text-red-600 whitespace-pre-wrap -mt-1.5">
|
||||
{{ desktopOutputDirError }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -288,6 +306,15 @@ const desktopBackendPortApplying = ref(false)
|
||||
const desktopBackendPortError = ref('')
|
||||
const desktopBackendPortDefault = ref(10392)
|
||||
|
||||
const desktopOutputDir = ref('')
|
||||
const desktopOutputDirLoading = ref(false)
|
||||
const desktopOutputDirError = ref('')
|
||||
const desktopOutputDirText = computed(() => {
|
||||
if (!isDesktopEnv.value) return '仅桌面端可用'
|
||||
const v = String(desktopOutputDir.value || '').trim()
|
||||
return v || '—'
|
||||
})
|
||||
|
||||
const switchTrackClass = (enabled, disabled = false) => {
|
||||
if (disabled) return enabled ? 'bg-[#07b75b] opacity-50 cursor-not-allowed' : 'bg-[#d0d0d0] opacity-50 cursor-not-allowed'
|
||||
return enabled ? 'bg-[#07b75b] hover:brightness-95' : 'bg-[#d0d0d0] hover:brightness-95'
|
||||
@@ -437,6 +464,36 @@ const refreshDesktopBackendPort = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshDesktopOutputDir = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
if (!window.wechatDesktop?.getOutputDir) return
|
||||
desktopOutputDirLoading.value = true
|
||||
desktopOutputDirError.value = ''
|
||||
try {
|
||||
const v = await window.wechatDesktop.getOutputDir()
|
||||
desktopOutputDir.value = String(v || '').trim()
|
||||
} catch (e) {
|
||||
desktopOutputDirError.value = e?.message || '读取 output 目录失败'
|
||||
} finally {
|
||||
desktopOutputDirLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onDesktopOpenOutputDir = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
if (!window.wechatDesktop?.openOutputDir) return
|
||||
desktopOutputDirLoading.value = true
|
||||
desktopOutputDirError.value = ''
|
||||
try {
|
||||
const res = await window.wechatDesktop.openOutputDir()
|
||||
if (res?.path) desktopOutputDir.value = String(res.path || '').trim()
|
||||
} catch (e) {
|
||||
desktopOutputDirError.value = e?.message || '打开 output 目录失败'
|
||||
} finally {
|
||||
desktopOutputDirLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const applyDesktopBackendPort = async () => {
|
||||
if (!process.client || typeof window === 'undefined') return
|
||||
const raw = String(desktopBackendPortInput.value || '').trim()
|
||||
@@ -567,6 +624,7 @@ onMounted(async () => {
|
||||
void desktopUpdate.initListeners()
|
||||
await refreshDesktopAutoLaunch()
|
||||
await refreshDesktopCloseBehavior()
|
||||
await refreshDesktopOutputDir()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
@@ -10,8 +10,13 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import FileResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from . import __version__ as APP_VERSION
|
||||
from .logging_config import setup_logging, get_logger
|
||||
|
||||
# 初始化日志系统
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
from . import __version__ as APP_VERSION
|
||||
from .path_fix import PathFixRoute
|
||||
from .chat_realtime_autosync import CHAT_REALTIME_AUTOSYNC
|
||||
from .routers.chat import router as _chat_router
|
||||
@@ -30,10 +35,6 @@ from .routers.wrapped import router as _wrapped_router
|
||||
from .sns_stage_timing import add_sns_stage_timing_headers
|
||||
from .wcdb_realtime import WCDB_REALTIME, shutdown as _wcdb_shutdown
|
||||
|
||||
# 初始化日志系统
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="微信数据库解密工具",
|
||||
description="现代化的微信数据库解密工具,支持微信信息检测和数据库解密功能",
|
||||
|
||||
@@ -53,9 +53,9 @@ class WeChatLogger:
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._initialized:
|
||||
self.setup_logging()
|
||||
WeChatLogger._initialized = True
|
||||
# Lazy-init in `setup_logging()` / accessors to avoid double-initialization when
|
||||
# callers instantiate the manager and then call `setup_logging()` again.
|
||||
pass
|
||||
|
||||
def setup_logging(self, log_level: str = "INFO"):
|
||||
"""设置日志配置"""
|
||||
@@ -66,7 +66,9 @@ class WeChatLogger:
|
||||
|
||||
# 创建日志目录
|
||||
now = datetime.now()
|
||||
log_dir = Path("output/logs") / str(now.year) / f"{now.month:02d}" / f"{now.day:02d}"
|
||||
from .app_paths import get_output_dir
|
||||
|
||||
log_dir = get_output_dir() / "logs" / str(now.year) / f"{now.month:02d}" / f"{now.day:02d}"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置日志文件名
|
||||
@@ -77,6 +79,10 @@ class WeChatLogger:
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
try:
|
||||
handler.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 配置日志格式
|
||||
# 文件格式(无颜色)
|
||||
@@ -109,22 +115,48 @@ class WeChatLogger:
|
||||
|
||||
# 只为uvicorn日志器添加文件处理器,保持其原有的控制台处理器(带颜色)
|
||||
uvicorn_logger = logging.getLogger("uvicorn")
|
||||
for handler in uvicorn_logger.handlers[:]:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
uvicorn_logger.removeHandler(handler)
|
||||
try:
|
||||
handler.close()
|
||||
except Exception:
|
||||
pass
|
||||
uvicorn_logger.addHandler(file_handler)
|
||||
uvicorn_logger.setLevel(level)
|
||||
|
||||
# 只为uvicorn.access日志器添加文件处理器
|
||||
uvicorn_access_logger = logging.getLogger("uvicorn.access")
|
||||
for handler in uvicorn_access_logger.handlers[:]:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
uvicorn_access_logger.removeHandler(handler)
|
||||
try:
|
||||
handler.close()
|
||||
except Exception:
|
||||
pass
|
||||
uvicorn_access_logger.addHandler(file_handler)
|
||||
uvicorn_access_logger.setLevel(level)
|
||||
|
||||
# 只为uvicorn.error日志器添加文件处理器
|
||||
uvicorn_error_logger = logging.getLogger("uvicorn.error")
|
||||
for handler in uvicorn_error_logger.handlers[:]:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
uvicorn_error_logger.removeHandler(handler)
|
||||
try:
|
||||
handler.close()
|
||||
except Exception:
|
||||
pass
|
||||
uvicorn_error_logger.addHandler(file_handler)
|
||||
uvicorn_error_logger.setLevel(level)
|
||||
|
||||
# 配置FastAPI日志器
|
||||
fastapi_logger = logging.getLogger("fastapi")
|
||||
fastapi_logger.handlers = []
|
||||
for handler in fastapi_logger.handlers[:]:
|
||||
fastapi_logger.removeHandler(handler)
|
||||
try:
|
||||
handler.close()
|
||||
except Exception:
|
||||
pass
|
||||
fastapi_logger.addHandler(file_handler)
|
||||
fastapi_logger.addHandler(console_handler)
|
||||
fastapi_logger.setLevel(level)
|
||||
@@ -136,6 +168,8 @@ class WeChatLogger:
|
||||
logger.info(f"日志文件: {self.log_file}")
|
||||
logger.info(f"日志级别: {logging.getLevelName(level)}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
WeChatLogger._initialized = True
|
||||
|
||||
return self.log_file
|
||||
|
||||
@@ -145,6 +179,8 @@ class WeChatLogger:
|
||||
|
||||
def get_log_file_path(self) -> Path:
|
||||
"""获取当前日志文件路径"""
|
||||
if not hasattr(self, "log_file"):
|
||||
self.setup_logging()
|
||||
return self.log_file
|
||||
|
||||
|
||||
@@ -157,10 +193,14 @@ def setup_logging(log_level: str = "INFO") -> Path:
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""获取日志器的便捷函数"""
|
||||
logger_manager = WeChatLogger()
|
||||
if not WeChatLogger._initialized:
|
||||
logger_manager.setup_logging()
|
||||
return logger_manager.get_logger(name)
|
||||
|
||||
|
||||
def get_log_file_path() -> Path:
|
||||
"""获取当前日志文件路径的便捷函数"""
|
||||
logger_manager = WeChatLogger()
|
||||
if not WeChatLogger._initialized:
|
||||
logger_manager.setup_logging()
|
||||
return logger_manager.get_log_file_path()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
|
||||
def _close_logging_handlers() -> None:
|
||||
# Close handlers to avoid Windows temp dir cleanup failures (FileHandler holds a lock).
|
||||
import logging
|
||||
|
||||
for logger_name in ("", "uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"):
|
||||
lg = logging.getLogger(logger_name)
|
||||
for h in lg.handlers[:]:
|
||||
try:
|
||||
h.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
lg.removeHandler(h)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class TestLoggingConfigDataDir(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._prev_data_dir = os.environ.get("WECHAT_TOOL_DATA_DIR")
|
||||
self._td = TemporaryDirectory()
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = self._td.name
|
||||
|
||||
import wechat_decrypt_tool.app_paths as app_paths
|
||||
import wechat_decrypt_tool.logging_config as logging_config
|
||||
|
||||
importlib.reload(app_paths)
|
||||
importlib.reload(logging_config)
|
||||
|
||||
self.logging_config = logging_config
|
||||
|
||||
def tearDown(self):
|
||||
_close_logging_handlers()
|
||||
|
||||
if self._prev_data_dir is None:
|
||||
os.environ.pop("WECHAT_TOOL_DATA_DIR", None)
|
||||
else:
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = self._prev_data_dir
|
||||
self._td.cleanup()
|
||||
|
||||
def test_setup_logging_uses_wechat_tool_data_dir(self):
|
||||
log_file = self.logging_config.setup_logging()
|
||||
|
||||
base = Path(self._td.name) / "output" / "logs"
|
||||
self.assertTrue(log_file.is_relative_to(base))
|
||||
self.assertTrue(log_file.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user