Windows 迁移 M0。纯新增两个模块 + 4 个脚本接线,**macOS 行为零漂移**。 背景:仓库此前把 /tmp/findatahub-*.pid、~/Library/Logs、 ~/Library/Preferences/...plist 直接写死在十来个文件里;采集管线散落着 start_new_session=True、os.kill(pid,0)、signal.SIGHUP 等 POSIX 专属写法。 这些在 Windows 上分别是:路径不存在、ValueError、探测即杀进程、AttributeError。 - paths.py:temp_dir / pid_file / heartbeat_file / log_root / log_dir / app_support_dir / backups_dir / config_dir / user_defaults_plist / env_file - process_utils.py:detach_kwargs / is_running / terminate_pid / available_signals / register_signal_handler / restore_default_and_die / utf8_subprocess_env 两个必须记住的坑(已写成测试断言): 1. tempfile.gettempdir() 在 macOS 返回 $TMPDIR(/var/folders/.../T), 与现网 PID 文件所在的 /tmp **不是同一目录**。paths.temp_dir() 返回 /tmp 是刻意的,混用会导致新旧进程互相看不见。 2. Windows 上 os.kill(pid, 0) 等价于 TerminateProcess,不是「探测」, 必须用 OpenProcess。 接线:newsflash_collect(PID/心跳/日志/plist)、daily_summary(PID)、 migrate_date_format(备份目录)、install_collector_launchd(plist + 平台守卫)。 同时修 newsflash_collect.write_heartbeat 的静默失败:Swift 端以心跳 300s 未更新判定卡死并重启,而此处原为 except OSError: pass——重启理由 在日志里毫无痕迹。改为状态翻转才记录(心跳 60s 一次,无脑打日志会刷屏)。 验证:现网采集器 status 正常,5 个路径与旧硬编码逐字一致。 回归:test_paths_process_utils.py 32 条 + 心跳可观测性 5 条。
159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
# coding=utf-8
|
||
"""跨平台路径解析 —— 收敛所有 macOS 专属硬编码路径.
|
||
|
||
背景
|
||
----
|
||
仓库此前把 ``/tmp/findatahub-*.pid``、``~/Library/Logs/FinDataHub``、
|
||
``~/Library/Preferences/com.findatahub.app.plist`` 等路径直接写死在十来个
|
||
文件里。这在 macOS 上工作正常,但在 Windows 上:
|
||
|
||
* 没有 ``/tmp`` 目录(``Path.home()/"Library"`` 也不存在);
|
||
* 日志应落 ``%LOCALAPPDATA%``,而非 ``~/Library/Logs``;
|
||
* 用户偏好不在 plist,而在注册表 / 应用数据目录。
|
||
|
||
本模块是这些路径的**唯一事实来源**。迁移到其他平台只需改这里,
|
||
不必再全仓搜字符串。
|
||
|
||
设计约束(重要)
|
||
----------------
|
||
**macOS 上必须解析出与旧硬编码完全相同的字符串。** 原因是:
|
||
|
||
1. 现网采集器已经把这些路径写进了 launchd plist、Swift 端探测逻辑与
|
||
正在运行的 PID 文件里;路径一变,新旧进程会互相看不见,
|
||
表现为"采集器明明在跑,App 却报未运行"。
|
||
2. 特别注意 ``temp_dir()`` 在 macOS 返回 ``/tmp`` 而**不是**
|
||
``tempfile.gettempdir()``(后者是 ``$TMPDIR``,形如
|
||
``/var/folders/xx/.../T/``)。这两个路径不同,混用即事故。
|
||
|
||
用例::
|
||
|
||
from paths import pid_file, log_dir, app_support_dir
|
||
|
||
PID_FILE = pid_file("newsflash") # /tmp/findatahub-newsflash.pid
|
||
LOG_DIR = log_dir("newsflash") # ~/Library/Logs/FinDataHub/newsflash
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
APP_NAME = "FinDataHub"
|
||
|
||
IS_WINDOWS = sys.platform == "win32"
|
||
IS_MACOS = sys.platform == "darwin"
|
||
|
||
|
||
# ── 临时目录 ────────────────────────────────────────────────
|
||
|
||
def temp_dir() -> Path:
|
||
"""临时目录。
|
||
|
||
macOS/Linux 固定 ``/tmp``(与现网 PID/心跳文件位置保持一致,**不要**
|
||
换成 ``tempfile.gettempdir()``);Windows 用系统 TEMP。
|
||
"""
|
||
if IS_WINDOWS:
|
||
return Path(tempfile.gettempdir())
|
||
return Path("/tmp")
|
||
|
||
|
||
def pid_file(name: str) -> str:
|
||
"""PID 文件路径,如 ``pid_file("newsflash")`` → ``/tmp/findatahub-newsflash.pid``."""
|
||
return str(temp_dir() / f"findatahub-{name}.pid")
|
||
|
||
|
||
def heartbeat_file(name: str) -> str:
|
||
"""心跳文件路径(Swift 端用它免 spawn 判断采集器是否活跃)."""
|
||
return str(temp_dir() / f"findatahub-{name}.heartbeat")
|
||
|
||
|
||
# ── 日志目录 ────────────────────────────────────────────────
|
||
|
||
def log_root() -> Path:
|
||
"""日志根目录。
|
||
|
||
macOS: ``~/Library/Logs/FinDataHub``(Console.app 可直接查看)
|
||
Windows: ``%LOCALAPPDATA%\\FinDataHub\\logs``
|
||
"""
|
||
if IS_WINDOWS:
|
||
base = os.getenv("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
|
||
return Path(base) / APP_NAME / "logs"
|
||
if IS_MACOS:
|
||
return Path.home() / "Library" / "Logs" / APP_NAME
|
||
# Linux 走 XDG
|
||
base = os.getenv("XDG_STATE_HOME") or str(Path.home() / ".local" / "state")
|
||
return Path(base) / APP_NAME / "logs"
|
||
|
||
|
||
def log_dir(sub: str = "", *, create: bool = False) -> Path:
|
||
"""日志子目录,如 ``log_dir("newsflash")``。``create=True`` 时顺带建目录."""
|
||
d = log_root() / sub if sub else log_root()
|
||
if create:
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
return d
|
||
|
||
|
||
# ── 应用数据 / 配置 ─────────────────────────────────────────
|
||
|
||
def app_support_dir(*, create: bool = False) -> Path:
|
||
"""应用数据目录。
|
||
|
||
macOS: ``~/Library/Application Support/FinDataHub``
|
||
Windows: ``%LOCALAPPDATA%\\FinDataHub``
|
||
Linux: ``~/.local/share/FinDataHub``
|
||
"""
|
||
if IS_WINDOWS:
|
||
base = os.getenv("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
|
||
d = Path(base) / APP_NAME
|
||
elif IS_MACOS:
|
||
d = Path.home() / "Library" / "Application Support" / APP_NAME
|
||
else:
|
||
base = os.getenv("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
|
||
d = Path(base) / APP_NAME
|
||
if create:
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
return d
|
||
|
||
|
||
def backups_dir(*, create: bool = False) -> Path:
|
||
"""备份目录(迁移脚本、数据库快照的落点)."""
|
||
d = app_support_dir() / "backups"
|
||
if create:
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
return d
|
||
|
||
|
||
def config_dir(*, create: bool = False) -> Path:
|
||
"""配置文件目录(``env`` 文件、API Key 等)。
|
||
|
||
macOS: ``~/.config/findatahub``(与 ``~/.config/findatahub/env`` 一致)
|
||
Windows: ``%APPDATA%\\FinDataHub``
|
||
"""
|
||
if IS_WINDOWS:
|
||
base = os.getenv("APPDATA") or str(Path.home() / "AppData" / "Roaming")
|
||
d = Path(base) / APP_NAME
|
||
else:
|
||
base = os.getenv("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
||
d = Path(base) / "findatahub"
|
||
if create:
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
return d
|
||
|
||
|
||
def user_defaults_plist() -> Path | None:
|
||
"""macOS 用户偏好 plist 路径;非 macOS 返回 ``None``。
|
||
|
||
调用方必须处理 ``None``——Windows/Linux 上没有 plist,
|
||
偏好读取应回退到环境变量或配置文件。
|
||
"""
|
||
if not IS_MACOS:
|
||
return None
|
||
return Path.home() / "Library" / "Preferences" / "com.findatahub.app.plist"
|
||
|
||
|
||
def env_file() -> Path:
|
||
"""生产环境变量文件(TUSHARE_TOKEN / MYSQL_PASSWORD 等,权限 0600)."""
|
||
return config_dir() / "env"
|