local-operator-ui 0.28.0 → 0.28.1
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 +524 -249
- package/out/renderer/assets/{_basePickBy-BOvo3PMo.js → _basePickBy-C02O_f_R.js} +1 -1
- package/out/renderer/assets/{_baseUniq-DM6bolHj.js → _baseUniq-CyWXoXbG.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DeUDUxv_.js → agent-details-page-0du1YTI_.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-FxmBdQhI.js → agent-hub-page-B7nVi1Jy.js} +1 -1
- package/out/renderer/assets/{agents-page-DYKw2vvx.js → agents-page-CYSotfFo.js} +2 -2
- package/out/renderer/assets/{arc-DpH8XBJW.js → arc-XTNYV0i5.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-ANhkj_oc.js → architectureDiagram-IEHRJDOE-DIL6Ll1r.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-CTB00Gt3.js → blockDiagram-JOT3LUYC-Ep_iozU3.js} +1 -1
- package/out/renderer/assets/{browser-page-D0RwIsvw.js → browser-page-DwWfYMRL.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-D2Hb_5e0.js → c4Diagram-VJAJSXHY-BjTSpvzv.js} +1 -1
- package/out/renderer/assets/channel-D9JdgjaE.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-Ccc3GaXX.js → chunk-4BMEZGHF-Bvj-NRkw.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-AjsU0EV0.js → chunk-A2AXSNBT-UUmKE301.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-CcKioR3D.js → chunk-AEK57VVT-NW7D31W_.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-BKiedEgU.js → chunk-D6G4REZN-DNqq3eoh.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-mHQEQ22d.js → chunk-RZ5BOZE2-Bga-d9GJ.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-ClLoX_HY.js → chunk-XZIHB7SX-BHx3SJQz.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2--M4DfyDu.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW--M4DfyDu.js +1 -0
- package/out/renderer/assets/clone-ApORSESi.js +1 -0
- package/out/renderer/assets/{compact-pagination-CPdPUvIX.js → compact-pagination-DNVRJTAX.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-Bl82QNX5.js → dagre-OKDRZEBW-KnrtYuYl.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-Dinlao46.js → diagram-SSKATNLV-VAbMy5h_.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-B7G1ezKY.js → diagram-VNBRO52H-i8oVPPp5.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-BRaUaIlI.js → erDiagram-Q7BY3M3F-Bkg4Xj6A.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-B1EbnkEl.js → flowDiagram-4HSFHLVR-D6YF1gP8.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-1k6P03ll.js → ganttDiagram-APWFNJXF-CV6sClgt.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-Dx-_n5Pt.js → gitGraphDiagram-7IBYFJ6S-OIj63WSM.js} +1 -1
- package/out/renderer/assets/{graph-Dp1vYuhs.js → graph-BdxNQDYo.js} +1 -1
- package/out/renderer/assets/{index-Cy6Q4C9i.js → index-5no0GDO0.js} +366 -366
- package/out/renderer/assets/{index-CGVkf3q5.js → index-B0qhIlIL.js} +1 -1
- package/out/renderer/assets/{index-DqnrrTlH.js → index-Ct9PNAEI.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-GxhMS_dh.js → infoDiagram-PH2N3AL5-D8oE75Dn.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-Co_F8-ZL.js → journeyDiagram-U35MCT3I-X-cfXqeY.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-wd1As6Op.js → kanban-definition-NDS4AKOZ-DKBkspnj.js} +1 -1
- package/out/renderer/assets/{layout-DkYEorlQ.js → layout-COR8ZIio.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-CAu6rhIe.js → legacy-agents-page-DWYOf5hQ.js} +1 -1
- package/out/renderer/assets/{mermaid.core-ApDCzSRO.js → mermaid.core-D-giE_vB.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BAA5mWWQ.js → mindmap-definition-ALO5MXBD-BH0jLzqf.js} +1 -1
- package/out/renderer/assets/{parseISO-BsCynSEy.js → parseISO-DL8GBZII.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-g3FOzz2y.js → pieDiagram-IB7DONF6-DW9M-Vtv.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-v4gWrVZb.js → quadrantDiagram-7GDLP6J5-CcbQfuRN.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-D2i-sp6E.js → radar-MK3ICKWK-Cb8VhXQ6.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-DcQ-N2DX.js → radient-auth-buttons-W3Ue6kiR.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-BM486FmB.js → requirementDiagram-KVF5MWMF-CSZX2Wo5.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CQzgAFhw.js → sankeyDiagram-QLVOVGJD-DOLbRwKf.js} +1 -1
- package/out/renderer/assets/{schedules-page-CFlqNgdV.js → schedules-page-BV8OqZDh.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-CbGnx919.js → sequenceDiagram-X6HHIX6F-DI4gtm_m.js} +1 -1
- package/out/renderer/assets/{settings-page-Cw6CMB-C.js → settings-page-Bh-7VIcH.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-BG6tAf1B.js → stateDiagram-DGXRK772-Dn0HYlb8.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-B1ZGpxnX.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-m44yI4Ta.js → timeline-definition-BDJGKUSR-BmVa1VwX.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-geCz91fs.js → use-agent-like-mutation-W2AIG7k0.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-coRt_1qL.js → xychartDiagram-VJFVF3MP-BRl6kaP4.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
- package/out/renderer/assets/channel-DIw4otfK.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-nMUSU4hp.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-nMUSU4hp.js +0 -1
- package/out/renderer/assets/clone-Sp7u6U6w.js +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-gZkDK_c7.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -5511,7 +5511,7 @@ function reapSupersededGenerations(options, keep, now = Date.now()) {
|
|
|
5511
5511
|
} catch {
|
|
5512
5512
|
continue;
|
|
5513
5513
|
}
|
|
5514
|
-
const survivors = new Set(kept
|
|
5514
|
+
const survivors = new Set(kept ?? []);
|
|
5515
5515
|
const generations = [];
|
|
5516
5516
|
for (const name of names) {
|
|
5517
5517
|
const path2 = path$1.join(root, name);
|
|
@@ -5673,40 +5673,73 @@ async function prepareManagedPython(options, install) {
|
|
|
5673
5673
|
const state = inspectManagedSelection(options);
|
|
5674
5674
|
if (state.kind === "ready") return state.selection;
|
|
5675
5675
|
const superseded = state.kind === "missing" ? state.detail : null;
|
|
5676
|
-
const published = state.kind === "missing" ? state.published :
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5676
|
+
const published = state.kind === "missing" ? state.published : null;
|
|
5677
|
+
return publishGeneration(options, install, published, superseded, false);
|
|
5678
|
+
});
|
|
5679
|
+
}
|
|
5680
|
+
async function publishGeneration(options, install, published, superseded, keepPublished) {
|
|
5681
|
+
const root = managedPythonRoot(options);
|
|
5682
|
+
reapSupersededGenerations(options, {
|
|
5683
|
+
runtime: published && (keepPublished || publishedRuntimeIsReusable(options, published)) ? [published.runtime] : [],
|
|
5684
|
+
venv: keepPublished && published ? [published.venv] : []
|
|
5685
|
+
});
|
|
5686
|
+
const { runtime, id: id2 } = await prepareRuntime(options);
|
|
5687
|
+
await promises.mkdir(environmentsRoot(options), { recursive: true, mode: 448 });
|
|
5688
|
+
realDirectory(environmentsRoot(options));
|
|
5689
|
+
const venv = path$1.join(environmentsRoot(options), `${id2}-${node_crypto.randomUUID()}`);
|
|
5690
|
+
if (!await install(venv, pythonPath(runtime)))
|
|
5691
|
+
throw new Error(
|
|
5692
|
+
"Backend preparation did not complete. Your previous environment and data were preserved."
|
|
5693
|
+
);
|
|
5694
|
+
const env = withPythonBytecodeCache({ ...process.env }, options.support);
|
|
5695
|
+
const backendVersion = await smokeEnvironment(venv, env);
|
|
5696
|
+
const selection = {
|
|
5697
|
+
format: FORMAT,
|
|
5698
|
+
runtimeId: id2,
|
|
5699
|
+
runtime,
|
|
5700
|
+
venv,
|
|
5701
|
+
backendVersion
|
|
5702
|
+
};
|
|
5703
|
+
const bytes = `${JSON.stringify(selection)}
|
|
5699
5704
|
`;
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5705
|
+
await promises.writeFile(path$1.join(venv, READY), bytes, { flag: "wx", mode: 384 });
|
|
5706
|
+
const temporary = path$1.join(root, `.selection-${node_crypto.randomUUID()}.json`);
|
|
5707
|
+
await promises.writeFile(temporary, bytes, { flag: "wx", mode: 384 });
|
|
5708
|
+
await promises.rename(temporary, path$1.join(root, POINTER));
|
|
5709
|
+
reapSupersededGenerations(options, {
|
|
5710
|
+
runtime: keepPublished && published ? [runtime, published.runtime] : [runtime],
|
|
5711
|
+
venv: keepPublished && published ? [venv, published.venv] : [venv]
|
|
5712
|
+
});
|
|
5713
|
+
if (superseded)
|
|
5714
|
+
console.warn(
|
|
5715
|
+
`Replaced an unusable managed Python selection: ${superseded}`
|
|
5716
|
+
);
|
|
5717
|
+
return selection;
|
|
5718
|
+
}
|
|
5719
|
+
function publishedBackendVersion(options) {
|
|
5720
|
+
return readManagedSelection(options)?.backendVersion ?? null;
|
|
5721
|
+
}
|
|
5722
|
+
function managedSelectionSatisfies(options, target2, isNewer) {
|
|
5723
|
+
if (!target2) return true;
|
|
5724
|
+
const published = publishedBackendVersion(options);
|
|
5725
|
+
if (published === null) return false;
|
|
5726
|
+
return !isNewer(target2, published);
|
|
5727
|
+
}
|
|
5728
|
+
async function updateManagedPython(options, install, decision) {
|
|
5729
|
+
return locked(managedPythonRoot(options), async () => {
|
|
5730
|
+
const state = inspectManagedSelection(options);
|
|
5731
|
+
const published = state.kind === "ready" ? state.selection : null;
|
|
5732
|
+
if (published && managedSelectionSatisfies(options, decision.target, decision.isNewer))
|
|
5733
|
+
return { selection: published, replaced: false, previous: null };
|
|
5734
|
+
const previous = published ?? (state.kind === "missing" ? state.published : null);
|
|
5735
|
+
const selection = await publishGeneration(
|
|
5736
|
+
options,
|
|
5737
|
+
install,
|
|
5738
|
+
previous,
|
|
5739
|
+
state.kind === "missing" ? state.detail : null,
|
|
5740
|
+
true
|
|
5741
|
+
);
|
|
5742
|
+
return { selection, replaced: true, previous };
|
|
5710
5743
|
});
|
|
5711
5744
|
}
|
|
5712
5745
|
function isLegacyManagedCommand(command, support) {
|
|
@@ -8308,6 +8341,19 @@ class BackendServiceManager {
|
|
|
8308
8341
|
return this.restartPromise;
|
|
8309
8342
|
}
|
|
8310
8343
|
}
|
|
8344
|
+
function managedPythonOptions() {
|
|
8345
|
+
return {
|
|
8346
|
+
support: path$1.join(
|
|
8347
|
+
electron.app.getPath("home"),
|
|
8348
|
+
"Library",
|
|
8349
|
+
"Application Support",
|
|
8350
|
+
"Local Operator"
|
|
8351
|
+
),
|
|
8352
|
+
resources: !electron.app.isPackaged ? path$1.join(process.cwd(), "resources") : path$1.join(process.resourcesPath),
|
|
8353
|
+
packaged: electron.app.isPackaged,
|
|
8354
|
+
arch: process.arch
|
|
8355
|
+
};
|
|
8356
|
+
}
|
|
8311
8357
|
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# Which environment this installs into - the app\'s decision, handed in. See the\n# macOS script for why the script must not re-derive it (a packaged and an\n# unpackaged instance use different environments, and only the app knows which\n# it is); `LOCAL_OPERATOR_VENV_PATH` is set from `managedVenvPath`.\n: "${LOCAL_OPERATOR_VENV_PATH:=$VENV_PATH}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\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# The refusal half of the pair, set rather than merged with whatever the caller\n# had: CPython reads the flag before its first import, so nothing this script\n# runs - `-m venv`, pip, the venv they create - can write a `__pycache__` at\n# all. Kept in step with the app\'s `withPythonBytecodeCache`, which sets both\n# variables on every python it spawns.\nexport PYTHONDONTWRITEBYTECODE=1\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';
|
|
8312
8358
|
const macosInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for macOS\n# This script uses the bundled standalone Python and sets up a virtual environment for the Local Operator backend.\n\nset -e # Exit immediately if a command exits with a non-zero status\n\n# Configuration\nAPP_NAME="Local Operator"\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${LOCAL_OPERATOR_SUPPORT_PATH:-$HOME/Library/Application Support/$APP_NAME}"\nVENV_PATH="$APP_DATA_DIR/$VENV_NAME"\nLOG_FILE="$APP_DATA_DIR/backend-install.log"\n\n# Which environment this installs into. The path is the app\'s decision, not this\n# script\'s: a packaged install and an unpackaged one must not share an\n# environment (the venv is built on whatever interpreter the instance resolves,\n# and the installed bundle\'s stdlib lives inside the code-sealed .app), and only\n# the app knows which one it is. The app passes its answer in\n# (`LOCAL_OPERATOR_VENV_PATH`, set from `managedVenvPath` in\n# src/main/backend/venv-paths.ts).\n#\n# macOS REFUSES a standalone run rather than falling back, and the asymmetry with\n# the Linux and Windows scripts is deliberate. Their default is the packaged name,\n# which is harmless there; here it is the exact environment this split exists to\n# stop a second instance from writing into - the measured failure is a dev-venv\n# interpreter whose stdlib is `/Applications/Local Operator.app/Contents/\n# Resources/python_aarch64`, so a silent default would rebuild that venv and\n# `pip install` into it, which is what R1 of the review caught. A caller that\n# cannot name the environment is a caller that should not be installing into one.\n: "${LOCAL_OPERATOR_VENV_PATH:?Pass the resolved managed environment path (see venv-paths.ts); this script will not guess which instance it belongs to}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\n\n# Keep CPython\'s bytecode cache out of the application bundle.\n#\n# The bundled interpreter writes __pycache__/*.pyc beside the stdlib sources it\n# imports, and those sources live inside the code-signed .app - which is the\n# build\'s own extraResource. Every such write is a change to a sealed resource:\n# measured on an installed 0.17.3, `codesign --verify --deep` reported 308\n# `file added:` violations, all of them .pyc, and ShipIt then refuses the\n# in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports\n# as *added* can be deleted and the seal comes back; a *modified* one cannot,\n# which is why nothing may ship a .pyc at all. The app sets this variable when\n# it spawns us; defaulting it here keeps a standalone run of this script in\n# agreement with the app about where bytecode goes. It is not a complete answer:\n# the app does not spawn every python that runs this interpreter, and a python\n# started by something else - a shell, a CLI script, an agent - has no prefix at\n# all (measured: a venv over this tree wrote 25 .pyc into it that way). The half\n# that covers those is the app\'s seal on the tree itself\n# (src/main/python-bytecode-cache.ts), which a standalone run of this script does\n# not apply. $HOME is the right place for the cache for the same reason the venv\n# lives there: it is never inside the thing that gets signed and swapped.\n: "${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# And the refusal half of the same pair, so a standalone run of this script\n# cannot write bytecode into the bundle even where the redirect above does not\n# apply - a relative or unwritable prefix, or a child that drops the variable.\n# CPython reads the flag before its first import, so this script\'s own\n# `python -m venv`, its pip runs and the venv they create all compile without\n# writing a `__pycache__` anywhere. Measured on CPython: `0` and the empty\n# string are the two falsy spellings, so the value is set rather than merged\n# with whatever the caller had.\nexport PYTHONDONTWRITEBYTECODE=1\n\n# The architecture block that used to live here computed and logged the name of\n# a directory for an in-bundle search this script no longer performs: every run\n# announced which architecture-named directory it was about to use, and then\n# installed from `PYTHON_BIN` anyway - the line stated the opposite of how the\n# script finds Python, and the names it computed were used nowhere else (review\n# N1). Which interpreter to build the environment with is the caller\'s decision,\n# checked immediately below.\n\n# The app prepares a complete external runtime before invoking this script.\n# Searching /Applications here would reintroduce legacy-bundle execution during\n# migration. Standalone callers must make the same explicit path decision.\n: "${PYTHON_BIN:?Pass an external prepared Python executable}"\ncase "$PYTHON_BIN" in\n *.app/*) echo "Refusing to execute Python inside an application bundle" >&2; exit 1 ;;\nesac\n\n# Create app data directory if it doesn\'t exist\nmkdir -p "$APP_DATA_DIR"\n\n# Start logging - capture everything and ensure it\'s visible to parent process\nexec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2)\necho "=============================================="\necho "$(date): Starting Local Operator backend installation..."\necho "=============================================="\necho "Python bin path: $PYTHON_BIN"\necho "Virtual environment path: $VENV_PATH"\necho "App data directory: $APP_DATA_DIR"\necho "Log file: $LOG_FILE"\necho "=============================================="\n\n# --- FFmpeg Installation ---\nBIN_DIR="$APP_DATA_DIR/bin"\nFFMPEG_BIN="$BIN_DIR/ffmpeg"\n\necho "Ensuring bin directory exists: $BIN_DIR"\nmkdir -p "$BIN_DIR"\n\n# Check if FFmpeg is already installed and executable\nif [ -f "$FFMPEG_BIN" ] && [ -x "$FFMPEG_BIN" ]; then\n echo "FFmpeg already installed at $FFMPEG_BIN. Skipping download."\nelse\n echo "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n # The architecture, read here because this URL is the one place left that needs\n # it: the environment is built on `PYTHON_BIN`, handed in above, so nothing\n # else in this script derives anything from `uname -m`. The block that used to\n # compute it at the top of the file was removed with the in-bundle interpreter\n # search it existed for (review N1) - and that removal took this variable with\n # it while this block still read it, so `$ARCH` was empty for every caller,\n # including the app, and any install on a machine without a cached ffmpeg\n # exited 1 here, before the venv was ever created. Measured: `bash\n # src/main/backend/scripts/macos-install-script.sh` with `PYTHON_BIN` set stops\n # on "Unsupported CPU architecture for FFmpeg download:".\n ARCH=$(uname -m)\n\n FFMPEG_DOWNLOAD_URL=""\n\n if [[ "$ARCH" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-mac-x64"\n elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-mac-arm64"\n else\n echo "Error: Unsupported CPU architecture for FFmpeg download: $ARCH"\n exit 1\n fi\n\n echo "Downloading FFmpeg from: $FFMPEG_DOWNLOAD_URL"\n if curl -L "$FFMPEG_DOWNLOAD_URL" -o "$FFMPEG_BIN"; then\n echo "FFmpeg downloaded successfully to $FFMPEG_BIN"\n chmod +x "$FFMPEG_BIN"\n echo "Set executable permissions for $FFMPEG_BIN"\n else\n echo "Error: Failed to download FFmpeg from $FFMPEG_DOWNLOAD_URL"\n exit 1\n fi\n\n # Verify FFmpeg is executable after download\n if [ ! -f "$FFMPEG_BIN" ] || [ ! -x "$FFMPEG_BIN" ]; then\n echo "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\necho "FFmpeg installation complete. FFmpeg binary is at: $FFMPEG_BIN"\n# --- End FFmpeg Installation ---\n\n# Verify bundled Python exists\nif [ ! -f "$PYTHON_BIN" ]; then\n echo "Error: Bundled Python not found at $PYTHON_BIN"\n echo "Please ensure standalone Python is properly installed in the application resources."\n exit 1\nfi\n\n# Make sure Python binary is executable\necho "Using bundled Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version\n\n# Check if Python has venv module available\necho "Checking for venv module availability..."\n"$PYTHON_BIN" -m venv --help > /dev/null 2>&1 || {\n echo "ERROR: Python venv module not available in the Python installation"\n echo "Python details:"\n "$PYTHON_BIN" --version\n "$PYTHON_BIN" -c "import sys; print(\'Prefix:\', sys.prefix); print(\'Exec Prefix:\', sys.exec_prefix)"\n "$PYTHON_BIN" -c "import sys; print(\'Modules path:\'); print(\'\\n\'.join(sys.path))"\n exit 1\n}\necho "venv module is available"\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Never repair a path we did not create. Preparation allocates a fresh final\n # pathname; a collision is evidence to preserve, not a reason to delete it.\n if [ -e "$VENV_PATH" ] || [ -L "$VENV_PATH" ]; then\n echo "Refusing to replace an existing environment path: $VENV_PATH" >&2\n exit 1\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Create the virtual environment with verbosity\n "$PYTHON_BIN" -m venv "$VENV_PATH" || {\n echo "ERROR: Failed to create virtual environment. Exit code: $?"\n echo "Virtual environment path: $VENV_PATH"\n echo "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")"\n echo "Python executable permissions:"\n ls -la "$PYTHON_BIN"\n exit 1\n }\n echo "Successfully created virtual environment"\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ] || [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Virtual environment is missing critical components"\n echo "Contents of virtual environment directory:"\n ls -la "$VENV_PATH"\n if [ -d "$VENV_PATH/bin" ]; then\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin"\n fi\n exit 1\nfi\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 "ERROR: Failed to upgrade pip. Exit code: $?"\n echo "pip version before failing:"\n pip --version\n exit 1\n}\necho "pip upgrade successful:"\npip --version\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\ncurl -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 echo "Attempting to ping common domains to diagnose network issues:"\n ping -c 1 google.com || echo "Cannot ping google.com"\n ping -c 1 pypi.org || echo "Cannot ping pypi.org"\n}\n\necho "Installing local-operator package..."\npython -m pip install --upgrade --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 echo "Pip config:"\n pip config list\n echo "Network diagnosis:"\n curl -I https://pypi.org || echo "Cannot reach PyPI server"\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n echo "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator"\n file "$VENV_PATH/bin/local-operator"\n \n # Try to run the version command with full error output\n echo "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n echo "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n echo "Binary details:"\n file "$VENV_PATH/bin/local-operator"\n echo "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator"\n echo "Dependencies:"\n if command -v otool >/dev/null; then\n otool -L "$VENV_PATH/bin/local-operator" || echo "Could not get dependencies with otool"\n fi\n exit 1\n }\nelse\n echo "Error: Failed to install Local Operator backend. Binary not found."\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/"\n exit 1\nfi\n\necho "$(date): Installation completed successfully."\n';
|
|
8313
8359
|
const windowsInstallScriptRaw = `# Local Operator Backend Installation Script for Windows
|
|
@@ -8670,9 +8716,13 @@ const SETUP_FAILURE_CAUSES = [
|
|
|
8670
8716
|
"A file the setup needed was missing, which usually means the download or the copy did not finish."
|
|
8671
8717
|
]
|
|
8672
8718
|
];
|
|
8719
|
+
function setupFailureCause(error) {
|
|
8720
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
8721
|
+
return SETUP_FAILURE_CAUSES.find(([pattern]) => pattern.test(raw))?.[1] ?? null;
|
|
8722
|
+
}
|
|
8673
8723
|
function backendSetupFailureDetail(error, support) {
|
|
8674
8724
|
const raw = error instanceof Error ? error.message : String(error);
|
|
8675
|
-
const cause =
|
|
8725
|
+
const cause = setupFailureCause(error) ?? "The backend could not be set up on this Mac.";
|
|
8676
8726
|
return [
|
|
8677
8727
|
`What happened: ${cause}`,
|
|
8678
8728
|
`The app recorded: ${raw}`,
|
|
@@ -8828,17 +8878,7 @@ class BackendInstaller {
|
|
|
8828
8878
|
* @returns Promise resolving to true if the backend was installed successfully, false otherwise
|
|
8829
8879
|
*/
|
|
8830
8880
|
managedOptions() {
|
|
8831
|
-
return
|
|
8832
|
-
support: path$1.join(
|
|
8833
|
-
electron.app.getPath("home"),
|
|
8834
|
-
"Library",
|
|
8835
|
-
"Application Support",
|
|
8836
|
-
"Local Operator"
|
|
8837
|
-
),
|
|
8838
|
-
resources: this.resourcesPath,
|
|
8839
|
-
packaged: electron.app.isPackaged,
|
|
8840
|
-
arch: process.arch
|
|
8841
|
-
};
|
|
8881
|
+
return managedPythonOptions();
|
|
8842
8882
|
}
|
|
8843
8883
|
async install() {
|
|
8844
8884
|
if (process.platform !== "darwin") return this.installEnvironment();
|
|
@@ -16520,6 +16560,7 @@ const BETA_VERSION_REGEX = /v\d+\.\d+\.\d+\.beta\.\d+/;
|
|
|
16520
16560
|
const BUNDLE_ID_REGEX = /^[\w.-]+$/;
|
|
16521
16561
|
const INSTALL_IN_FLIGHT_RECHECK_MS = 1e4;
|
|
16522
16562
|
const LAUNCHCTL_NOT_LOADED_REGEX = /could not find|no such process/i;
|
|
16563
|
+
const APP_OWNED_PUBLISH_HEADROOM_BYTES = 450 * 1024 * 1024;
|
|
16523
16564
|
const SEAL_PROBE_TIMEOUT_MS = 45e3;
|
|
16524
16565
|
const STARTUP_SEAL_PROBE_DELAY_MS = 15e3;
|
|
16525
16566
|
const POST_WAKE_CHECK_DELAY_MS = 2e4;
|
|
@@ -16559,6 +16600,9 @@ function runCommand(command, args, options = {}) {
|
|
|
16559
16600
|
);
|
|
16560
16601
|
});
|
|
16561
16602
|
}
|
|
16603
|
+
function installerOutputOf(run) {
|
|
16604
|
+
return [run.stderr.trim(), run.stdout.trim()].filter((part) => part.length > 0).join("\n").trim();
|
|
16605
|
+
}
|
|
16562
16606
|
function serverUpdateLogPath() {
|
|
16563
16607
|
return path$1.join(logger.getLogPath(), LogFileType.UPDATE_SERVICE);
|
|
16564
16608
|
}
|
|
@@ -18837,7 +18881,17 @@ ${heal.removed.join("\n")}`,
|
|
|
18837
18881
|
return {
|
|
18838
18882
|
canManageUpdate: true,
|
|
18839
18883
|
updateCommand: "pip install --upgrade local-operator",
|
|
18840
|
-
|
|
18884
|
+
/*
|
|
18885
|
+
* A REAL CONSEQUENCE, not the benefit sentence a second time (UX U2). This
|
|
18886
|
+
* string rendered through `managedCostSentence`, and the panel above it
|
|
18887
|
+
* already carries the static line about AI functionality, security and
|
|
18888
|
+
* bugs - so the offer showed the same filler twice, in two lengths, and the
|
|
18889
|
+
* one sentence that has to be here (what the press does to the server the
|
|
18890
|
+
* reader is talking to) was missing on the only arm whose press now
|
|
18891
|
+
* publishes a generation and restarts a daemon. It is the managed global
|
|
18892
|
+
* arm's own wording, aimed at this arm's mechanism.
|
|
18893
|
+
*/
|
|
18894
|
+
remedy: "The app publishes a new server environment and then restarts the server it started, so a turn that is in flight is dropped while the server comes back. This can take a minute or two.",
|
|
18841
18895
|
detail: `The app started this server itself (${startupMode}).`,
|
|
18842
18896
|
sourceBuild: false,
|
|
18843
18897
|
/*
|
|
@@ -18991,7 +19045,14 @@ ${heal.removed.join("\n")}`,
|
|
|
18991
19045
|
*/
|
|
18992
19046
|
eventRunning: this.eventRunningVersion(runningVersion),
|
|
18993
19047
|
silent,
|
|
18994
|
-
releaseRead: false
|
|
19048
|
+
releaseRead: false,
|
|
19049
|
+
/*
|
|
19050
|
+
* The ownership reading is local too, so the offline pass carries it as
|
|
19051
|
+
* well: the control this event may offer must be offered, or withheld,
|
|
19052
|
+
* on exactly the reading an online check uses. An offline machine whose
|
|
19053
|
+
* environment is the app's own still gets its restart.
|
|
19054
|
+
*/
|
|
19055
|
+
appOwnedEnvironment: this.appOwnsInstall(startupMode, serving)
|
|
18995
19056
|
});
|
|
18996
19057
|
return { status: "unavailable", info: null };
|
|
18997
19058
|
}
|
|
@@ -19111,6 +19172,16 @@ ${heal.removed.join("\n")}`,
|
|
|
19111
19172
|
* other two events already carry. It travels here too.
|
|
19112
19173
|
*/
|
|
19113
19174
|
restartable: this.backendIsAppOwned(),
|
|
19175
|
+
/*
|
|
19176
|
+
* AND WHOSE INSTALL A PRESS WOULD MOVE (design D5), which is the other
|
|
19177
|
+
* ownership reading: `restartable` is true here on a global install too,
|
|
19178
|
+
* where the app supervises a daemon it did not install. The mode alone
|
|
19179
|
+
* cannot answer it either - a GLOBAL_INSTALL launch can be serving from
|
|
19180
|
+
* the app's OWN managed environment, and since #318 landed that arm IS
|
|
19181
|
+
* routed to the publish path below - so the reading is the mode plus the
|
|
19182
|
+
* serving install's own root.
|
|
19183
|
+
*/
|
|
19184
|
+
appOwnedEnvironment: this.appOwnsInstall(startupMode, serving),
|
|
19114
19185
|
/* `silent` is the whole difference between the two callers: the periodic
|
|
19115
19186
|
and start-up checks pass `true`, the IPC handlers behind the buttons
|
|
19116
19187
|
pass `false`. The renderer needs to know which one it is answering,
|
|
@@ -19139,7 +19210,8 @@ ${heal.removed.join("\n")}`,
|
|
|
19139
19210
|
* affirmation above both turn on it - so the panel may call the install
|
|
19140
19211
|
* current. The offline caller passes false for exactly that reason.
|
|
19141
19212
|
*/
|
|
19142
|
-
releaseRead: true
|
|
19213
|
+
releaseRead: true,
|
|
19214
|
+
appOwnedEnvironment: this.appOwnsInstall(startupMode, serving)
|
|
19143
19215
|
});
|
|
19144
19216
|
return { status: channel.status, info: null };
|
|
19145
19217
|
} catch (error) {
|
|
@@ -19398,36 +19470,6 @@ ${heal.removed.join("\n")}`,
|
|
|
19398
19470
|
return false;
|
|
19399
19471
|
}
|
|
19400
19472
|
}
|
|
19401
|
-
/**
|
|
19402
|
-
* Bring the previous server back after a failed upgrade, and say whether it
|
|
19403
|
-
* actually came back.
|
|
19404
|
-
*
|
|
19405
|
-
* A failure here must not leave the user with no backend at all: whatever pip
|
|
19406
|
-
* did or did not do, the environment usually still holds a working install, so
|
|
19407
|
-
* start it again and then ask /health rather than assuming. The answer is not
|
|
19408
|
-
* cosmetic: `pip install --upgrade` uninstalls before it installs, so a
|
|
19409
|
-
* mid-install failure can leave the package absent, and telling the user "the
|
|
19410
|
-
* previous server is still running" when it is not is a claim we had no
|
|
19411
|
-
* evidence for (review R6).
|
|
19412
|
-
*/
|
|
19413
|
-
async restartBackendAfterFailedUpgrade() {
|
|
19414
|
-
try {
|
|
19415
|
-
await this.backendService?.start();
|
|
19416
|
-
const healthy = await this.checkBackendHealth();
|
|
19417
|
-
logger.info(
|
|
19418
|
-
healthy ? "Restarted the previous backend after a failed update" : "The backend did not answer its health check after a failed update",
|
|
19419
|
-
LogFileType.UPDATE_SERVICE
|
|
19420
|
-
);
|
|
19421
|
-
return healthy;
|
|
19422
|
-
} catch (error) {
|
|
19423
|
-
logger.error(
|
|
19424
|
-
"Could not restart the backend after a failed update:",
|
|
19425
|
-
LogFileType.UPDATE_SERVICE,
|
|
19426
|
-
error
|
|
19427
|
-
);
|
|
19428
|
-
return false;
|
|
19429
|
-
}
|
|
19430
|
-
}
|
|
19431
19473
|
/**
|
|
19432
19474
|
* Poll /health until it reports `targetVersion`, bounded by a timeout.
|
|
19433
19475
|
*
|
|
@@ -19573,6 +19615,14 @@ ${heal.removed.join("\n")}`,
|
|
|
19573
19615
|
* panel's own rule - "a reading that cannot be taken is silence" - applied to one
|
|
19574
19616
|
* clause of it). Every caller that did read the release, and every pre-existing
|
|
19575
19617
|
* reader of this event, sees the payload it always saw.
|
|
19618
|
+
*
|
|
19619
|
+
* AND WHOSE INSTALL A PRESS WOULD MOVE (design D5) travels with the pair, because
|
|
19620
|
+
* this event opens the one panel that ACTS: `restartable` says who started the
|
|
19621
|
+
* DAEMON, which is also true in GLOBAL_INSTALL mode, while the control the panel may
|
|
19622
|
+
* offer is a publish-and-restart the app performs only on its own environment. So
|
|
19623
|
+
* both ownership readings ride one payload, and BOTH callers pass both - the offline
|
|
19624
|
+
* pass measures this reading locally like the rest of the pair, so a machine that
|
|
19625
|
+
* reports no network is not the machine that loses its restart control.
|
|
19576
19626
|
*/
|
|
19577
19627
|
sendBackendSkewNotice(options) {
|
|
19578
19628
|
const { installedVersion, eventRunning, silent, releaseRead } = options;
|
|
@@ -19582,12 +19632,56 @@ ${heal.removed.join("\n")}`,
|
|
|
19582
19632
|
version: installedVersion,
|
|
19583
19633
|
runningVersion: eventRunning,
|
|
19584
19634
|
/** Whether the app may restart the daemon that is behind. */
|
|
19585
|
-
restartable: this.backendIsAppOwned()
|
|
19635
|
+
restartable: this.backendIsAppOwned(),
|
|
19636
|
+
/*
|
|
19637
|
+
* WHOSE INSTALL THE PANEL'S PRESS WOULD MOVE (design D5). `restartable`
|
|
19638
|
+
* above answers who started the DAEMON and is true on a global install too,
|
|
19639
|
+
* where the press behind that control runs the install's own updater rather
|
|
19640
|
+
* than the restart its label promises - so the control the renderer may offer
|
|
19641
|
+
* needs this second reading beside it. It travels on this event rather than in
|
|
19642
|
+
* a second send, because this is the event whose panel acts.
|
|
19643
|
+
*/
|
|
19644
|
+
appOwnedEnvironment: options.appOwnedEnvironment
|
|
19586
19645
|
};
|
|
19587
19646
|
if (!releaseRead) payload.releaseRead = false;
|
|
19588
19647
|
this.mainWindow.webContents.send("backend-update-not-available", payload);
|
|
19589
19648
|
}
|
|
19590
19649
|
}
|
|
19650
|
+
/**
|
|
19651
|
+
* Whether the INSTALL this app would move is the app's own managed environment.
|
|
19652
|
+
*
|
|
19653
|
+
* THE SECOND OWNERSHIP READING, and it is deliberately not `backendIsAppOwned`
|
|
19654
|
+
* (design D5). That one asks who started the DAEMON, and it answers yes in
|
|
19655
|
+
* `GLOBAL_INSTALL` mode as well: the app spawns and supervises a daemon whose
|
|
19656
|
+
* install is a uv tool / pipx / global one, so the app owns the process and not
|
|
19657
|
+
* the environment. The two questions diverge on exactly that machine, and the
|
|
19658
|
+
* skew panel's restart control is decided by this one, because `update-backend`
|
|
19659
|
+
* on a global install runs the install's own updater (an install, not a restart)
|
|
19660
|
+
* or lands on the by-hand panel - a control whose label promises a restart the
|
|
19661
|
+
* press does not perform.
|
|
19662
|
+
*
|
|
19663
|
+
* `APP_BUNDLED_VENV` is the app's own environment by construction and
|
|
19664
|
+
* `NOT_STARTED` has no daemon at all, so neither has anything to ask the serving
|
|
19665
|
+
* root about. `GLOBAL_INSTALL` and `EXISTING_SERVER` BOTH consult it rather than
|
|
19666
|
+
* being decided by the mode (review R8, which caught this paragraph saying the
|
|
19667
|
+
* opposite of the switch below): the mode answers who LAUNCHED the process, and
|
|
19668
|
+
* a process launched elsewhere can still read the app's managed tree - an
|
|
19669
|
+
* adopted daemon serving out of it is an install this app may move, and that was
|
|
19670
|
+
* the operator's own machine (design D5, #318). Both arms are stated rather than
|
|
19671
|
+
* left to a default so a new mode has to be classified here on purpose instead of
|
|
19672
|
+
* inheriting an answer.
|
|
19673
|
+
*/
|
|
19674
|
+
appOwnsInstall(startupMode, serving) {
|
|
19675
|
+
switch (startupMode) {
|
|
19676
|
+
case LocalOperatorStartupMode.APP_BUNDLED_VENV:
|
|
19677
|
+
return true;
|
|
19678
|
+
case LocalOperatorStartupMode.GLOBAL_INSTALL:
|
|
19679
|
+
case LocalOperatorStartupMode.EXISTING_SERVER:
|
|
19680
|
+
return serving?.appOwned === true;
|
|
19681
|
+
case LocalOperatorStartupMode.NOT_STARTED:
|
|
19682
|
+
return false;
|
|
19683
|
+
}
|
|
19684
|
+
}
|
|
19591
19685
|
/**
|
|
19592
19686
|
* The version the resolved global install reports on disk, or null.
|
|
19593
19687
|
*
|
|
@@ -19647,16 +19741,19 @@ ${heal.removed.join("\n")}`,
|
|
|
19647
19741
|
return run;
|
|
19648
19742
|
}
|
|
19649
19743
|
/**
|
|
19650
|
-
* True while an app-driven update
|
|
19744
|
+
* True while an app-driven server update is running, whichever install it moves.
|
|
19651
19745
|
*
|
|
19652
19746
|
* The guard is here and not on the surfaces that ask for the update, because
|
|
19653
19747
|
* their flags are per-surface: the offer holds `checking` and the run panel holds
|
|
19654
|
-
* `updatingBackend`, so a press on each of them reached
|
|
19655
|
-
*
|
|
19656
|
-
*
|
|
19657
|
-
*
|
|
19748
|
+
* `updatingBackend`, so a press on each of them reached an updater twice and the
|
|
19749
|
+
* second install ran beside the first against one tree (review R1-8).
|
|
19750
|
+
*
|
|
19751
|
+
* Both layouts need it now. The global path never stopped anything; the app-owned
|
|
19752
|
+
* path used to serialise itself by accident (it stopped the daemon first) and
|
|
19753
|
+
* stops nothing since it began publishing a generation instead of writing into
|
|
19754
|
+
* the live tree.
|
|
19658
19755
|
*/
|
|
19659
|
-
|
|
19756
|
+
serverUpdateInFlight = false;
|
|
19660
19757
|
/**
|
|
19661
19758
|
* Update the resolved global install, then restart the daemon this app owns.
|
|
19662
19759
|
*
|
|
@@ -19673,7 +19770,7 @@ ${heal.removed.join("\n")}`,
|
|
|
19673
19770
|
* exactly as the pip path does (`didUpgradeLand`).
|
|
19674
19771
|
*/
|
|
19675
19772
|
async updateGlobalInstall(backend, plan, target2) {
|
|
19676
|
-
if (this.
|
|
19773
|
+
if (this.serverUpdateInFlight) {
|
|
19677
19774
|
logger.warn(
|
|
19678
19775
|
"A global install update is already running; refusing to start a second beside it",
|
|
19679
19776
|
LogFileType.UPDATE_SERVICE
|
|
@@ -19685,11 +19782,11 @@ ${heal.removed.join("\n")}`,
|
|
|
19685
19782
|
});
|
|
19686
19783
|
return false;
|
|
19687
19784
|
}
|
|
19688
|
-
this.
|
|
19785
|
+
this.serverUpdateInFlight = true;
|
|
19689
19786
|
try {
|
|
19690
19787
|
return await this.runGlobalUpdateAttempt(backend, plan, target2);
|
|
19691
19788
|
} finally {
|
|
19692
|
-
this.
|
|
19789
|
+
this.serverUpdateInFlight = false;
|
|
19693
19790
|
}
|
|
19694
19791
|
}
|
|
19695
19792
|
/** The attempt itself, under the guard above. */
|
|
@@ -19745,7 +19842,13 @@ ${tail}` : ""}`,
|
|
|
19745
19842
|
installVersion: after,
|
|
19746
19843
|
runningVersion: running,
|
|
19747
19844
|
restarted: false,
|
|
19748
|
-
restartable: this.backendIsAppOwned()
|
|
19845
|
+
restartable: this.backendIsAppOwned(),
|
|
19846
|
+
/*
|
|
19847
|
+
* FALSE BY CONSTRUCTION, and stated rather than left absent: this arm is
|
|
19848
|
+
* the global install's own updater, so the environment it moved is never
|
|
19849
|
+
* the app's managed one (design D5).
|
|
19850
|
+
*/
|
|
19851
|
+
appOwnedEnvironment: false
|
|
19749
19852
|
});
|
|
19750
19853
|
return true;
|
|
19751
19854
|
}
|
|
@@ -19849,6 +19952,13 @@ ${tail}` : ""}`,
|
|
|
19849
19952
|
restarted: false,
|
|
19850
19953
|
unattended: true,
|
|
19851
19954
|
restartable: this.backendIsAppOwned(),
|
|
19955
|
+
/*
|
|
19956
|
+
* False, and it is also the honest answer to UX U3: the marker is written
|
|
19957
|
+
* on the global path only, and this reader compares it against
|
|
19958
|
+
* `readGlobalInstallVersion()` - the install the global updater moves -
|
|
19959
|
+
* so nothing here is a statement about the app's own environment.
|
|
19960
|
+
*/
|
|
19961
|
+
appOwnedEnvironment: false,
|
|
19852
19962
|
before: marker.before
|
|
19853
19963
|
});
|
|
19854
19964
|
}
|
|
@@ -19893,6 +20003,9 @@ ${tail}` : ""}`,
|
|
|
19893
20003
|
serving
|
|
19894
20004
|
);
|
|
19895
20005
|
const target2 = targetVersion ?? this.lastPublishedBackendVersion ?? null;
|
|
20006
|
+
if (serving.appOwned) {
|
|
20007
|
+
return await this.updateAppOwnedEnvironment(targetVersion ?? null);
|
|
20008
|
+
}
|
|
19896
20009
|
if (plan.canManageUpdate) {
|
|
19897
20010
|
return await this.updateGlobalInstall(
|
|
19898
20011
|
this.backendService,
|
|
@@ -19938,198 +20051,360 @@ ${tail}` : ""}`,
|
|
|
19938
20051
|
});
|
|
19939
20052
|
return false;
|
|
19940
20053
|
}
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
19954
|
-
|
|
19955
|
-
|
|
19956
|
-
|
|
19957
|
-
|
|
19958
|
-
|
|
20054
|
+
return await this.updateAppOwnedEnvironment(targetVersion ?? null);
|
|
20055
|
+
} catch (error) {
|
|
20056
|
+
logger.error(
|
|
20057
|
+
"Error updating backend:",
|
|
20058
|
+
LogFileType.UPDATE_SERVICE,
|
|
20059
|
+
error
|
|
20060
|
+
);
|
|
20061
|
+
if (this.backendService && !this.backendService.isUsingExternalBackend()) {
|
|
20062
|
+
try {
|
|
20063
|
+
logger.info(
|
|
20064
|
+
"Attempting to restart backend service after update failure...",
|
|
20065
|
+
LogFileType.UPDATE_SERVICE
|
|
20066
|
+
);
|
|
20067
|
+
await this.backendService.start();
|
|
20068
|
+
} catch (restartError) {
|
|
20069
|
+
logger.error(
|
|
20070
|
+
"Failed to restart backend service after update failure:",
|
|
20071
|
+
LogFileType.UPDATE_SERVICE,
|
|
20072
|
+
restartError
|
|
20073
|
+
);
|
|
20074
|
+
}
|
|
19959
20075
|
}
|
|
19960
|
-
if (!
|
|
19961
|
-
|
|
19962
|
-
//
|
|
19963
|
-
//
|
|
19964
|
-
|
|
19965
|
-
isUnpreparedVenvPath(venvPath ?? "") ? "Cannot update the bundled backend: no environment has been selected for this instance yet" : `Cannot update the bundled backend: no Python at ${pythonPath2 || "an unknown path"}`,
|
|
19966
|
-
LogFileType.UPDATE_SERVICE
|
|
19967
|
-
);
|
|
19968
|
-
await this.restartBackendAfterFailedUpgrade();
|
|
19969
|
-
this.sendToRenderer("backend-update-error", {
|
|
19970
|
-
message: "The bundled server's Python environment could not be found, so the update did not run. Please reinstall the application.",
|
|
20076
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.webContents && !this.mainWindow.webContents.isDestroyed()) {
|
|
20077
|
+
this.mainWindow.webContents.send("backend-update-error", {
|
|
20078
|
+
// Cleaned at the source, for the reason written on the check-phase
|
|
20079
|
+
// report above.
|
|
20080
|
+
message: stripErrorPrefixes(error.message),
|
|
19971
20081
|
phase: "update",
|
|
19972
20082
|
logPath: serverUpdateLogPath()
|
|
19973
20083
|
});
|
|
19974
|
-
return false;
|
|
19975
20084
|
}
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
20085
|
+
return false;
|
|
20086
|
+
}
|
|
20087
|
+
}
|
|
20088
|
+
/**
|
|
20089
|
+
* Move the app's OWN managed environment onto the release the app means to run.
|
|
20090
|
+
*
|
|
20091
|
+
* The shape, and its order is load-bearing (design section 5): land the new build
|
|
20092
|
+
* in a tree nothing is reading, tell the consumer the build moved, let it leave
|
|
20093
|
+
* at its own boundary. `updateManagedPython` builds a NEW generation beside the
|
|
20094
|
+
* published one, smokes it there, and only then flips `selected-environment.json`
|
|
20095
|
+
* atomically - so a daemon that is serving keeps reading the tree it loaded, and
|
|
20096
|
+
* no file it has open is ever written.
|
|
20097
|
+
*
|
|
20098
|
+
* The guard is the one the global path uses, for the reason that one's own
|
|
20099
|
+
* comment gives: the surfaces that ask for an update hold per-surface flags, so a
|
|
20100
|
+
* press on each reaches an updater twice. The old bundled path was serialised by
|
|
20101
|
+
* accident - it stopped the daemon first - and this one stops nothing, so it
|
|
20102
|
+
* needs the guard on purpose.
|
|
20103
|
+
*/
|
|
20104
|
+
async updateAppOwnedEnvironment(targetVersion) {
|
|
20105
|
+
if (this.serverUpdateInFlight) {
|
|
20106
|
+
logger.warn(
|
|
20107
|
+
"A server update is already running; refusing to start a second beside it",
|
|
19980
20108
|
LogFileType.UPDATE_SERVICE
|
|
19981
20109
|
);
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
20110
|
+
this.sendToRenderer("backend-update-error", {
|
|
20111
|
+
message: "A server update is already running. Let it finish before starting another.",
|
|
20112
|
+
phase: "update",
|
|
20113
|
+
logPath: serverUpdateLogPath()
|
|
19985
20114
|
});
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
20115
|
+
return false;
|
|
20116
|
+
}
|
|
20117
|
+
this.serverUpdateInFlight = true;
|
|
20118
|
+
try {
|
|
20119
|
+
return await this.runAppOwnedUpdateAttempt(targetVersion);
|
|
20120
|
+
} finally {
|
|
20121
|
+
this.serverUpdateInFlight = false;
|
|
20122
|
+
}
|
|
20123
|
+
}
|
|
20124
|
+
/** The attempt itself, under the guard above. */
|
|
20125
|
+
async runAppOwnedUpdateAttempt(targetVersion) {
|
|
20126
|
+
const backend = this.backendService;
|
|
20127
|
+
if (!backend) {
|
|
20128
|
+
logger.error(
|
|
20129
|
+
"An app-managed environment update was asked for with no backend service reference",
|
|
19989
20130
|
LogFileType.UPDATE_SERVICE
|
|
19990
20131
|
);
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20132
|
+
this.sendToRenderer("backend-update-error", {
|
|
20133
|
+
message: "The app has no server to update right now. Restart Local Operator and try again.",
|
|
20134
|
+
phase: "update",
|
|
20135
|
+
logPath: serverUpdateLogPath()
|
|
20136
|
+
});
|
|
20137
|
+
return false;
|
|
20138
|
+
}
|
|
20139
|
+
const options = managedPythonOptions();
|
|
20140
|
+
const target2 = targetVersion ?? this.lastPublishedBackendVersion ?? await this.getLatestPypiVersion() ?? null;
|
|
20141
|
+
if (!target2) {
|
|
20142
|
+
logger.error(
|
|
20143
|
+
"An app-managed environment update was asked for with no release to move to: no caller target, no published reading, and PyPI could not be read",
|
|
20144
|
+
LogFileType.UPDATE_SERVICE
|
|
20145
|
+
);
|
|
20146
|
+
this.sendToRenderer("backend-update-error", {
|
|
20147
|
+
message: "The app could not read which release to install, so it left its server environment alone. Check the connection and try again.",
|
|
20148
|
+
phase: "update",
|
|
20149
|
+
logPath: serverUpdateLogPath()
|
|
20003
20150
|
});
|
|
20004
|
-
|
|
20151
|
+
return false;
|
|
20152
|
+
}
|
|
20153
|
+
const before = publishedBackendVersion(options);
|
|
20154
|
+
const willPublish = before === null || this.isNewerVersion(target2, before);
|
|
20155
|
+
if (willPublish) {
|
|
20156
|
+
const free = this.freeBytesAt(managedPythonRoot(options));
|
|
20157
|
+
if (free !== null && free < APP_OWNED_PUBLISH_HEADROOM_BYTES) {
|
|
20158
|
+
const neededMb = Math.ceil(
|
|
20159
|
+
APP_OWNED_PUBLISH_HEADROOM_BYTES / 1024 / 1024
|
|
20160
|
+
);
|
|
20161
|
+
const freeMb = Math.floor(free / 1024 / 1024);
|
|
20005
20162
|
logger.error(
|
|
20006
|
-
`
|
|
20163
|
+
`Refusing the app-managed environment update: ${freeMb} MB free where ${neededMb} MB is needed`,
|
|
20007
20164
|
LogFileType.UPDATE_SERVICE
|
|
20008
20165
|
);
|
|
20009
|
-
const serverIsBack = await this.restartBackendAfterFailedUpgrade();
|
|
20010
20166
|
this.sendToRenderer("backend-update-error", {
|
|
20011
|
-
message:
|
|
20167
|
+
message: `The server update needs about ${neededMb} MB of free space on this disk, and ${freeMb} MB is free, so the app left the environment serving this app alone. Free some space and try again.`,
|
|
20012
20168
|
phase: "update",
|
|
20013
20169
|
logPath: serverUpdateLogPath()
|
|
20014
20170
|
});
|
|
20015
20171
|
return false;
|
|
20016
20172
|
}
|
|
20017
|
-
|
|
20018
|
-
|
|
20173
|
+
}
|
|
20174
|
+
logger.info(
|
|
20175
|
+
`Updating the app-managed environment (published ${before ?? "no reading"}, target ${target2}, publishes: ${willPublish})`,
|
|
20176
|
+
LogFileType.UPDATE_SERVICE
|
|
20177
|
+
);
|
|
20178
|
+
this.sendToRenderer("backend-update-progress", {
|
|
20179
|
+
phase: willPublish ? "installing" : "restarting"
|
|
20180
|
+
});
|
|
20181
|
+
let outcome;
|
|
20182
|
+
try {
|
|
20183
|
+
outcome = await updateManagedPython(
|
|
20184
|
+
options,
|
|
20185
|
+
(venv, python) => this.installEnvironmentInto(venv, python, target2),
|
|
20186
|
+
{
|
|
20187
|
+
target: target2,
|
|
20188
|
+
isNewer: (candidate, subject) => this.isNewerVersion(candidate, subject)
|
|
20189
|
+
}
|
|
20190
|
+
);
|
|
20191
|
+
} catch (error) {
|
|
20192
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
20193
|
+
const cause = setupFailureCause(error);
|
|
20194
|
+
logger.error(
|
|
20195
|
+
`The app-managed environment update published nothing: ${reason}`,
|
|
20019
20196
|
LogFileType.UPDATE_SERVICE
|
|
20020
20197
|
);
|
|
20198
|
+
this.sendToRenderer("backend-update-error", {
|
|
20199
|
+
message: `The server update did not install, so the environment serving this app was left as it was.${cause ? ` ${cause}` : ""}${reason ? `
|
|
20200
|
+
|
|
20201
|
+
${reason}` : ""}`,
|
|
20202
|
+
phase: "update",
|
|
20203
|
+
logPath: serverUpdateLogPath()
|
|
20204
|
+
});
|
|
20205
|
+
return false;
|
|
20206
|
+
}
|
|
20207
|
+
const installVersion = outcome.selection.backendVersion;
|
|
20208
|
+
logger.info(
|
|
20209
|
+
outcome.replaced ? `Published a new app-managed environment generation: ${before ?? "none"} -> ${installVersion}; the generation it supersedes stays on disk at ${outcome.previous?.venv ?? "no previous generation"}` : `The published app-managed environment already satisfies ${target2}; nothing was installed (published ${installVersion})`,
|
|
20210
|
+
LogFileType.UPDATE_SERVICE
|
|
20211
|
+
);
|
|
20212
|
+
const servingBeforeRestart = await this.getInstalledBackendVersion();
|
|
20213
|
+
const movedTheServer = outcome.replaced || servingBeforeRestart !== installVersion;
|
|
20214
|
+
if (!movedTheServer) {
|
|
20021
20215
|
logger.info(
|
|
20022
|
-
|
|
20216
|
+
`The published environment already satisfies ${target2} and the server serving this app already reports ${servingBeforeRestart}; nothing to move, so it was not restarted`,
|
|
20023
20217
|
LogFileType.UPDATE_SERVICE
|
|
20024
20218
|
);
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
);
|
|
20049
|
-
if (this.mainWindow) {
|
|
20050
|
-
this.mainWindow.webContents.send("backend-update-error", {
|
|
20051
|
-
message: "Backend was updated but failed to restart properly. Please restart the application.",
|
|
20052
|
-
phase: "update",
|
|
20053
|
-
logPath: serverUpdateLogPath()
|
|
20054
|
-
});
|
|
20055
|
-
}
|
|
20056
|
-
return false;
|
|
20057
|
-
}
|
|
20058
|
-
}
|
|
20059
|
-
} else {
|
|
20060
|
-
logger.error(
|
|
20061
|
-
"Failed to restart backend service after update",
|
|
20062
|
-
LogFileType.UPDATE_SERVICE
|
|
20063
|
-
);
|
|
20064
|
-
this.backendService.setAutoUpdating(false);
|
|
20065
|
-
if (this.mainWindow) {
|
|
20066
|
-
this.mainWindow.webContents.send("backend-update-error", {
|
|
20067
|
-
message: "Backend was updated but failed to restart. Please restart the application.",
|
|
20068
|
-
phase: "update",
|
|
20069
|
-
logPath: serverUpdateLogPath()
|
|
20070
|
-
});
|
|
20071
|
-
}
|
|
20072
|
-
return false;
|
|
20073
|
-
}
|
|
20074
|
-
const reportedVersion = await this.waitForBackendVersion(
|
|
20075
|
-
targetVersion ?? null
|
|
20076
|
-
);
|
|
20077
|
-
if (reportedVersion == null) {
|
|
20078
|
-
logger.error(
|
|
20079
|
-
`Backend did not report version ${targetVersion ?? "the new one"} after the update`,
|
|
20080
|
-
LogFileType.UPDATE_SERVICE
|
|
20081
|
-
);
|
|
20082
|
-
this.sendToRenderer("backend-update-error", {
|
|
20083
|
-
message: `The server restarted but did not report ${targetVersion ? `version ${targetVersion}` : "the updated version"}. Check the update service log, then try again.`,
|
|
20084
|
-
phase: "update",
|
|
20085
|
-
logPath: serverUpdateLogPath()
|
|
20086
|
-
});
|
|
20087
|
-
return false;
|
|
20088
|
-
}
|
|
20089
|
-
logger.info(
|
|
20090
|
-
`Backend reports version ${reportedVersion} after the update`,
|
|
20219
|
+
this.sendToRenderer("backend-update-completed", {
|
|
20220
|
+
installVersion,
|
|
20221
|
+
runningVersion: servingBeforeRestart,
|
|
20222
|
+
restarted: false,
|
|
20223
|
+
restartable: this.backendIsAppOwned(),
|
|
20224
|
+
appOwnedEnvironment: true
|
|
20225
|
+
});
|
|
20226
|
+
return true;
|
|
20227
|
+
}
|
|
20228
|
+
logger.info(
|
|
20229
|
+
"Restarting backend service onto the published environment...",
|
|
20230
|
+
LogFileType.UPDATE_SERVICE
|
|
20231
|
+
);
|
|
20232
|
+
if (willPublish) {
|
|
20233
|
+
this.sendToRenderer("backend-update-progress", { phase: "restarting" });
|
|
20234
|
+
}
|
|
20235
|
+
backend.setAutoUpdating(true);
|
|
20236
|
+
const restarted = await backend.restart();
|
|
20237
|
+
backend.setAutoUpdating(false);
|
|
20238
|
+
const healthy = restarted ? await this.checkBackendHealth() : false;
|
|
20239
|
+
if (!restarted || !healthy) {
|
|
20240
|
+
logger.error(
|
|
20241
|
+
`The environment is published at ${installVersion} but the backend did not come back onto it (restart ${restarted ? "succeeded" : "failed"}, health ${healthy ? "ok" : "did not answer"})`,
|
|
20091
20242
|
LogFileType.UPDATE_SERVICE
|
|
20092
20243
|
);
|
|
20093
|
-
|
|
20094
|
-
|
|
20244
|
+
const serving = await this.getInstalledBackendVersion();
|
|
20245
|
+
this.sendToRenderer("backend-update-completed", {
|
|
20246
|
+
installVersion,
|
|
20247
|
+
runningVersion: serving,
|
|
20248
|
+
restarted: false,
|
|
20249
|
+
restartable: this.backendIsAppOwned(),
|
|
20250
|
+
appOwnedEnvironment: true,
|
|
20251
|
+
/*
|
|
20252
|
+
* THE MISSING READING IS THE NEWS (UX U1). `serving === null` is not only
|
|
20253
|
+
* "no comparison available": it is a restart the app performed ITSELF, on
|
|
20254
|
+
* an environment it owns, after which the process it stopped did not
|
|
20255
|
+
* answer. Without this field the renderer's funnel declined - correctly,
|
|
20256
|
+
* by its own rule, for want of a reading - and the press was answered by
|
|
20257
|
+
* the fall-through success toast, "Server update completed successfully",
|
|
20258
|
+
* about a server that is not running. That is the same class of claim as
|
|
20259
|
+
* the operator's original report (a surface asserting what it had not
|
|
20260
|
+
* proved), which is why this branch exists rather than a softer sentence
|
|
20261
|
+
* on it.
|
|
20262
|
+
*
|
|
20263
|
+
* AND WITH `restartable` READ RATHER THAN ASSUMED (review R8) this field is
|
|
20264
|
+
* no longer only a shape a future producer could send: on an adopted daemon
|
|
20265
|
+
* whose restart did not take, `restartable` is false here, so the renderer's
|
|
20266
|
+
* `!actionRestartsTheServer` arm - "Nothing is serving this conversation
|
|
20267
|
+
* until that server is running again" - PAINTS. Design D14 filed that string
|
|
20268
|
+
* as copy no producer could reach; the arm is now reachable through this one.
|
|
20269
|
+
*/
|
|
20270
|
+
serverDidNotComeBack: serving === null
|
|
20271
|
+
});
|
|
20272
|
+
return true;
|
|
20273
|
+
}
|
|
20274
|
+
const running = await this.waitForBackendVersion(installVersion);
|
|
20275
|
+
if (running === null) {
|
|
20276
|
+
const serving = await this.getInstalledBackendVersion();
|
|
20277
|
+
logger.error(
|
|
20278
|
+
`The environment is published at ${installVersion} but the server serving this app reports ${serving ?? "no reading"}`,
|
|
20095
20279
|
LogFileType.UPDATE_SERVICE
|
|
20096
20280
|
);
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
|
|
20281
|
+
this.sendToRenderer("backend-update-completed", {
|
|
20282
|
+
installVersion,
|
|
20283
|
+
runningVersion: serving,
|
|
20284
|
+
restarted: false,
|
|
20285
|
+
restartable: this.backendIsAppOwned(),
|
|
20286
|
+
appOwnedEnvironment: true
|
|
20287
|
+
/*
|
|
20288
|
+
* This arm is NOT `serverDidNotComeBack` even when `serving` is null, and
|
|
20289
|
+
* the difference is what the app knows: the daemon answered the health
|
|
20290
|
+
* probe a moment ago (that is the branch above this one), so a missing
|
|
20291
|
+
* reading here is a version read that failed rather than a server that
|
|
20292
|
+
* never came back, and the panel may not claim the second.
|
|
20293
|
+
*
|
|
20294
|
+
* AND THE SUCCESS TOAST IS DELIBERATE HERE (UX U7, which asked for this
|
|
20295
|
+
* decision to be RECORDED where the reason already is rather than for a new
|
|
20296
|
+
* state). The completion is a claim about the update, and the update is what
|
|
20297
|
+
* happened: the environment moved, the app restarted the daemon onto it and
|
|
20298
|
+
* the daemon answered the probe - the only thing unread is WHICH build that
|
|
20299
|
+
* running process reports. The alternatives are worse rather than more
|
|
20300
|
+
* honest: a panel keyed on "a reading failed" would put a standing failed-
|
|
20301
|
+
* tone surface in front of a server that is up and serving, and it would
|
|
20302
|
+
* have to be dismissed by a user who has nothing to do about it, while the
|
|
20303
|
+
* sentence the toast carries says nothing about the build either way. What
|
|
20304
|
+
* the app may not do is CLAIM a build it did not read, and it does not.
|
|
20305
|
+
*/
|
|
20306
|
+
});
|
|
20100
20307
|
return true;
|
|
20101
|
-
}
|
|
20308
|
+
}
|
|
20309
|
+
logger.info(
|
|
20310
|
+
`The server serving this app reports ${running} after the app-managed environment update`,
|
|
20311
|
+
LogFileType.UPDATE_SERVICE
|
|
20312
|
+
);
|
|
20313
|
+
this.sendToRenderer("backend-update-completed", {
|
|
20314
|
+
installVersion,
|
|
20315
|
+
runningVersion: running,
|
|
20316
|
+
restarted: true
|
|
20317
|
+
});
|
|
20318
|
+
return true;
|
|
20319
|
+
}
|
|
20320
|
+
/**
|
|
20321
|
+
* Install a release into a freshly created environment, and say whether it landed
|
|
20322
|
+
* there.
|
|
20323
|
+
*
|
|
20324
|
+
* The two commands are the ones this app's own install script runs for the same
|
|
20325
|
+
* steps - create the environment on the managed runtime's interpreter, then
|
|
20326
|
+
* `pip install` into it - aimed at the generation the caller has just minted, and
|
|
20327
|
+
* pinned to the release the user was offered.
|
|
20328
|
+
*
|
|
20329
|
+
* It is deliberately NOT the install script itself. That one is the interactive
|
|
20330
|
+
* first run: it builds a modal progress window and it calls `app.exit(1)` when
|
|
20331
|
+
* the user cancels that window, so reaching for it here would put a second window
|
|
20332
|
+
* and a quit path inside an update - and an update that can quit the app is not
|
|
20333
|
+
* an update that leaves the runtimes running.
|
|
20334
|
+
*
|
|
20335
|
+
* `didUpgradeLand` rather than the exit code, for the reason it exists: pip
|
|
20336
|
+
* reports success when the requirement is already satisfied, and it can exit 0
|
|
20337
|
+
* having installed for a different interpreter, so the version read back out of
|
|
20338
|
+
* THIS environment is the only thing that answers whether the user got the
|
|
20339
|
+
* release they were offered.
|
|
20340
|
+
*
|
|
20341
|
+
* EVERY FAILING STEP HERE THROWS, and the caller's `false` is not this arm's to
|
|
20342
|
+
* send (review R6). The callback contract is `Promise<boolean>` because the
|
|
20343
|
+
* first-run installer shares it, but a boolean carries no words, and the words are
|
|
20344
|
+
* the whole remedy on a full disk: `publishGeneration` turns `false` into a fixed
|
|
20345
|
+
* sentence that `SETUP_FAILURE_CAUSES` does not match, so a publish that ran out of
|
|
20346
|
+
* space reached the reader with no disk-space remedy on it. Throwing the child's
|
|
20347
|
+
* captured output keeps that table's matching text intact end to end.
|
|
20348
|
+
*/
|
|
20349
|
+
async installEnvironmentInto(venv, python, target2) {
|
|
20350
|
+
const created = await runCommand(python, ["-m", "venv", venv], {
|
|
20351
|
+
timeoutMs: 10 * 60 * 1e3,
|
|
20352
|
+
env: this.pythonSpawnEnv()
|
|
20353
|
+
});
|
|
20354
|
+
if (created.exitCode !== 0) {
|
|
20355
|
+
const output = installerOutputOf(created);
|
|
20102
20356
|
logger.error(
|
|
20103
|
-
|
|
20104
|
-
LogFileType.UPDATE_SERVICE
|
|
20105
|
-
|
|
20357
|
+
`Could not create the update environment at ${venv}: ${output}`,
|
|
20358
|
+
LogFileType.UPDATE_SERVICE
|
|
20359
|
+
);
|
|
20360
|
+
throw new Error(
|
|
20361
|
+
`Could not create the update environment at ${venv}.
|
|
20362
|
+
|
|
20363
|
+
${output}`
|
|
20364
|
+
);
|
|
20365
|
+
}
|
|
20366
|
+
const interpreter = path$1.join(
|
|
20367
|
+
venv,
|
|
20368
|
+
process.platform === "win32" ? "Scripts" : "bin",
|
|
20369
|
+
process.platform === "win32" ? "python.exe" : "python"
|
|
20370
|
+
);
|
|
20371
|
+
const pip = buildPipUpgradeCommand(interpreter, target2);
|
|
20372
|
+
logger.info(
|
|
20373
|
+
`Installing into the new environment: ${pip.display}`,
|
|
20374
|
+
LogFileType.UPDATE_SERVICE
|
|
20375
|
+
);
|
|
20376
|
+
const run = await runCommand(pip.command, pip.args, {
|
|
20377
|
+
timeoutMs: 15 * 60 * 1e3,
|
|
20378
|
+
env: this.pythonSpawnEnv()
|
|
20379
|
+
});
|
|
20380
|
+
logger.info(
|
|
20381
|
+
`Install stdout:
|
|
20382
|
+
${run.stdout.trim()}`,
|
|
20383
|
+
LogFileType.UPDATE_SERVICE
|
|
20384
|
+
);
|
|
20385
|
+
if (run.stderr.trim().length > 0) {
|
|
20386
|
+
logger.warn(
|
|
20387
|
+
`Install stderr:
|
|
20388
|
+
${run.stderr.trim()}`,
|
|
20389
|
+
LogFileType.UPDATE_SERVICE
|
|
20106
20390
|
);
|
|
20107
|
-
if (this.backendService && !this.backendService.isUsingExternalBackend()) {
|
|
20108
|
-
try {
|
|
20109
|
-
logger.info(
|
|
20110
|
-
"Attempting to restart backend service after update failure...",
|
|
20111
|
-
LogFileType.UPDATE_SERVICE
|
|
20112
|
-
);
|
|
20113
|
-
await this.backendService.start();
|
|
20114
|
-
} catch (restartError) {
|
|
20115
|
-
logger.error(
|
|
20116
|
-
"Failed to restart backend service after update failure:",
|
|
20117
|
-
LogFileType.UPDATE_SERVICE,
|
|
20118
|
-
restartError
|
|
20119
|
-
);
|
|
20120
|
-
}
|
|
20121
|
-
}
|
|
20122
|
-
if (this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.webContents && !this.mainWindow.webContents.isDestroyed()) {
|
|
20123
|
-
this.mainWindow.webContents.send("backend-update-error", {
|
|
20124
|
-
// Cleaned at the source, for the reason written on the check-phase
|
|
20125
|
-
// report above.
|
|
20126
|
-
message: stripErrorPrefixes(error.message),
|
|
20127
|
-
phase: "update",
|
|
20128
|
-
logPath: serverUpdateLogPath()
|
|
20129
|
-
});
|
|
20130
|
-
}
|
|
20131
|
-
return false;
|
|
20132
20391
|
}
|
|
20392
|
+
const installed = await this.readBundledBackendVersion(interpreter);
|
|
20393
|
+
if (!didUpgradeLand({ before: null, after: installed, target: target2 })) {
|
|
20394
|
+
const output = installerOutputOf(run);
|
|
20395
|
+
logger.error(
|
|
20396
|
+
`The new environment is not on ${target2}: it reports ${installed ?? "no readable version"}; the installer's own output follows:
|
|
20397
|
+
${output}`,
|
|
20398
|
+
LogFileType.UPDATE_SERVICE
|
|
20399
|
+
);
|
|
20400
|
+
throw new Error(
|
|
20401
|
+
`The environment was created but ${target2} did not land in it; the installer reports ${installed ?? "no readable version"}.
|
|
20402
|
+
|
|
20403
|
+
${output}`
|
|
20404
|
+
);
|
|
20405
|
+
}
|
|
20406
|
+
ensureVenvBytecodeGuard(venv);
|
|
20407
|
+
return true;
|
|
20133
20408
|
}
|
|
20134
20409
|
/**
|
|
20135
20410
|
* Check for all updates (UI and backend)
|