From 9c1ccac473ea0247960603829dafd875f441c82e Mon Sep 17 00:00:00 2001 From: Sora <1294058688@qq.com> Date: Sat, 22 Aug 2026 11:55:19 +0800 Subject: [PATCH 1/4] fix(macOS): avoid translocated execution paths --- ...annot verify the developer's solution).txt | 8 ++- ...¯·看这个(包已损坏,无法验证开发者解决方案).txt | 8 ++- ElectronJS/main.js | 41 +++++++++--- ElectronJS/package_macos.sh | 16 +++++ ElectronJS/server.js | 31 +++++---- ElectronJS/src/taskGrid/executeTask.html | 21 ++++++- ExecuteStage/easyspider_executestage.py | 63 +++++++++++++++++-- 7 files changed, 152 insertions(+), 36 deletions(-) diff --git a/.temp_to_pub/EasySpider_MacOS/First time open please see this (The package is damaged and cannot verify the developer's solution).txt b/.temp_to_pub/EasySpider_MacOS/First time open please see this (The package is damaged and cannot verify the developer's solution).txt index 0d1be28..df16811 100644 --- a/.temp_to_pub/EasySpider_MacOS/First time open please see this (The package is damaged and cannot verify the developer's solution).txt +++ b/.temp_to_pub/EasySpider_MacOS/First time open please see this (The package is damaged and cannot verify the developer's solution).txt @@ -58,6 +58,12 @@ xattr: [Errno 13] Permission denied: 'EasySpider.app/Contents/Resources Now, you can design tasks as you would in other operating systems. +- Running tasks after approving the app in Privacy & Security + +After approving EasySpider, use "Directly Run Locally (Clean Mode)" or "Directly Run Locally (Data Mode)" on the task page. The current release starts the signed execution stage bundled inside the app automatically. You do not need to copy a `cd` command from the page, and you should not use an App Translocation path under `/private/var/...`. + +For scheduled or command-line execution, continue with the "Execution stage" instructions below and run `execute_macos.sh` from the actual downloaded EasySpider_MacOS directory, not from a private temporary path shown while the app is running. + - Execution phase @@ -67,4 +73,4 @@ During the execution of the task, if an error similar to the following occurs, i Traceback (most recent call last): File "multiprocessing/resource_tracker.py", line 209, in main KeyError: '/mp-5dxyey7c' -File access permissions must be granted, but microphone permissions are not needed at all. The author is also unclear why microphone access permissions would be requested, so you can refuse. \ No newline at end of file +File access permissions must be granted, but microphone permissions are not needed at all. The author is also unclear why microphone access permissions would be requested, so you can refuse. diff --git a/.temp_to_pub/EasySpider_MacOS/第一次打开请看这个(包已损坏,无法验证开发者解决方案).txt b/.temp_to_pub/EasySpider_MacOS/第一次打开请看这个(包已损坏,无法验证开发者解决方案).txt index edc307e..3ba455a 100644 --- a/.temp_to_pub/EasySpider_MacOS/第一次打开请看这个(包已损坏,无法验证开发者解决方案).txt +++ b/.temp_to_pub/EasySpider_MacOS/第一次打开请看这个(包已损坏,无法验证开发者解决方案).txt @@ -55,6 +55,12 @@ xattr: [Errno 13] Permission denied: 'EasySpider.app/Contents/Resources/app/node 现在,您就可以像在其他操作系统中一样设计任务了。 +- 使用隐私与安全性批准打开后的执行方式 + +批准打开EasySpider后,在任务页面点击“本地直接执行(纯净模式)”或“本地直接执行(带用户信息模式)”即可。新版程序会自动启动应用内置并已签名的执行阶段程序,不需要根据页面中显示的路径手动执行 `cd`,也不要使用 App Translocation 生成的 `/private/var/...` 临时路径。 + +如果需要定时任务或命令行执行,仍可按照下方“执行阶段”说明使用发布目录中的 `execute_macos.sh`;该方式请在实际下载的EasySpider_MacOS目录中运行,不要复制应用运行时显示的private临时目录。 + - 执行阶段 @@ -65,4 +71,4 @@ xattr: [Errno 13] Permission denied: 'EasySpider.app/Contents/Resources/app/node Traceback (most recent call last): File "multiprocessing/resource_tracker.py", line 209, in main KeyError: '/mp-5dxyey7c' -必须授予文件访问权限,但根本不需要麦克风权限。作者也不清楚为什么会请求麦克风访问权限,所以可以拒绝。 \ No newline at end of file +必须授予文件访问权限,但根本不需要麦克风权限。作者也不清楚为什么会请求麦克风访问权限,所以可以拒绝。 diff --git a/ElectronJS/main.js b/ElectronJS/main.js index 20a5904..7d226ac 100644 --- a/ElectronJS/main.js +++ b/ElectronJS/main.js @@ -97,7 +97,11 @@ if (process.platform === "win32" && process.arch === "ia32") { __dirname, "chrome_mac64.app/Contents/MacOS/Google Chrome" ); - execute_path = path.join(__dirname, ""); + // Keep the macOS executor inside the signed application bundle. An app + // opened through Gatekeeper's "Allow Anyway" flow can be App Translocated; + // sibling files next to the .app then resolve to a temporary private path. + // The packaged macOS build copies this binary into Resources/app. + execute_path = path.join(__dirname, "easyspider_executestage"); } else if (process.platform === "linux") { driverPath = path.join(__dirname, "chrome_linux64/chromedriver_linux64"); chromeBinaryPath = path.join(__dirname, "chrome_linux64/chrome"); @@ -1093,20 +1097,37 @@ async function beginInvoke(msg, ws) { // }); let spawn = require("child_process").spawn; - if ( - process.platform != "darwin" && - msg.message.execute_type == 1 && - msg.message.id != -1 - ) { - let child_process = spawn(execute_path, parameters); - child_process.stdout.on("data", function (data) { - console.log(data.toString()); - }); + let execution_started = false; + if (msg.message.execute_type == 1 && msg.message.id != -1) { + if (process.platform === "darwin" && !fs.existsSync(execute_path)) { + console.error("macOS execution stage is missing from the app bundle:", execute_path); + } else { + const child_process = spawn(execute_path, parameters, { + // All mutable task state belongs in Electron's stable + // userData directory, never in a translocated app path. + cwd: task_server.getDir(), + env: { + ...process.env, + PYTHONUNBUFFERED: "1", + PYTHONUTF8: "1", + EASYSPIDER_DATA_DIR: task_server.getDir(), + EASYSPIDER_APP_RESOURCES: __dirname, + }, + }); + execution_started = true; + child_process.on("error", (error) => { + console.error("Failed to start execution stage:", error); + }); + child_process.stdout.on("data", function (data) { + console.log(data.toString()); + }); + } } ws.send( JSON.stringify({ config_folder: task_server.getDir() + "/", easyspider_location: task_server.getEasySpiderLocation(), + execution_started, }) ); } else if (msg.type == 6) { diff --git a/ElectronJS/package_macos.sh b/ElectronJS/package_macos.sh index 950fb33..bf5b4c6 100755 --- a/ElectronJS/package_macos.sh +++ b/ElectronJS/package_macos.sh @@ -69,6 +69,20 @@ find "$STAGE_DIR" -type d -name .pytest_cache -prune -exec rm -rf {} + chmod 755 "$STAGE_DIR/first_time_run.sh" "$STAGE_DIR/execute_macos.sh" 2>/dev/null || true chmod 755 "$STAGE_DIR/easyspider_executestage" "$STAGE_DIR/easyspider_executestage_full" 2>/dev/null || true +# Keep copies inside the signed app as well as in the portable release folder. +# Gatekeeper may App-Translocate the .app when a user approves it in Privacy & +# Security; an external sibling executable would then no longer be reachable +# at the path shown by the translocated process. Electron launches these +# bundled copies with the stable userData directory as their working directory. +for executor in easyspider_executestage easyspider_executestage_full; do + if [[ ! -f "$STAGE_DIR/$executor" ]]; then + echo "Missing macOS execution stage: $STAGE_DIR/$executor" >&2 + exit 1 + fi + cp "$STAGE_DIR/$executor" "$APP_RESOURCES/$executor" + chmod 755 "$APP_RESOURCES/$executor" +done + # Sign only after all files have been copied/deleted. Set CODESIGN_IDENTITY to # a Developer ID identity for a distributable build; the default '-' is ad hoc. CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}" @@ -82,6 +96,8 @@ sign_path() { } sign_path "$STAGE_DIR/easyspider_executestage" sign_path "$STAGE_DIR/easyspider_executestage_full" +sign_path "$APP_RESOURCES/easyspider_executestage" +sign_path "$APP_RESOURCES/easyspider_executestage_full" sign_path "$APP_PATH" codesign --verify --deep --strict "$APP_PATH" echo "macOS $HOST_ARCH application staged at $STAGE_DIR" diff --git a/ElectronJS/server.js b/ElectronJS/server.js index c34ae99..b87a98e 100644 --- a/ElectronJS/server.js +++ b/ElectronJS/server.js @@ -33,26 +33,23 @@ function compare(p) { } function getDir() { - if (__dirname.indexOf("app") >= 0 && __dirname.indexOf("sources") >= 0) { - if (process.platform == "darwin") { - return app.getPath("userData"); - } else { - return path.join(__dirname, "../../.."); - } - } else { - return __dirname; - } + // On macOS, never use the packaged app's __dirname for mutable data. + // Gatekeeper may launch the app through App Translocation, where that path + // points into a temporary private directory. Electron's userData path is + // stable across normal launches and security approval flows. Preserve the + // historical portable-root behaviour on Windows and Linux. + if (app.isPackaged && process.platform === "darwin") return app.getPath("userData"); + if (app.isPackaged) return path.resolve(process.resourcesPath, "../.."); + return __dirname; } function getEasySpiderLocation() { - if (__dirname.indexOf("app") >= 0 && __dirname.indexOf("sources") >= 0) { - if (process.platform == "darwin") { - return path.join(__dirname, "../../../"); - } else { - return path.join(__dirname, "../../../"); - } - } else { - return __dirname; + if (app.isPackaged) { + // This is retained for the Windows/Linux resource lookup and diagnostics. + // macOS execution no longer depends on this path: the executor is bundled + // under Resources/app and is started directly by Electron. + return path.resolve(process.resourcesPath, "../.."); } + return __dirname; } if (!fs.existsSync(path.join(getDir(), "tasks"))) { fs.mkdirSync(path.join(getDir(), "tasks")); diff --git a/ElectronJS/src/taskGrid/executeTask.html b/ElectronJS/src/taskGrid/executeTask.html index 4c73ee9..a6e66bb 100644 --- a/ElectronJS/src/taskGrid/executeTask.html +++ b/ElectronJS/src/taskGrid/executeTask.html @@ -348,6 +348,7 @@ config_folder: "", easyspider_location: "", mysql_config_path: "", + executionStarted: false, OS: "Windows", }, mounted() { $.get(this.backEndAddressServiceWrapper + "/getConfig", function (result) { @@ -558,13 +559,22 @@ } }; app.$data.ID = result; + app.$data.executionStarted = false; ws.send(JSON.stringify(message)); // 使用函数并打印结果 const systemInfo = detectOperatingSystemAndArch(); // $.get(app.$data.backEndAddressServiceWrapper + "/queryOSVersion", function (OSInfo) { if (systemInfo.OS == 'MacOS') { changeCommand(); - $('#myModal').modal('show'); + // The packaged macOS executor is now launched inside the + // signed app bundle. Wait briefly for the websocket + // acknowledgement before falling back to the manual + // command dialog used by older packages. + setTimeout(function () { + if (!app.$data.executionStarted) { + $('#myModal').modal('show'); + } + }, 250); } // }); }); @@ -626,8 +636,13 @@ }; ws.onmessage = function (message) { message = JSON.parse(message.data); - app.$data.config_folder = message.config_folder.replaceAll("\\", "/"); - app.$data.easyspider_location = message.easyspider_location.replace("/EasySpider.app/", ""); + if (message.config_folder) { + app.$data.config_folder = message.config_folder.replaceAll("\\", "/"); + } + if (message.easyspider_location) { + app.$data.easyspider_location = message.easyspider_location.replace("/EasySpider.app/", ""); + } + app.$data.executionStarted = message.execution_started === true; } ws.onclose = function () { // 关闭 websocket diff --git a/ExecuteStage/easyspider_executestage.py b/ExecuteStage/easyspider_executestage.py index ff23b9a..3e38302 100644 --- a/ExecuteStage/easyspider_executestage.py +++ b/ExecuteStage/easyspider_executestage.py @@ -40,6 +40,7 @@ import re # import shutil import subprocess import sys +from pathlib import Path # from urllib import parse # import base64 # import hashlib @@ -73,6 +74,38 @@ desired_capabilities = DesiredCapabilities.CHROME desired_capabilities["pageLoadStrategy"] = "none" +def _runtime_directory(): + """Return the directory containing the running executor binary/script.""" + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return Path(__file__).resolve().parent + + +def _data_directory(): + """Resolve mutable task data independently from the app's cwd. + + Electron supplies EASYSPIDER_DATA_DIR when it starts the bundled macOS + executor. A manually launched portable executable keeps the historical + behaviour of writing beside the executable; a frozen macOS executable + launched directly falls back to Electron's userData location. + """ + configured = os.environ.get("EASYSPIDER_DATA_DIR", "").strip() + if configured: + return Path(configured).expanduser().resolve() + if sys.platform == "darwin" and getattr(sys, "frozen", False): + return Path(os.path.expanduser("~/Library/Application Support/EasySpider")).resolve() + return Path.cwd().resolve() + + +RUNTIME_DIRECTORY = _runtime_directory() +DATA_DIRECTORY = _data_directory() +DATA_DIRECTORY.mkdir(parents=True, exist_ok=True) +# The executor historically uses relative Data/, execution_instances/ and +# TempUserDataFolder paths throughout the file. Normalize cwd once so those +# paths remain valid even when macOS launches the app through App Translocation. +os.chdir(DATA_DIRECTORY) + + class BrowserThread(Thread): def __init__(self, browser_t, id, service, version, event, saveName, config, option, shutdown_event, commandline_config=""): Thread.__init__(self) @@ -133,8 +166,7 @@ class BrowserThread(Thread): "将从头开始执行,如果需要从上次退出的步骤开始执行,请在保存任务时设置是否从上次保存位置开始执行为“是”。") self.print_and_log("In this mode, task ID", self.id, "will start from the beginning, if you want to start from the last step, please set the option 'start from the last step' to 'yes' when saving the task.") - stealth_path = driver_path[:driver_path.find( - "chromedriver")] + "stealth.min.js" + stealth_path = os.path.join(os.path.dirname(driver_path), "stealth.min.js") with open(stealth_path, 'r') as f: js = f.read() self.print_and_log("Loading stealth.min.js") @@ -2325,8 +2357,31 @@ if __name__ == '__main__': if not os.path.exists(os.getcwd() + "/Data"): os.mkdir(os.getcwd() + "/Data") if sys.platform == "darwin" and platform.architecture()[0] == "64bit": - options.binary_location = "EasySpider.app/Contents/Resources/app/chrome_mac64.app/Contents/MacOS/Google Chrome" - driver_path = "EasySpider.app/Contents/Resources/app/chromedriver_mac64" + resource_override = os.environ.get("EASYSPIDER_APP_RESOURCES", "").strip() + resource_candidates = [] + if resource_override: + resource_candidates.append(Path(resource_override).expanduser().resolve()) + # Bundled executor: RUNTIME_DIRECTORY is Resources/app. + resource_candidates.append(RUNTIME_DIRECTORY) + # Portable release fallback: executor is next to EasySpider.app. + resource_candidates.append(RUNTIME_DIRECTORY / "EasySpider.app" / "Contents" / "Resources" / "app") + # Development fallback when running the Python entry point directly. + resource_candidates.append(RUNTIME_DIRECTORY.parent / "ElectronJS") + resource_root = next( + (candidate for candidate in resource_candidates + if (candidate / "chromedriver_mac64").exists() and + (candidate / "chrome_mac64.app").exists()), + None, + ) + if resource_root is None: + raise FileNotFoundError( + "Cannot locate the bundled macOS Chrome resources. " + "Checked: " + ", ".join(str(candidate) for candidate in resource_candidates) + ) + options.binary_location = str( + resource_root / "chrome_mac64.app" / "Contents" / "MacOS" / "Google Chrome" + ) + driver_path = str(resource_root / "chromedriver_mac64") print(driver_path) if c.config_folder == "": c.config_folder = os.path.expanduser( From fce434d9606cd1333e1c72b7ac4f1658c58799df Mon Sep 17 00:00:00 2001 From: Sora <1294058688@qq.com> Date: Mon, 24 Aug 2026 00:37:37 +0800 Subject: [PATCH 2/4] fix(macOS): stabilize command-line execution path --- ElectronJS/.gitignore | 1 + ElectronJS/main.js | 125 ++++++++++++++++++++++- ElectronJS/package_macos.sh | 1 + ElectronJS/src/taskGrid/executeTask.html | 88 ++++++++++++++-- 4 files changed, 203 insertions(+), 12 deletions(-) diff --git a/ElectronJS/.gitignore b/ElectronJS/.gitignore index f2d8a6f..de8e71e 100644 --- a/ElectronJS/.gitignore +++ b/ElectronJS/.gitignore @@ -16,6 +16,7 @@ execution_instances/* info.log npminstall-debug.log mysql_config.json +macos_command_folder.json EasySpider_en/ EasySpider_zh/ TempUserDataFolder/ diff --git a/ElectronJS/main.js b/ElectronJS/main.js index 7d226ac..73344b2 100644 --- a/ElectronJS/main.js +++ b/ElectronJS/main.js @@ -36,6 +36,64 @@ let config_context = JSON.parse( fs.readFileSync(path.join(task_server.getDir(), `config.json`), "utf8") ); //仅在当前进程中使用,不会写入文件 +const macOSCommandFolderConfigPath = path.join( + task_server.getDir(), + "macos_command_folder.json" +); + +function getMacOSCommandLauncher(folder) { + if (!folder) { + return ""; + } + const absoluteFolder = path.resolve(folder); + const appPath = path.join(absoluteFolder, "EasySpider.app"); + const scriptPath = path.join(absoluteFolder, "execute_macos.sh"); + const executablePath = path.join(absoluteFolder, "easyspider_executestage"); + if (!fs.existsSync(appPath) || !fs.existsSync(executablePath)) { + return ""; + } + // New packages include the wrapper so it can resolve its own directory. + // Retain compatibility with older portable releases that only contain + // the execution-stage binary. + return fs.existsSync(scriptPath) ? scriptPath : executablePath; +} + +function readSavedMacOSCommandFolder() { + try { + const saved = JSON.parse( + fs.readFileSync(macOSCommandFolderConfigPath, "utf8") + ); + return getMacOSCommandLauncher(saved.folder) ? path.resolve(saved.folder) : ""; + } catch { + return ""; + } +} + +function detectMacOSCommandFolder() { + const savedFolder = readSavedMacOSCommandFolder(); + if (savedFolder) { + return savedFolder; + } + if (process.platform !== "darwin" || !app.isPackaged) { + return ""; + } + // This resolves to the directory containing EasySpider.app during a + // normal launch. Under App Translocation it resolves to .../d, which + // does not contain the portable executor and therefore fails validation. + const appBundlePath = path.resolve(process.resourcesPath, "../.."); + const candidate = path.dirname(appBundlePath); + return getMacOSCommandLauncher(candidate) ? candidate : ""; +} + +function saveMacOSCommandFolder(folder) { + const absoluteFolder = path.resolve(folder); + fs.writeFileSync( + macOSCommandFolderConfigPath, + JSON.stringify({folder: absoluteFolder}, null, 2) + ); + return absoluteFolder; +} + if (config.debug) { let logPath = "info.log"; let logFile = fs.createWriteStream(logPath, {flags: "a"}); @@ -1126,7 +1184,19 @@ async function beginInvoke(msg, ws) { ws.send( JSON.stringify({ config_folder: task_server.getDir() + "/", - easyspider_location: task_server.getEasySpiderLocation(), + // Never expose an App Translocation directory as a runnable + // command-line location. macOS uses a validated portable + // release folder instead. + easyspider_location: + process.platform === "darwin" + ? detectMacOSCommandFolder() + : task_server.getEasySpiderLocation(), + macos_command_folder: + process.platform === "darwin" ? detectMacOSCommandFolder() : "", + macos_command_launcher: + process.platform === "darwin" + ? getMacOSCommandLauncher(detectMacOSCommandFolder()) + : "", execution_started, }) ); @@ -1155,6 +1225,59 @@ async function beginInvoke(msg, ws) { } catch { console.log("Cannot get Cookies"); } + } else if (msg.type == 8) { + if (process.platform !== "darwin") { + ws.send(JSON.stringify({ + response_type: "macos_command_folder", + canceled: true, + error: "The portable execution folder is only used on macOS.", + })); + return; + } + const currentFolder = detectMacOSCommandFolder(); + const parentWindow = + invoke_window && !invoke_window.isDestroyed() + ? invoke_window + : mainWindow; + const selection = await dialog.showOpenDialog(parentWindow, { + title: + language === "zh" + ? "选择 EasySpider_MacOS 发布文件夹" + : "Select the EasySpider_MacOS release folder", + defaultPath: currentFolder || app.getPath("downloads"), + properties: ["openDirectory"], + message: + language === "zh" + ? "请选择同时包含 EasySpider.app 和 easyspider_executestage 的文件夹" + : "Select the folder containing EasySpider.app and easyspider_executestage", + }); + if (selection.canceled || selection.filePaths.length === 0) { + ws.send(JSON.stringify({ + response_type: "macos_command_folder", + canceled: true, + })); + return; + } + const selectedFolder = path.resolve(selection.filePaths[0]); + const launcher = getMacOSCommandLauncher(selectedFolder); + if (!launcher) { + ws.send(JSON.stringify({ + response_type: "macos_command_folder", + canceled: false, + error: + language === "zh" + ? "所选文件夹必须同时包含 EasySpider.app 和 easyspider_executestage。" + : "The selected folder must contain both EasySpider.app and easyspider_executestage.", + })); + return; + } + saveMacOSCommandFolder(selectedFolder); + ws.send(JSON.stringify({ + response_type: "macos_command_folder", + canceled: false, + macos_command_folder: selectedFolder, + macos_command_launcher: launcher, + })); } else if (msg.type == 30) { send_message_to_browser( JSON.stringify({ diff --git a/ElectronJS/package_macos.sh b/ElectronJS/package_macos.sh index bf5b4c6..47044b0 100755 --- a/ElectronJS/package_macos.sh +++ b/ElectronJS/package_macos.sh @@ -50,6 +50,7 @@ rm -rf "$STAGE_DIR/Code" "$STAGE_DIR/Sample Tasks" "$STAGE_DIR/Data" \ rm -f "$STAGE_DIR/config.json" "$STAGE_DIR/mysql_config.json" mkdir -p "$STAGE_DIR/Code" "$STAGE_DIR/Sample Tasks" "$STAGE_DIR/Data" \ "$STAGE_DIR/execution_instances" +cp "$SCRIPT_DIR/execute_macos.sh" "$STAGE_DIR/execute_macos.sh" cp "$REPO_ROOT/ExecuteStage"/*.py "$STAGE_DIR/Code/" cp "$REPO_ROOT/ExecuteStage/requirements.txt" "$STAGE_DIR/Code/" cp "$REPO_ROOT/ExecuteStage/Readme.md" "$STAGE_DIR/Code/" diff --git a/ElectronJS/src/taskGrid/executeTask.html b/ElectronJS/src/taskGrid/executeTask.html index a6e66bb..c2fb605 100644 --- a/ElectronJS/src/taskGrid/executeTask.html +++ b/ElectronJS/src/taskGrid/executeTask.html @@ -92,8 +92,17 @@ - - +