local-operator-ui 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/main/index.js +414 -30
- package/out/renderer/assets/{_basePickBy-MfDCo2Uh.js → _basePickBy-ykG0NPFf.js} +1 -1
- package/out/renderer/assets/{_baseUniq-Cpz-frgt.js → _baseUniq-zF370IN-.js} +1 -1
- package/out/renderer/assets/{agent-details-page-BvHXXiUb.js → agent-details-page-CFeQgmpk.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-XWqph1fJ.js → agent-hub-page-WTDOpgeF.js} +1 -1
- package/out/renderer/assets/{agents-page-BQKGRvX9.js → agents-page-BcpOVq5W.js} +2 -2
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CcAlBJ35.js → architectureDiagram-IEHRJDOE-BgE-rKS0.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-B0OCfiqo.js → blockDiagram-JOT3LUYC-fcZbaNLm.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-Doz1qDn4.js → c4Diagram-VJAJSXHY-DECLed3f.js} +1 -1
- package/out/renderer/assets/channel-CsUT-bgQ.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-CU2CNrGP.js → chunk-4BMEZGHF-CCYSb0Fh.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-CAMatFhx.js → chunk-A2AXSNBT-CjRtRikA.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-BZ-bO6ec.js → chunk-AEK57VVT-eVeRvEm8.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-CxhbOyZR.js → chunk-D6G4REZN-BN7tXfjV.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-CWa1WtXF.js → chunk-RZ5BOZE2-hZ_ZoDPs.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-CG0Minxk.js → chunk-XZIHB7SX-LDikEfMz.js} +1 -1
- package/out/renderer/assets/{classDiagram-GIVACNV2-QRAx18rR.js → classDiagram-GIVACNV2-8XRUpU40.js} +1 -1
- package/out/renderer/assets/{classDiagram-v2-COTLJTTW-QRAx18rR.js → classDiagram-v2-COTLJTTW-8XRUpU40.js} +1 -1
- package/out/renderer/assets/clone-BTzCuJ8z.js +1 -0
- package/out/renderer/assets/{compact-pagination-DID-3WmW.js → compact-pagination-DqiXMGmM.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-DqJ5M5Av.js → dagre-OKDRZEBW-f4DSNbNt.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-C3xzoBqy.js → diagram-SSKATNLV-DFy9Kjn5.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-DwzP1mk9.js → diagram-VNBRO52H-DxfeD-Xq.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-CAYEsR0C.js → erDiagram-Q7BY3M3F-BKIiiDiS.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-Br6uHcfK.js → flowDiagram-4HSFHLVR-bPQ65Vad.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-C85JWXKh.js → ganttDiagram-APWFNJXF-B4QAAaiA.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-Fj8dCS1a.js → gitGraphDiagram-7IBYFJ6S-CKw3yJdA.js} +1 -1
- package/out/renderer/assets/{graph-crAb0JBI.js → graph-BQE_YPzn.js} +1 -1
- package/out/renderer/assets/{index-DgfB0f4a.js → index-Byb6o6Kr.js} +282 -282
- package/out/renderer/assets/{index-CZfhJNkc.js → index-Cv1VxdCo.js} +1 -1
- package/out/renderer/assets/{index-DQsZQ882.js → index-lnppTSlu.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-BzOoP2K2.js → infoDiagram-PH2N3AL5-dZAW4pJY.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-rzeK8RKJ.js → journeyDiagram-U35MCT3I-Bfaldm3O.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-ngZfGkvv.js → kanban-definition-NDS4AKOZ-BihrMyrx.js} +1 -1
- package/out/renderer/assets/{layout-DoujDOcd.js → layout-BY0TgmPG.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-UwDNPEqi.js → legacy-agents-page-nTvYG2R1.js} +1 -1
- package/out/renderer/assets/{mermaid.core-CEj-gugH.js → mermaid.core-C_9cdmTh.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-qkNEqED3.js → mindmap-definition-ALO5MXBD-DGcjfOea.js} +1 -1
- package/out/renderer/assets/{parseISO-DFQYXDmJ.js → parseISO-Nf075j_I.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-BpHYrnjZ.js → pieDiagram-IB7DONF6-CLQQsy4S.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-jf2EIRrp.js → quadrantDiagram-7GDLP6J5-BW18K7e-.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-B_Zwcolm.js → radar-MK3ICKWK-D5qSNCES.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BzSb8qfY.js → radient-auth-buttons-C68jw6Rq.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-CDT8zCqY.js → requirementDiagram-KVF5MWMF-BGO78UpF.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CDyX6T2C.js → sankeyDiagram-QLVOVGJD-D10GCyqA.js} +1 -1
- package/out/renderer/assets/{schedules-page-BeC2UhOv.js → schedules-page-BhoAmrxA.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-IXPlMsm3.js → sequenceDiagram-X6HHIX6F-DJ0J92CL.js} +1 -1
- package/out/renderer/assets/{settings-page-Z2OLjBKF.js → settings-page-7W7M_FIE.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-DwnKmcwl.js → stateDiagram-DGXRK772-BRFGcjMT.js} +1 -1
- package/out/renderer/assets/{stateDiagram-v2-YXO3MK2T-DvubQewt.js → stateDiagram-v2-YXO3MK2T-BYuk7Mif.js} +1 -1
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-CjzEHtD7.js → timeline-definition-BDJGKUSR-V2afOmNy.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-DbSbfmyO.js → use-agent-like-mutation-CS4DoZ5N.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DTe8TwT9.js → xychartDiagram-VJFVF3MP-DSF1yRSG.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +4 -2
- package/out/renderer/assets/channel-dajMwelj.js +0 -1
- package/out/renderer/assets/clone-CNUTfmCA.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -1323,6 +1323,19 @@ function trustedDesktopFrame(actual, expected) {
|
|
|
1323
1323
|
return false;
|
|
1324
1324
|
}
|
|
1325
1325
|
}
|
|
1326
|
+
const PYTHON_BYTECODE_CACHE_DIR_NAME = "python-bytecode-cache";
|
|
1327
|
+
const PATH_SEPARATORS = /[\\/]/;
|
|
1328
|
+
function insideAppBundle(path2) {
|
|
1329
|
+
return path2.split(PATH_SEPARATORS).some((segment) => segment.length > 4 && segment.endsWith(".app"));
|
|
1330
|
+
}
|
|
1331
|
+
function pythonBytecodeCacheDir(userDataDir) {
|
|
1332
|
+
return path.join(userDataDir, PYTHON_BYTECODE_CACHE_DIR_NAME);
|
|
1333
|
+
}
|
|
1334
|
+
function withPythonBytecodeCache(env, userDataDir) {
|
|
1335
|
+
const existing = env.PYTHONPYCACHEPREFIX?.trim();
|
|
1336
|
+
if (existing && !insideAppBundle(existing)) return { ...env };
|
|
1337
|
+
return { ...env, PYTHONPYCACHEPREFIX: pythonBytecodeCacheDir(userDataDir) };
|
|
1338
|
+
}
|
|
1326
1339
|
var main = { exports: {} };
|
|
1327
1340
|
const version = "16.5.0";
|
|
1328
1341
|
const require$$4 = {
|
|
@@ -1989,6 +2002,7 @@ class BackendServiceManager {
|
|
|
1989
2002
|
`Shell environment variables loaded successfully. PATH: ${this.shellEnv.PATH || this.shellEnv.Path || "(not set)"}`,
|
|
1990
2003
|
LogFileType.BACKEND
|
|
1991
2004
|
);
|
|
2005
|
+
this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
|
|
1992
2006
|
} catch (error) {
|
|
1993
2007
|
logger.error(
|
|
1994
2008
|
"Error loading shell environment variables:",
|
|
@@ -1997,6 +2011,34 @@ class BackendServiceManager {
|
|
|
1997
2011
|
);
|
|
1998
2012
|
}
|
|
1999
2013
|
}
|
|
2014
|
+
/**
|
|
2015
|
+
* The environment the backend process is spawned with.
|
|
2016
|
+
*
|
|
2017
|
+
* The prefix is applied HERE, at the point the environment is handed to the
|
|
2018
|
+
* spawn, rather than only inside `loadShellEnvironment()`. That load is
|
|
2019
|
+
* started from the constructor without `await` (a constructor cannot wait)
|
|
2020
|
+
* and `start()` has nothing sequencing it, so a shell rc that takes seconds
|
|
2021
|
+
* to source - nvm, pyenv, conda init all do - leaves `shellEnv` at its
|
|
2022
|
+
* `{ ...process.env }` seed when the first spawn happens: no prefix, and the
|
|
2023
|
+
* exact pre-fix configuration, on the machines whose rc is slowest. Applying
|
|
2024
|
+
* it here makes the guarantee structural instead of ordering-dependent, so
|
|
2025
|
+
* no spawn path can miss it.
|
|
2026
|
+
*
|
|
2027
|
+
* These spawns are the ones whose `python` is the interpreter we ship:
|
|
2028
|
+
* CPython writes `__pycache__/*.pyc` beside the sources it imports, those
|
|
2029
|
+
* sources are inside the code-sealed `.app`, and every such write breaks the
|
|
2030
|
+
* signature ShipIt validates before an in-place update.
|
|
2031
|
+
*/
|
|
2032
|
+
backendSpawnEnv() {
|
|
2033
|
+
return {
|
|
2034
|
+
...withPythonBytecodeCache(this.shellEnv, this.appDataPath),
|
|
2035
|
+
// Managed starts rotate the token; set after the spread so the spawn
|
|
2036
|
+
// can never inherit a stale one from the shell environment. `start()`
|
|
2037
|
+
// mints it before it can reach a spawn site, so the `??` only satisfies
|
|
2038
|
+
// the field's nullable seed from `process.env`.
|
|
2039
|
+
LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken ?? void 0
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2000
2042
|
/**
|
|
2001
2043
|
* Load environment variables from macOS shell configuration files
|
|
2002
2044
|
*/
|
|
@@ -2315,10 +2357,7 @@ class BackendServiceManager {
|
|
|
2315
2357
|
detached: false,
|
|
2316
2358
|
// Ensure process is not detached from parent
|
|
2317
2359
|
stdio: "pipe",
|
|
2318
|
-
env:
|
|
2319
|
-
...this.shellEnv,
|
|
2320
|
-
LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken
|
|
2321
|
-
},
|
|
2360
|
+
env: this.backendSpawnEnv(),
|
|
2322
2361
|
// On Windows, we need to create a new process group to ensure proper termination
|
|
2323
2362
|
...process.platform === "win32" ? { windowsHide: true } : {}
|
|
2324
2363
|
});
|
|
@@ -2381,10 +2420,7 @@ class BackendServiceManager {
|
|
|
2381
2420
|
detached: false,
|
|
2382
2421
|
// Ensure process is not detached from parent
|
|
2383
2422
|
stdio: "pipe",
|
|
2384
|
-
env:
|
|
2385
|
-
...this.shellEnv,
|
|
2386
|
-
LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken
|
|
2387
|
-
},
|
|
2423
|
+
env: this.backendSpawnEnv(),
|
|
2388
2424
|
// On Windows, we need to create a new process group to ensure proper termination
|
|
2389
2425
|
...process.platform === "win32" ? { windowsHide: true } : {}
|
|
2390
2426
|
});
|
|
@@ -2818,7 +2854,7 @@ class BackendServiceManager {
|
|
|
2818
2854
|
return success;
|
|
2819
2855
|
}
|
|
2820
2856
|
}
|
|
2821
|
-
const linuxInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for Linux\n# This script sets up a virtual environment for the Local Operator backend\n# without requiring sudo or admin privileges.\n\n# Exit on error, undefined variables, and failed pipe commands\nset -e # Exit immediately if a command exits with a non-zero status\nset -u # Error on unset variables\nset -o pipefail # Fail if any command in a pipe fails\n\n# Configuration\n# Variables APP_NAME and MIN_PYTHON_VERSION removed as they were unused\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${HOME}/.config/local-operator"\nVENV_PATH="${APP_DATA_DIR}/${VENV_NAME}"\nLOG_FILE="${APP_DATA_DIR}/backend-install.log"\nTIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")\n\n# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n if [ -n "${FFMPEG_TEMP_ARCHIVE:-}" ] && [ -f "${FFMPEG_TEMP_ARCHIVE}" ]; then\n log "Removing temporary FFmpeg archive: ${FFMPEG_TEMP_ARCHIVE}"\n rm -f "${FFMPEG_TEMP_ARCHIVE}"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Function to log messages with timestamp (defined later, but used by FFmpeg section)\n_log_internal() {\n local internal_timestamp\n internal_timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${internal_timestamp}] $1"\n}\n\n# --- FFmpeg Installation ---\nBIN_DIR="${APP_DATA_DIR}/bin"\nFFMPEG_BIN="${BIN_DIR}/ffmpeg" # Path to ffmpeg binary\n\n_log_internal "Ensuring bin directory exists: ${BIN_DIR}"\nif ! mkdir -p "${BIN_DIR}"; then\n _log_internal "ERROR: Unable to create bin directory at ${BIN_DIR}"\n exit 1\nfi\n\n# Check if FFmpeg is already installed and executable\nif [ -f "${FFMPEG_BIN}" ] && [ -x "${FFMPEG_BIN}" ]; then\n _log_internal "FFmpeg already installed at ${FFMPEG_BIN}. Skipping download."\nelse\n _log_internal "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n FFMPEG_DOWNLOAD_URL=""\n\n LINUX_ARCH=$(uname -m)\n if [[ "${LINUX_ARCH}" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64"\n elif [[ "${LINUX_ARCH}" == "aarch64" ]] || [[ "${LINUX_ARCH}" == "arm64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64"\n else\n _log_internal "Error: Unsupported CPU architecture for FFmpeg download: ${LINUX_ARCH}"\n exit 1\n fi\n\n _log_internal "Downloading FFmpeg from: ${FFMPEG_DOWNLOAD_URL}"\n if command -v curl >/dev/null 2>&1; then\n if ! curl -L "${FFMPEG_DOWNLOAD_URL}" -o "${FFMPEG_BIN}"; then\n _log_internal "Error: Failed to download FFmpeg using curl from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n elif command -v wget >/dev/null 2>&1; then\n if ! wget -O "${FFMPEG_BIN}" "${FFMPEG_DOWNLOAD_URL}"; then\n _log_internal "Error: Failed to download FFmpeg using wget from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n else\n _log_internal "Error: Neither curl nor wget found. Cannot download FFmpeg."\n exit 1\n fi\n _log_internal "FFmpeg binary downloaded successfully to ${FFMPEG_BIN}"\n\n chmod +x "${FFMPEG_BIN}"\n _log_internal "Set executable permissions for ${FFMPEG_BIN}"\n\n # Verify FFmpeg is executable after download\n if [ ! -f "${FFMPEG_BIN}" ] || [ ! -x "${FFMPEG_BIN}" ]; then\n _log_internal "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\n_log_internal "FFmpeg installation complete. FFmpeg binary is at: ${FFMPEG_BIN}"\n\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --connect-timeout 5 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=5 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n}\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n curl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nelif command_exists wget; then\n wget -q --spider https://pypi.org/pypi/local-operator/json || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nfi\n\necho "Installing local-operator package..."\npython -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
|
|
2857
|
+
const linuxInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for Linux\n# This script sets up a virtual environment for the Local Operator backend\n# without requiring sudo or admin privileges.\n\n# Exit on error, undefined variables, and failed pipe commands\nset -e # Exit immediately if a command exits with a non-zero status\nset -u # Error on unset variables\nset -o pipefail # Fail if any command in a pipe fails\n\n# Configuration\n# Variables APP_NAME and MIN_PYTHON_VERSION removed as they were unused\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${HOME}/.config/local-operator"\nVENV_PATH="${APP_DATA_DIR}/${VENV_NAME}"\nLOG_FILE="${APP_DATA_DIR}/backend-install.log"\nTIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")\n\n# Keep CPython\'s bytecode cache out of the application directory.\n#\n# Same reason as the macOS script: an interpreter writing __pycache__/*.pyc\n# beside its own stdlib sources writes into the installed tree. Linux does not\n# code-seal the app the way macOS does, so this is uniformity and hygiene here\n# rather than the load-bearing fix it is on macOS - but it is also the value a\n# standalone run of this script has to agree with, because the app and the\n# script must not disagree about where bytecode goes.\n: "${PYTHONPYCACHEPREFIX:=${APP_DATA_DIR}/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n if [ -n "${FFMPEG_TEMP_ARCHIVE:-}" ] && [ -f "${FFMPEG_TEMP_ARCHIVE}" ]; then\n log "Removing temporary FFmpeg archive: ${FFMPEG_TEMP_ARCHIVE}"\n rm -f "${FFMPEG_TEMP_ARCHIVE}"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Function to log messages with timestamp (defined later, but used by FFmpeg section)\n_log_internal() {\n local internal_timestamp\n internal_timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${internal_timestamp}] $1"\n}\n\n# --- FFmpeg Installation ---\nBIN_DIR="${APP_DATA_DIR}/bin"\nFFMPEG_BIN="${BIN_DIR}/ffmpeg" # Path to ffmpeg binary\n\n_log_internal "Ensuring bin directory exists: ${BIN_DIR}"\nif ! mkdir -p "${BIN_DIR}"; then\n _log_internal "ERROR: Unable to create bin directory at ${BIN_DIR}"\n exit 1\nfi\n\n# Check if FFmpeg is already installed and executable\nif [ -f "${FFMPEG_BIN}" ] && [ -x "${FFMPEG_BIN}" ]; then\n _log_internal "FFmpeg already installed at ${FFMPEG_BIN}. Skipping download."\nelse\n _log_internal "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n FFMPEG_DOWNLOAD_URL=""\n\n LINUX_ARCH=$(uname -m)\n if [[ "${LINUX_ARCH}" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64"\n elif [[ "${LINUX_ARCH}" == "aarch64" ]] || [[ "${LINUX_ARCH}" == "arm64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64"\n else\n _log_internal "Error: Unsupported CPU architecture for FFmpeg download: ${LINUX_ARCH}"\n exit 1\n fi\n\n _log_internal "Downloading FFmpeg from: ${FFMPEG_DOWNLOAD_URL}"\n if command -v curl >/dev/null 2>&1; then\n if ! curl -L "${FFMPEG_DOWNLOAD_URL}" -o "${FFMPEG_BIN}"; then\n _log_internal "Error: Failed to download FFmpeg using curl from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n elif command -v wget >/dev/null 2>&1; then\n if ! wget -O "${FFMPEG_BIN}" "${FFMPEG_DOWNLOAD_URL}"; then\n _log_internal "Error: Failed to download FFmpeg using wget from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n else\n _log_internal "Error: Neither curl nor wget found. Cannot download FFmpeg."\n exit 1\n fi\n _log_internal "FFmpeg binary downloaded successfully to ${FFMPEG_BIN}"\n\n chmod +x "${FFMPEG_BIN}"\n _log_internal "Set executable permissions for ${FFMPEG_BIN}"\n\n # Verify FFmpeg is executable after download\n if [ ! -f "${FFMPEG_BIN}" ] || [ ! -x "${FFMPEG_BIN}" ]; then\n _log_internal "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\n_log_internal "FFmpeg installation complete. FFmpeg binary is at: ${FFMPEG_BIN}"\n\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --connect-timeout 5 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=5 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n}\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n curl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nelif command_exists wget; then\n wget -q --spider https://pypi.org/pypi/local-operator/json || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nfi\n\necho "Installing local-operator package..."\npython -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
|
|
2822
2858
|
const macosInstallScriptRaw = `#!/bin/bash
|
|
2823
2859
|
# Local Operator Backend Installation Script for macOS
|
|
2824
2860
|
# This script uses the bundled standalone Python and sets up a virtual environment for the Local Operator backend.
|
|
@@ -2832,6 +2868,22 @@ APP_DATA_DIR="$HOME/Library/Application Support/$APP_NAME"
|
|
|
2832
2868
|
VENV_PATH="$APP_DATA_DIR/$VENV_NAME"
|
|
2833
2869
|
LOG_FILE="$APP_DATA_DIR/backend-install.log"
|
|
2834
2870
|
|
|
2871
|
+
# Keep CPython's bytecode cache out of the application bundle.
|
|
2872
|
+
#
|
|
2873
|
+
# The bundled interpreter writes __pycache__/*.pyc beside the stdlib sources it
|
|
2874
|
+
# imports, and those sources live inside the code-signed .app - which is the
|
|
2875
|
+
# build's own extraResource. Every such write is a change to a sealed resource:
|
|
2876
|
+
# measured on an installed 0.17.3, \`codesign --verify --deep\` reported 308
|
|
2877
|
+
# \`file added:\` violations, all of them .pyc, and ShipIt then refuses the
|
|
2878
|
+
# in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports
|
|
2879
|
+
# as *added* can be deleted and the seal comes back; a *modified* one cannot,
|
|
2880
|
+
# which is why nothing may ship a .pyc at all. The app sets this variable when
|
|
2881
|
+
# it spawns us; defaulting it here means a standalone run of this script cannot
|
|
2882
|
+
# pollute the bundle either. $HOME is the right place for the same reason the
|
|
2883
|
+
# venv lives there: it is never inside the thing that gets signed and swapped.
|
|
2884
|
+
: "\${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"
|
|
2885
|
+
export PYTHONPYCACHEPREFIX
|
|
2886
|
+
|
|
2835
2887
|
# Determine CPU Architecture and Python Directory Name
|
|
2836
2888
|
ARCH=$(uname -m)
|
|
2837
2889
|
PYTHON_DIR_NAME="python" # Default for x86_64
|
|
@@ -3102,6 +3154,19 @@ $VenvPath = "$AppDataDir\\\\$VenvName"
|
|
|
3102
3154
|
$LogFile = "$AppDataDir\\\\backend-install-shell.log"
|
|
3103
3155
|
$PyenvDir = "$env:USERPROFILE\\\\.pyenv"
|
|
3104
3156
|
|
|
3157
|
+
# Keep CPython's bytecode cache out of the application directory.
|
|
3158
|
+
#
|
|
3159
|
+
# Same rule as the macOS script, and stated here for the same reason: an
|
|
3160
|
+
# interpreter writes __pycache__/*.pyc beside the stdlib sources it imports. On
|
|
3161
|
+
# Windows those sources are the bundled python directory rather than a
|
|
3162
|
+
# code-signed bundle, so this is hygiene rather than the load-bearing fix it is
|
|
3163
|
+
# on macOS - but the app and the script must not disagree about where bytecode
|
|
3164
|
+
# goes. $AppDataDir is used for the same reason the venv is: it is never inside
|
|
3165
|
+
# the installed application tree.
|
|
3166
|
+
if (-not $env:PYTHONPYCACHEPREFIX) {
|
|
3167
|
+
$env:PYTHONPYCACHEPREFIX = "$AppDataDir\\\\python-bytecode-cache"
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3105
3170
|
# Create app data directory if it doesn't exist
|
|
3106
3171
|
if (-not (Test-Path $AppDataDir)) {
|
|
3107
3172
|
New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null
|
|
@@ -3672,7 +3737,7 @@ class BackendInstaller {
|
|
|
3672
3737
|
electron.app.once("before-quit", appQuitHandler);
|
|
3673
3738
|
const result = await new Promise((resolve) => {
|
|
3674
3739
|
try {
|
|
3675
|
-
const env = { ...process.env };
|
|
3740
|
+
const env = withPythonBytecodeCache({ ...process.env }, this.appDataPath);
|
|
3676
3741
|
env.ELECTRON_RESOURCE_PATH = this.resourcesPath;
|
|
3677
3742
|
logger.info(
|
|
3678
3743
|
`Setting ELECTRON_RESOURCE_PATH to ${this.resourcesPath}`,
|
|
@@ -3982,6 +4047,22 @@ function registerDesktopIPC(window, expectedUrl, request, streams, media, notifi
|
|
|
3982
4047
|
});
|
|
3983
4048
|
}
|
|
3984
4049
|
}
|
|
4050
|
+
function presentWindow(window, show) {
|
|
4051
|
+
if (show === "never") return;
|
|
4052
|
+
if (window.isMinimized()) window.restore();
|
|
4053
|
+
if (show === "inactive") window.showInactive();
|
|
4054
|
+
else window.show();
|
|
4055
|
+
}
|
|
4056
|
+
function raiseWindow(window, show) {
|
|
4057
|
+
if (show === "never") return;
|
|
4058
|
+
if (window.isMinimized()) window.restore();
|
|
4059
|
+
if (show === "inactive") {
|
|
4060
|
+
window.showInactive();
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
window.show();
|
|
4064
|
+
window.focus();
|
|
4065
|
+
}
|
|
3985
4066
|
const NOTIFY_TTL_MS = 10 * 60 * 1e3;
|
|
3986
4067
|
const MAX_DEDUPE_KEYS = 2048;
|
|
3987
4068
|
const CONTRACT_PROBE_ATTEMPTS = 3;
|
|
@@ -4000,9 +4081,10 @@ function gateBody(kind, gateTitle, gateDetail) {
|
|
|
4000
4081
|
return subject || GATE_BODIES[kind] || GATE_BODIES.approval;
|
|
4001
4082
|
}
|
|
4002
4083
|
class DesktopNotifier {
|
|
4003
|
-
constructor(window, request) {
|
|
4084
|
+
constructor(window, request, windowRaise = "focus") {
|
|
4004
4085
|
this.window = window;
|
|
4005
4086
|
this.request = request;
|
|
4087
|
+
this.windowRaise = windowRaise;
|
|
4006
4088
|
}
|
|
4007
4089
|
delivered = /* @__PURE__ */ new Map();
|
|
4008
4090
|
/** Last known frontend epoch per session, to key dedupe on owner epoch. */
|
|
@@ -4191,6 +4273,7 @@ class DesktopNotifier {
|
|
|
4191
4273
|
/** Called by the stream relay for every parsed frame it forwards. */
|
|
4192
4274
|
observe(sessionId2, frame) {
|
|
4193
4275
|
if (!this.canNotify) return;
|
|
4276
|
+
if (this.windowRaise === "never") return;
|
|
4194
4277
|
if (frame.type === "snapshot") {
|
|
4195
4278
|
this.epochs.set(sessionId2, frame.payload.frontend.epoch);
|
|
4196
4279
|
const gate = frame.payload.frontend.snapshot.pending_gate;
|
|
@@ -4414,9 +4497,7 @@ class DesktopNotifier {
|
|
|
4414
4497
|
notification.on("click", () => {
|
|
4415
4498
|
const target2 = this.window();
|
|
4416
4499
|
if (!target2) return;
|
|
4417
|
-
|
|
4418
|
-
target2.show();
|
|
4419
|
-
target2.focus();
|
|
4500
|
+
raiseWindow(target2, this.windowRaise);
|
|
4420
4501
|
target2.webContents.send("desktop-open-conversation", { sessionId: sessionId2 });
|
|
4421
4502
|
});
|
|
4422
4503
|
notification.show();
|
|
@@ -4472,6 +4553,111 @@ function installedBundleSealBlock(appBundlePath, detail, version2) {
|
|
|
4472
4553
|
detail: `${appBundlePath}: ${detail}`
|
|
4473
4554
|
};
|
|
4474
4555
|
}
|
|
4556
|
+
const VIOLATION_LINE = /^file (added|modified|missing):\s*(.+)$/;
|
|
4557
|
+
function parseSealViolations(output) {
|
|
4558
|
+
const violations = [];
|
|
4559
|
+
for (const rawLine of output.split("\n")) {
|
|
4560
|
+
const line = rawLine.trim();
|
|
4561
|
+
if (line.length === 0) continue;
|
|
4562
|
+
const match = VIOLATION_LINE.exec(line);
|
|
4563
|
+
if (match) {
|
|
4564
|
+
violations.push({
|
|
4565
|
+
kind: match[1],
|
|
4566
|
+
path: match[2].trim()
|
|
4567
|
+
});
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
4570
|
+
violations.push({ kind: "other", path: line });
|
|
4571
|
+
}
|
|
4572
|
+
return violations;
|
|
4573
|
+
}
|
|
4574
|
+
function canonicalPath(path$1) {
|
|
4575
|
+
try {
|
|
4576
|
+
return fs.realpathSync(path$1);
|
|
4577
|
+
} catch {
|
|
4578
|
+
try {
|
|
4579
|
+
return path.join(fs.realpathSync(path.dirname(path$1)), path.basename(path$1));
|
|
4580
|
+
} catch {
|
|
4581
|
+
return path$1;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
const BUNDLED_PYTHON_DIRS = ["python", "python_aarch64"];
|
|
4586
|
+
function isPythonBytecodePath(bundlePath, candidate) {
|
|
4587
|
+
const bundle = canonicalPath(bundlePath);
|
|
4588
|
+
const path$1 = canonicalPath(candidate);
|
|
4589
|
+
const dirs = BUNDLED_PYTHON_DIRS.map(
|
|
4590
|
+
(name) => path.join(bundle, "Contents", "Resources", name)
|
|
4591
|
+
);
|
|
4592
|
+
const separator = "/";
|
|
4593
|
+
if (!path$1.endsWith(".pyc")) return false;
|
|
4594
|
+
const segments = path$1.split(separator);
|
|
4595
|
+
if (!segments.includes("__pycache__")) return false;
|
|
4596
|
+
return dirs.some((dir) => path$1.startsWith(`${dir}${separator}`));
|
|
4597
|
+
}
|
|
4598
|
+
function planPythonBytecodeHeal(bundlePath, violations) {
|
|
4599
|
+
const added = violations.filter((violation) => violation.kind === "added");
|
|
4600
|
+
const elsewhere = violations.filter(
|
|
4601
|
+
(violation) => violation.kind !== "added"
|
|
4602
|
+
);
|
|
4603
|
+
if (added.length === 0) {
|
|
4604
|
+
return {
|
|
4605
|
+
healable: false,
|
|
4606
|
+
reason: `no added-resource violation to heal (${violations.length} violation(s))`,
|
|
4607
|
+
paths: []
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
if (elsewhere.length > 0) {
|
|
4611
|
+
const kinds = [...new Set(elsewhere.map((violation) => violation.kind))];
|
|
4612
|
+
return {
|
|
4613
|
+
healable: false,
|
|
4614
|
+
reason: `unhealable violation class(es) present: ${kinds.join(", ")}`,
|
|
4615
|
+
paths: []
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
const outside = added.filter(
|
|
4619
|
+
(violation) => !isPythonBytecodePath(bundlePath, violation.path)
|
|
4620
|
+
);
|
|
4621
|
+
if (outside.length > 0) {
|
|
4622
|
+
return {
|
|
4623
|
+
healable: false,
|
|
4624
|
+
reason: `added resource outside the bundled python trees: ${outside[0].path}`,
|
|
4625
|
+
paths: []
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
return {
|
|
4629
|
+
healable: true,
|
|
4630
|
+
reason: `${added.length} bytecode file(s) added after signing`,
|
|
4631
|
+
paths: [...new Set(added.map((violation) => violation.path))]
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
function healPythonBytecode(bundlePath, violations, remove = (path2) => fs.rmSync(path2, { force: true })) {
|
|
4635
|
+
const plan = planPythonBytecodeHeal(bundlePath, violations);
|
|
4636
|
+
if (!plan.healable) return { ...plan, removed: [] };
|
|
4637
|
+
const removed = [];
|
|
4638
|
+
for (const path2 of plan.paths) {
|
|
4639
|
+
if (!isPythonBytecodePath(bundlePath, path2)) {
|
|
4640
|
+
return {
|
|
4641
|
+
healable: false,
|
|
4642
|
+
reason: `refused to remove ${path2}: not bytecode under the bundled python trees`,
|
|
4643
|
+
paths: plan.paths,
|
|
4644
|
+
removed
|
|
4645
|
+
};
|
|
4646
|
+
}
|
|
4647
|
+
try {
|
|
4648
|
+
remove(path2);
|
|
4649
|
+
removed.push(path2);
|
|
4650
|
+
} catch (error) {
|
|
4651
|
+
return {
|
|
4652
|
+
healable: false,
|
|
4653
|
+
reason: `could not remove ${path2}: ${error instanceof Error ? error.message : String(error)}`,
|
|
4654
|
+
paths: plan.paths,
|
|
4655
|
+
removed
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
return { ...plan, removed };
|
|
4660
|
+
}
|
|
4475
4661
|
function metadataBasenames(filePath, updatePath) {
|
|
4476
4662
|
const names = [path.basename(filePath)];
|
|
4477
4663
|
if (updatePath) names.push(path.basename(updatePath));
|
|
@@ -5318,7 +5504,11 @@ function runCommand(command, args, options = {}) {
|
|
|
5318
5504
|
node_child_process.execFile(
|
|
5319
5505
|
command,
|
|
5320
5506
|
args,
|
|
5321
|
-
{
|
|
5507
|
+
{
|
|
5508
|
+
timeout: options.timeoutMs ?? 2e4,
|
|
5509
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
5510
|
+
...options.env ? { env: options.env } : {}
|
|
5511
|
+
},
|
|
5322
5512
|
(error, stdout, stderr) => {
|
|
5323
5513
|
const code = error?.code;
|
|
5324
5514
|
const killed = error?.killed === true;
|
|
@@ -5457,6 +5647,20 @@ class UpdateService {
|
|
|
5457
5647
|
markerDir() {
|
|
5458
5648
|
return electron.app.getPath("userData");
|
|
5459
5649
|
}
|
|
5650
|
+
/**
|
|
5651
|
+
* The environment every python this service spawns has to run under.
|
|
5652
|
+
*
|
|
5653
|
+
* The bundled venv's `python` is the interpreter we ship, so its stdlib - and
|
|
5654
|
+
* therefore any `__pycache__` CPython writes for it - is inside the code-sealed
|
|
5655
|
+
* `.app`. `pip show` and the pip upgrade both compile stdlib modules, so both
|
|
5656
|
+
* would unseal the bundle the pre-flight is about to ask ShipIt to replace.
|
|
5657
|
+
*
|
|
5658
|
+
* userData is the location for the same reason the pending marker uses it: it
|
|
5659
|
+
* is never inside the bundle being swapped.
|
|
5660
|
+
*/
|
|
5661
|
+
pythonSpawnEnv() {
|
|
5662
|
+
return withPythonBytecodeCache(process.env, this.markerDir());
|
|
5663
|
+
}
|
|
5460
5664
|
sendToRenderer(channel, payload) {
|
|
5461
5665
|
const webContents = this.mainWindow?.webContents;
|
|
5462
5666
|
if (!webContents || webContents.isDestroyed()) return false;
|
|
@@ -5721,6 +5925,13 @@ class UpdateService {
|
|
|
5721
5925
|
* - A probe that could not run is retried once and then *allowed to proceed*.
|
|
5722
5926
|
* "We could not ask" is not "the bundle is bad", and treating it as one is
|
|
5723
5927
|
* how a transient hiccup became a permanent reinstall message (R5).
|
|
5928
|
+
*
|
|
5929
|
+
* A bundle that fails the check is given one chance to heal itself, because
|
|
5930
|
+
* the failure every 0.17.x/0.18.0 install has is ours rather than the user's:
|
|
5931
|
+
* a CPython stdlib bytecode cache written into the sealed bundle by our own
|
|
5932
|
+
* backend. Those writes are `file added:` violations and deleting exactly
|
|
5933
|
+
* those files restores the seal (measured; see `healPythonBytecode`), where a
|
|
5934
|
+
* bundle broken any other way still gets the reinstall refusal below.
|
|
5724
5935
|
*/
|
|
5725
5936
|
async probeInstalledBundleSeal(version2) {
|
|
5726
5937
|
if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
|
|
@@ -5737,13 +5948,15 @@ class UpdateService {
|
|
|
5737
5948
|
["--verify", "--deep", "--verbose=2", bundlePath],
|
|
5738
5949
|
{ timeoutMs: SEAL_PROBE_TIMEOUT_MS }
|
|
5739
5950
|
);
|
|
5740
|
-
let
|
|
5951
|
+
let probe = await probeBundle();
|
|
5952
|
+
let seal = evaluateBundleSeal(probe);
|
|
5741
5953
|
if (seal.kind === "unavailable") {
|
|
5742
5954
|
logger.warn(
|
|
5743
5955
|
`Seal check did not complete, retrying once: ${seal.detail}`,
|
|
5744
5956
|
LogFileType.UPDATE_SERVICE
|
|
5745
5957
|
);
|
|
5746
|
-
|
|
5958
|
+
probe = await probeBundle();
|
|
5959
|
+
seal = evaluateBundleSeal(probe);
|
|
5747
5960
|
}
|
|
5748
5961
|
if (seal.kind === "unavailable") {
|
|
5749
5962
|
logger.warn(
|
|
@@ -5763,7 +5976,33 @@ class UpdateService {
|
|
|
5763
5976
|
`Installed bundle failed its seal check: ${seal.detail}`,
|
|
5764
5977
|
LogFileType.UPDATE_SERVICE
|
|
5765
5978
|
);
|
|
5766
|
-
|
|
5979
|
+
const violations = parseSealViolations(probe.stdout);
|
|
5980
|
+
const heal = healPythonBytecode(bundlePath, violations);
|
|
5981
|
+
if (!heal.healable) {
|
|
5982
|
+
logger.warn(
|
|
5983
|
+
`Installed bundle is not healable in place: ${heal.reason}`,
|
|
5984
|
+
LogFileType.UPDATE_SERVICE
|
|
5985
|
+
);
|
|
5986
|
+
return installedBundleSealBlock(bundlePath, seal.detail, version2);
|
|
5987
|
+
}
|
|
5988
|
+
logger.info(
|
|
5989
|
+
`Removed ${heal.removed.length} bytecode file(s) our own interpreter had written into the sealed bundle: ${heal.reason}. Paths:
|
|
5990
|
+
${heal.removed.join("\n")}`,
|
|
5991
|
+
LogFileType.UPDATE_SERVICE
|
|
5992
|
+
);
|
|
5993
|
+
const healed = evaluateBundleSeal(await probeBundle());
|
|
5994
|
+
if (healed.kind === "sealed") {
|
|
5995
|
+
logger.info(
|
|
5996
|
+
`Installed bundle sealed again after removing ${heal.removed.length} added bytecode file(s); continuing with the install.`,
|
|
5997
|
+
LogFileType.UPDATE_SERVICE
|
|
5998
|
+
);
|
|
5999
|
+
return null;
|
|
6000
|
+
}
|
|
6001
|
+
logger.error(
|
|
6002
|
+
`Installed bundle still fails its seal check after healing: ${healed.detail}`,
|
|
6003
|
+
LogFileType.UPDATE_SERVICE
|
|
6004
|
+
);
|
|
6005
|
+
return installedBundleSealBlock(bundlePath, healed.detail, version2);
|
|
5767
6006
|
}
|
|
5768
6007
|
/** File names the updater's metadata knows this update by. */
|
|
5769
6008
|
stagedArtifactCandidates(info) {
|
|
@@ -6866,7 +7105,7 @@ class UpdateService {
|
|
|
6866
7105
|
const probe = await runCommand(
|
|
6867
7106
|
pythonPath,
|
|
6868
7107
|
["-m", "pip", "show", "local-operator"],
|
|
6869
|
-
{ timeoutMs: 12e4 }
|
|
7108
|
+
{ timeoutMs: 12e4, env: this.pythonSpawnEnv() }
|
|
6870
7109
|
);
|
|
6871
7110
|
if (probe.exitCode !== 0) {
|
|
6872
7111
|
logger.warn(
|
|
@@ -7030,7 +7269,8 @@ class UpdateService {
|
|
|
7030
7269
|
LogFileType.UPDATE_SERVICE
|
|
7031
7270
|
);
|
|
7032
7271
|
const pipRun = await runCommand(pip.command, pip.args, {
|
|
7033
|
-
timeoutMs: 15 * 60 * 1e3
|
|
7272
|
+
timeoutMs: 15 * 60 * 1e3,
|
|
7273
|
+
env: this.pythonSpawnEnv()
|
|
7034
7274
|
});
|
|
7035
7275
|
logger.info(
|
|
7036
7276
|
`pip stdout:
|
|
@@ -7298,6 +7538,105 @@ ${pipRun.stderr.trim()}`,
|
|
|
7298
7538
|
}
|
|
7299
7539
|
}
|
|
7300
7540
|
}
|
|
7541
|
+
const WINDOW_MODES = ["normal", "headless", "inactive"];
|
|
7542
|
+
const WINDOW_MODE_ENV = "LOCAL_OPERATOR_UI_WINDOW_MODE";
|
|
7543
|
+
const WINDOW_SIZE_ENV = "LOCAL_OPERATOR_UI_WINDOW_SIZE";
|
|
7544
|
+
const WINDOW_MODE_FLAG = "--window-mode";
|
|
7545
|
+
const WINDOW_SIZE_FLAG = "--window-size";
|
|
7546
|
+
const WINDOW_MIN_WIDTH = 800;
|
|
7547
|
+
const WINDOW_MIN_HEIGHT = 600;
|
|
7548
|
+
const WINDOW_MAX_EDGE = 16384;
|
|
7549
|
+
const DEFAULT_WINDOW_WIDTH = 1380;
|
|
7550
|
+
const DEFAULT_WINDOW_HEIGHT = 900;
|
|
7551
|
+
const WINDOW_BEHAVIOUR = {
|
|
7552
|
+
normal: { show: "focus", focusable: true, backgroundThrottling: true },
|
|
7553
|
+
inactive: { show: "inactive", focusable: true, backgroundThrottling: false },
|
|
7554
|
+
headless: { show: "never", focusable: false, backgroundThrottling: false }
|
|
7555
|
+
};
|
|
7556
|
+
function readFlag(argv, name) {
|
|
7557
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
7558
|
+
const argument = argv[index];
|
|
7559
|
+
if (argument === name) {
|
|
7560
|
+
const next = argv[index + 1];
|
|
7561
|
+
return { found: true, value: next?.startsWith("--") ? void 0 : next };
|
|
7562
|
+
}
|
|
7563
|
+
if (argument.startsWith(`${name}=`)) {
|
|
7564
|
+
return { found: true, value: argument.slice(name.length + 1) };
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
return { found: false, value: void 0 };
|
|
7568
|
+
}
|
|
7569
|
+
function parseWindowMode(value) {
|
|
7570
|
+
if (value === void 0) return null;
|
|
7571
|
+
const normalized = value.trim().toLowerCase();
|
|
7572
|
+
return WINDOW_MODES.includes(normalized) ? normalized : null;
|
|
7573
|
+
}
|
|
7574
|
+
const WINDOW_SIZE_PATTERN = /^(\d+)\s*[x×]\s*(\d+)$/i;
|
|
7575
|
+
function parseWindowSize(value) {
|
|
7576
|
+
if (value === void 0) return null;
|
|
7577
|
+
const match = value.trim().match(WINDOW_SIZE_PATTERN);
|
|
7578
|
+
if (!match) return null;
|
|
7579
|
+
const width = Number(match[1]);
|
|
7580
|
+
const height = Number(match[2]);
|
|
7581
|
+
if (width <= 0 || height <= 0) return null;
|
|
7582
|
+
return { width, height };
|
|
7583
|
+
}
|
|
7584
|
+
function resolveWindowLaunchPlan(input = {}) {
|
|
7585
|
+
const env = input.env ?? {};
|
|
7586
|
+
const argv = input.argv ?? [];
|
|
7587
|
+
const problems = [];
|
|
7588
|
+
const modeFlag = readFlag(argv, WINDOW_MODE_FLAG);
|
|
7589
|
+
const modeRaw = modeFlag.found ? modeFlag.value : env[WINDOW_MODE_ENV];
|
|
7590
|
+
const parsedMode = parseWindowMode(modeRaw);
|
|
7591
|
+
if (modeFlag.found && modeFlag.value === void 0) {
|
|
7592
|
+
problems.push(
|
|
7593
|
+
`${WINDOW_MODE_FLAG} needs a value: ${WINDOW_MODES.join("|")}`
|
|
7594
|
+
);
|
|
7595
|
+
} else if (parsedMode === null && modeRaw !== void 0) {
|
|
7596
|
+
problems.push(
|
|
7597
|
+
`${modeFlag.found ? WINDOW_MODE_FLAG : WINDOW_MODE_ENV}="${modeRaw}" is not one of ${WINDOW_MODES.join("|")}; using normal`
|
|
7598
|
+
);
|
|
7599
|
+
}
|
|
7600
|
+
const mode = parsedMode ?? "normal";
|
|
7601
|
+
const sizeFlag = readFlag(argv, WINDOW_SIZE_FLAG);
|
|
7602
|
+
const sizeRaw = sizeFlag.found ? sizeFlag.value : env[WINDOW_SIZE_ENV];
|
|
7603
|
+
const parsedSize = parseWindowSize(sizeRaw);
|
|
7604
|
+
if (sizeFlag.found && sizeFlag.value === void 0) {
|
|
7605
|
+
problems.push(`${WINDOW_SIZE_FLAG} needs a value: <width>x<height>`);
|
|
7606
|
+
} else if (parsedSize === null && sizeRaw !== void 0) {
|
|
7607
|
+
problems.push(
|
|
7608
|
+
`${sizeFlag.found ? WINDOW_SIZE_FLAG : WINDOW_SIZE_ENV}="${sizeRaw}" is not <width>x<height>; using ${DEFAULT_WINDOW_WIDTH}x${DEFAULT_WINDOW_HEIGHT}`
|
|
7609
|
+
);
|
|
7610
|
+
}
|
|
7611
|
+
const requested = parsedSize ?? {
|
|
7612
|
+
width: DEFAULT_WINDOW_WIDTH,
|
|
7613
|
+
height: DEFAULT_WINDOW_HEIGHT
|
|
7614
|
+
};
|
|
7615
|
+
const width = Math.min(
|
|
7616
|
+
Math.max(requested.width, WINDOW_MIN_WIDTH),
|
|
7617
|
+
WINDOW_MAX_EDGE
|
|
7618
|
+
);
|
|
7619
|
+
const height = Math.min(
|
|
7620
|
+
Math.max(requested.height, WINDOW_MIN_HEIGHT),
|
|
7621
|
+
WINDOW_MAX_EDGE
|
|
7622
|
+
);
|
|
7623
|
+
if (width !== requested.width || height !== requested.height) {
|
|
7624
|
+
problems.push(
|
|
7625
|
+
`window size ${requested.width}x${requested.height} clamped to ${width}x${height} (floor ${WINDOW_MIN_WIDTH}x${WINDOW_MIN_HEIGHT}, ceiling ${WINDOW_MAX_EDGE})`
|
|
7626
|
+
);
|
|
7627
|
+
}
|
|
7628
|
+
return {
|
|
7629
|
+
mode,
|
|
7630
|
+
...WINDOW_BEHAVIOUR[mode],
|
|
7631
|
+
width,
|
|
7632
|
+
height,
|
|
7633
|
+
problems
|
|
7634
|
+
};
|
|
7635
|
+
}
|
|
7636
|
+
function describeWindowLaunch(plan) {
|
|
7637
|
+
const behaviour = plan.show === "never" ? "window created and never shown" : plan.show === "inactive" ? "window shown without activating the app" : "window shown and focused";
|
|
7638
|
+
return `window mode ${plan.mode}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}`;
|
|
7639
|
+
}
|
|
7301
7640
|
const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
|
|
7302
7641
|
electron.app.setName("Local Operator");
|
|
7303
7642
|
const image = electron.nativeImage.createFromPath(icon);
|
|
@@ -7435,16 +7774,28 @@ function createApplicationMenu() {
|
|
|
7435
7774
|
}
|
|
7436
7775
|
function createWindow() {
|
|
7437
7776
|
const mainWindow2 = new electron.BrowserWindow({
|
|
7438
|
-
width:
|
|
7439
|
-
height:
|
|
7777
|
+
width: windowLaunch.width,
|
|
7778
|
+
height: windowLaunch.height,
|
|
7440
7779
|
// The layout is verified down to 800x600 and not below: the app rail,
|
|
7441
7780
|
// the per-route list pane and the canvas all have their own minimums,
|
|
7442
7781
|
// and past this point they start taking room from each other rather
|
|
7443
7782
|
// than from the window. The auth popup below sets its own floor the
|
|
7444
|
-
// same way.
|
|
7445
|
-
|
|
7446
|
-
|
|
7783
|
+
// same way. The floor lives in `window-mode.ts` because that is what
|
|
7784
|
+
// clamps a requested `--window-size` to it.
|
|
7785
|
+
minWidth: WINDOW_MIN_WIDTH,
|
|
7786
|
+
minHeight: WINDOW_MIN_HEIGHT,
|
|
7447
7787
|
show: false,
|
|
7788
|
+
/*
|
|
7789
|
+
* `headless` is never shown, so no key window can be made out of it; this
|
|
7790
|
+
* keeps that true of the window object itself rather than of the code
|
|
7791
|
+
* path. It is NOT a safety net against a stray `show()`: on macOS
|
|
7792
|
+
* `NativeWindowMac::Show()` activates the app for any non-panel window
|
|
7793
|
+
* whatever `focusable` says (measured — a non-focusable window, shown,
|
|
7794
|
+
* made the app frontmost while `isFocused()` still read false). The guard
|
|
7795
|
+
* against a window being raised is that `window-raise.ts` is the only
|
|
7796
|
+
* module that raises one, and this flag is the second line of defence.
|
|
7797
|
+
*/
|
|
7798
|
+
focusable: windowLaunch.focusable,
|
|
7448
7799
|
autoHideMenuBar: true,
|
|
7449
7800
|
title: "Local Operator",
|
|
7450
7801
|
icon,
|
|
@@ -7458,7 +7809,17 @@ function createWindow() {
|
|
|
7458
7809
|
nodeIntegration: false,
|
|
7459
7810
|
contextIsolation: true,
|
|
7460
7811
|
webSecurity: true,
|
|
7461
|
-
allowRunningInsecureContent: false
|
|
7812
|
+
allowRunningInsecureContent: false,
|
|
7813
|
+
// Chromium throttles timers and animation frames in a window it
|
|
7814
|
+
// believes nobody is looking at, which would make a headless test
|
|
7815
|
+
// run measure a different app than the one users get. Measured on
|
|
7816
|
+
// Electron 35 / macOS: a `show: false` window does keep
|
|
7817
|
+
// `visibilityState` at "visible" and keeps rAF ticking, but that
|
|
7818
|
+
// comes from Electron's MacWebContentsOcclusion default rather
|
|
7819
|
+
// than from anything this app controls, so hold the page at full
|
|
7820
|
+
// rate explicitly instead of trusting the platform. `normal`
|
|
7821
|
+
// keeps Electron's own default.
|
|
7822
|
+
backgroundThrottling: windowLaunch.backgroundThrottling
|
|
7462
7823
|
}
|
|
7463
7824
|
});
|
|
7464
7825
|
mainWindow2.webContents.on("console-message", (_event, level, message) => {
|
|
@@ -7467,7 +7828,16 @@ function createWindow() {
|
|
|
7467
7828
|
write.call(logger, `[renderer] ${message}`, LogFileType.BACKEND);
|
|
7468
7829
|
});
|
|
7469
7830
|
mainWindow2.on("ready-to-show", () => {
|
|
7470
|
-
mainWindow2.show
|
|
7831
|
+
presentWindow(mainWindow2, windowLaunch.show);
|
|
7832
|
+
if (windowLaunch.mode === "normal") return;
|
|
7833
|
+
setTimeout(() => {
|
|
7834
|
+
if (!mainWindow2 || mainWindow2.isDestroyed()) return;
|
|
7835
|
+
const [width, height] = mainWindow2.getSize();
|
|
7836
|
+
const [contentWidth, contentHeight] = mainWindow2.getContentSize();
|
|
7837
|
+
console.log(
|
|
7838
|
+
`[window-mode] state: visible=${mainWindow2.isVisible()} focused=${mainWindow2.isFocused()} focusable=${mainWindow2.isFocusable()} size=${width}x${height} content=${contentWidth}x${contentHeight}`
|
|
7839
|
+
);
|
|
7840
|
+
}, 1500);
|
|
7471
7841
|
});
|
|
7472
7842
|
mainWindow2.webContents.setWindowOpenHandler((details) => {
|
|
7473
7843
|
const url = new URL(details.url);
|
|
@@ -7538,6 +7908,17 @@ function createWindow() {
|
|
|
7538
7908
|
}
|
|
7539
7909
|
const backendService = new BackendServiceManager();
|
|
7540
7910
|
const backendInstaller = new BackendInstaller();
|
|
7911
|
+
const windowLaunch = resolveWindowLaunchPlan({
|
|
7912
|
+
env: process.env,
|
|
7913
|
+
argv: process.argv
|
|
7914
|
+
});
|
|
7915
|
+
for (const problem of windowLaunch.problems) {
|
|
7916
|
+
logger.warn(`[window-mode] ${problem}`, LogFileType.BACKEND);
|
|
7917
|
+
console.log(`[window-mode] ${problem}`);
|
|
7918
|
+
}
|
|
7919
|
+
if (windowLaunch.mode !== "normal") {
|
|
7920
|
+
console.log(`[window-mode] ${describeWindowLaunch(windowLaunch)}`);
|
|
7921
|
+
}
|
|
7541
7922
|
let mainWindow = null;
|
|
7542
7923
|
const zoomInFromEvent = () => {
|
|
7543
7924
|
if (mainWindow) {
|
|
@@ -7565,8 +7946,7 @@ if (!gotTheLock) {
|
|
|
7565
7946
|
} else {
|
|
7566
7947
|
electron.app.on("second-instance", (_event, commandLine) => {
|
|
7567
7948
|
if (mainWindow) {
|
|
7568
|
-
|
|
7569
|
-
mainWindow.focus();
|
|
7949
|
+
raiseWindow(mainWindow, windowLaunch.show);
|
|
7570
7950
|
}
|
|
7571
7951
|
});
|
|
7572
7952
|
}
|
|
@@ -7586,7 +7966,11 @@ electron.app.whenReady().then(async () => {
|
|
|
7586
7966
|
() => mainWindow,
|
|
7587
7967
|
(input) => backendService.requestDesktop(input)
|
|
7588
7968
|
);
|
|
7589
|
-
const desktopNotifier = new DesktopNotifier(
|
|
7969
|
+
const desktopNotifier = new DesktopNotifier(
|
|
7970
|
+
() => mainWindow,
|
|
7971
|
+
sendDesktop,
|
|
7972
|
+
windowLaunch.show
|
|
7973
|
+
);
|
|
7590
7974
|
backendService.onBackendReady(() => {
|
|
7591
7975
|
void desktopNotifier.refreshNotificationContract();
|
|
7592
7976
|
});
|