mirror of
https://github.com/NaiboWang/EasySpider.git
synced 2026-10-30 18:22:29 +08:00
fix(macOS): avoid translocated execution paths
This commit is contained in:
+7
-1
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
必须授予文件访问权限,但根本不需要麦克风权限。作者也不清楚为什么会请求麦克风访问权限,所以可以拒绝。
|
||||
必须授予文件访问权限,但根本不需要麦克风权限。作者也不清楚为什么会请求麦克风访问权限,所以可以拒绝。
|
||||
|
||||
+31
-10
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
-17
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user