[eric] apps: fix 2nd instance on old workspaces (port shim) + dedupe header keys

This commit is contained in:
ciregenz
2026-07-02 21:39:59 -07:00
parent 40b5fc93cb
commit 40d975c702
3 changed files with 56 additions and 2 deletions
+3
View File
@@ -34,6 +34,7 @@ from backend.apps.outputs.runtime_proc import (
RECENT_ERRORS_MAX,
TERMINATE_GRACE_SECONDS,
background_priority_kwargs,
ensure_force_port_shim,
find_free_port,
is_new_mode,
is_port_free,
@@ -207,6 +208,8 @@ class AppRuntime:
return await self.p_start_old_mode()
async def p_start_new_mode(self) -> bool:
# Legacy workspaces (scaffolded pre-multi-instance) ignore the forced ports; self-heal their run.sh so a second instance stops colliding on the primary's ports.
ensure_force_port_shim(self.workspace_path)
env_path = os.path.join(self.workspace_path, ".env")
fp_raw = read_env_value(env_path, "FRONTEND_PORT")
bp_raw = read_env_value(env_path, "BACKEND_PORT")
+50
View File
@@ -210,6 +210,56 @@ def write_env_value(env_path: str, key: str, value: str) -> None:
logger.exception("failed writing %s=%s to %s", key, value, env_path)
def ensure_force_port_shim(workspace_path: str) -> None:
"""Retrofit the per-instance port override into a legacy workspace's `run.sh`.
Apps scaffolded before the multi-instance patch have a run.sh that sources
`.env` and then pins every instance to the same FRONTEND_PORT/BACKEND_PORT,
so opening a SECOND instance collides on the primary's ports and its preview
never binds ("Starting preview" forever). This injects the exact override
block the current template ships (inert unless OPENSWARM_FORCE_*_PORT is set,
i.e. only for secondary instances), leaving the primary untouched. Idempotent;
mirrors Patch 1a in scripts/fetch-webapp-template.sh."""
run_sh = os.path.join(workspace_path, "run.sh")
if not os.path.isfile(run_sh):
return
try:
with open(run_sh, encoding="utf-8") as f:
lines = f.readlines()
except Exception:
return
if any("OPENSWARM_FORCE_FRONTEND_PORT" in ln for ln in lines):
return
block = [
"\n",
"# Per-instance port overrides: OpenSwarm passes these when the user opens a SECOND instance of the app, so it boots on fresh ports instead of colliding with the primary's .env-pinned ones.\n",
'if [[ -n "${OPENSWARM_FORCE_FRONTEND_PORT:-}" ]]; then\n',
' export FRONTEND_PORT="$OPENSWARM_FORCE_FRONTEND_PORT"\n',
"fi\n",
'if [[ -n "${OPENSWARM_FORCE_BACKEND_PORT:-}" ]]; then\n',
' export BACKEND_PORT="$OPENSWARM_FORCE_BACKEND_PORT"\n',
"fi\n",
]
out: list[str] = []
sourced = False
inserted = False
for ln in lines:
out.append(ln)
if 'source "$ROOT_DIR/.env"' in ln:
sourced = True
elif sourced and not inserted and ln.strip() == "fi":
out.extend(block)
inserted = True
if not inserted:
logger.warning("run.sh in %s lacks a recognizable `source .env` block; secondary instances may collide on ports", workspace_path)
return
try:
with open(run_sh, "w", encoding="utf-8") as f:
f.writelines(out)
except Exception:
logger.exception("failed injecting per-instance port shim into %s", run_sh)
def is_new_mode(workspace_path: str) -> bool:
"""A workspace is "new-mode" (webapp-template scaffold) if it has a
`run.sh` at its root. Old-mode workspaces are flat `index.html`-only
@@ -10,7 +10,7 @@ import { useAppDispatch } from '@/shared/hooks';
import DashboardGlyph from './DashboardGlyph';
import ShareButton from '@/app/components/share/ShareButton';
import type { AgentSession } from '@/shared/state/agentsSlice';
import { saveLayout } from '@/shared/state/dashboardLayoutSlice';
import { saveLayout, viewCardKey } from '@/shared/state/dashboardLayoutSlice';
import type { CardPosition, ViewCardPosition, BrowserCardPosition, NotePosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
import type { Output } from '@/shared/state/outputsSlice';
import type { CanvasActions } from '../hooks/interaction/useCanvasControls';
@@ -73,7 +73,8 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
.map((vc) => {
const output = outputs[vc.output_id];
if (!output) return null;
return { id: vc.output_id, name: output.name, card: vc };
const label = (vc.instance ?? 1) > 1 ? `${output.name} #${vc.instance}` : output.name;
return { id: viewCardKey(vc.output_id, vc.instance), name: label, card: vc };
})
.filter(Boolean) as Array<{ id: string; name: string; card: ViewCardPosition }>;