Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 19 additions & 33 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,29 @@
# Generated by unreal-index setup. Re-run 'npm run setup' to regenerate.
# Generated by unreal-index. Re-run 'npm start' or 'npm run setup' to regenerate.
services:
discovery:
build: .
play:
build:
context: .
args:
BUILD_GIT_HASH: ${BUILD_GIT_HASH:-unknown}
image: unreal-index:latest
container_name: unreal-index-discovery
container_name: unreal-index-play
ports:
- "3847:3847"
volumes:
- discovery-db:/data/db
- discovery-mirror:/data/mirror
- discovery-zoekt:/data/zoekt-index
- play-db:/data/db
- play-mirror:/data/mirror
- play-zoekt:/data/zoekt-index
- ./workspaces.json:/app/workspaces.json:ro
- ./workspace-configs/discovery.json:/app/config.json:ro
mem_limit: 8g
memswap_limit: 10g
restart: unless-stopped
stop_grace_period: 15s

editormain:
build: .
image: unreal-index:latest
container_name: unreal-index-editormain
ports:
- "3848:3847"
volumes:
- pioneer-db:/data/db
- pioneer-mirror:/data/mirror
- pioneer-zoekt:/data/zoekt-index
- ./workspaces.json:/app/workspaces.json:ro
- ./workspace-configs/editormain.json:/app/config.json:ro
mem_limit: 8g
memswap_limit: 10g
- ./workspace-configs/play.json:/app/config.json:ro
mem_limit: 7g
memswap_limit: 9g
restart: unless-stopped
stop_grace_period: 15s

volumes:
discovery-db:
discovery-mirror:
discovery-zoekt:
pioneer-db:
pioneer-mirror:
pioneer-zoekt:
play-db:
name: unreal-index-play-db
play-mirror:
name: unreal-index-play-mirror
play-zoekt:
name: unreal-index-play-zoekt
152 changes: 71 additions & 81 deletions public/setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ <h3>Start Watcher</h3>
${ws.running
? `<a class="btn btn-primary btn-sm" href="/index.html" onclick="WorkspaceContext.onSelect('${escAttr(name)}')">Dashboard</a>
<button class="btn btn-secondary btn-sm" onclick="stopWorkspace('${esc(name)}', this)">Stop</button>`
: `<button class="btn btn-success btn-sm" onclick="startWorkspace('${esc(name)}', this)">Start</button>
: `<button class="btn btn-success btn-sm" data-workspace="${escAttr(name)}" onclick="startWorkspace('${esc(name)}', this)">Start</button>
<span class="ws-start-error error-msg" style="font-size:12px"></span>`}
<button class="btn btn-secondary btn-sm" onclick="rebuildWorkspace('${esc(name)}')">Rebuild</button>
<button class="btn btn-secondary btn-sm" onclick="viewWorkspaceLogs('${esc(name)}')">Logs</button>
Expand Down Expand Up @@ -793,7 +793,8 @@ <h3>Start Watcher</h3>
}
}

async function startWorkspace(name, btn) {
async function startWorkspace(name, btn, opts) {
if (!btn) return;
btn.disabled = true;
btn.textContent = 'Starting...';
const statusEl = btn.parentElement.querySelector('.ws-start-error');
Expand All @@ -802,19 +803,34 @@ <h3>Start Watcher</h3>
const resp = await fetch('/api/docker/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspace: name }),
body: JSON.stringify({ workspace: name, ...opts }),
});
const data = await resp.json();

// Proactive port conflict detection
if (data.portConflicts && data.portConflicts.length > 0 && statusEl) {
btn.textContent = 'Start';
btn.disabled = false;
const c = data.portConflicts[0];
const procInfo = c.process ? `${esc(c.process)} (PID ${esc(String(c.pid))})` : 'unknown process';
statusEl.innerHTML = `Port ${esc(String(c.port))} is in use by ${procInfo}. `
+ `<button class="btn btn-danger btn-sm" onclick="startWorkspace('${escAttr(name)}', document.querySelector('[data-workspace=&quot;${escAttr(name)}&quot;]'), {killConflicts:true})" style="margin-left:4px">Kill &amp; Start</button> `
+ `<button class="btn btn-secondary btn-sm" onclick="startWorkspace('${escAttr(name)}', document.querySelector('[data-workspace=&quot;${escAttr(name)}&quot;]'), {force:true})" style="margin-left:4px">Start Anyway</button>`;
return;
}

if (!resp.ok || !data.ok) {
btn.textContent = 'Start';
btn.disabled = false;
const errMsg = data.error || 'Failed to start container';
// Detect port conflict and offer to kill the blocking process
// Fallback: detect port conflict from Docker error
if (errMsg.includes('address already in use') && statusEl) {
const portMatch = errMsg.match(/host port for [^:]*:(\d+)/);
const port = portMatch ? portMatch[1] : null;
if (port) {
statusEl.innerHTML = `Port ${esc(port)} is in use. <button class="btn btn-danger btn-sm" onclick="killPortProcess(${port}, '${escAttr(name)}', this)" style="margin-left:4px">Kill blocking process</button>`;
statusEl.innerHTML = `Port ${esc(port)} is in use. `
+ `<button class="btn btn-danger btn-sm" onclick="startWorkspace('${escAttr(name)}', document.querySelector('[data-workspace=&quot;${escAttr(name)}&quot;]'), {killConflicts:true})" style="margin-left:4px">Kill &amp; Start</button> `
+ `<button class="btn btn-secondary btn-sm" onclick="startWorkspace('${escAttr(name)}', document.querySelector('[data-workspace=&quot;${escAttr(name)}&quot;]'), {force:true})" style="margin-left:4px">Start Anyway</button>`;
} else {
statusEl.textContent = errMsg;
}
Expand All @@ -834,43 +850,6 @@ <h3>Start Watcher</h3>
}
}

async function killPortProcess(port, workspaceName, btn) {
btn.disabled = true;
btn.textContent = 'Killing...';
try {
// Check what's on the port first
const checkResp = await fetch(`/api/port-check/${port}`);
const checkData = await checkResp.json();
if (!checkData.inUse) {
btn.parentElement.innerHTML = '<span class="success-msg">Port is free now.</span>';
return;
}
// Kill it
const killResp = await fetch('/api/port-kill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port }),
});
const killData = await killResp.json();
if (killData.ok) {
const procInfo = checkData.process ? ` (${checkData.process} PID ${checkData.pid})` : '';
btn.parentElement.innerHTML = `<span class="success-msg">Killed process${esc(procInfo)}. Retrying...</span>`;
// Auto-retry starting the workspace
setTimeout(() => {
const startBtn = btn.closest('.ws-actions')?.querySelector('.btn-success');
if (startBtn) startWorkspace(workspaceName, startBtn);
else loadWorkspaces();
}, 1500);
} else {
btn.textContent = 'Failed';
btn.disabled = false;
}
} catch (err) {
btn.textContent = 'Error';
btn.disabled = false;
}
}

async function stopWorkspace(name, btn) {
btn.disabled = true;
btn.textContent = 'Stopping watcher...';
Expand Down Expand Up @@ -1063,22 +1042,38 @@ <h3 style="color:#f44747;margin-bottom:12px">Delete workspace "${esc(name)}"?</h

buildDocker(terminal, status, (success) => {
if (success) {
status.textContent = 'Restarting container...';
fetch('/api/docker/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspace: name }),
}).then(resp => resp.json()).then(data => {
if (data.ok) {
status.innerHTML = '<span class="success-msg">Rebuilt and started!</span>';
setTimeout(loadWorkspaces, 3000);
} else {
status.textContent = 'Start failed: ' + (data.error || 'unknown');
}
}).catch(err => {
status.textContent = 'Start error: ' + err.message;
});
startAfterRebuild(name);
}
});
}

function startAfterRebuild(name, opts) {
const status = document.getElementById('ws-rebuild-status-' + name);
// Disable any action buttons in the status area to prevent double-submission
status.querySelectorAll('button').forEach(b => { b.disabled = true; });
status.textContent = 'Restarting container...';
fetch('/api/docker/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspace: name, ...opts }),
}).then(resp => resp.json()).then(data => {
// Proactive port conflict detection
if (data.portConflicts && data.portConflicts.length > 0) {
const c = data.portConflicts[0];
const procInfo = c.process ? `${esc(c.process)} (PID ${esc(String(c.pid))})` : 'unknown process';
status.innerHTML = `Port ${esc(String(c.port))} is in use by ${procInfo}. `
+ `<button class="btn btn-danger btn-sm" onclick="startAfterRebuild('${escAttr(name)}', {killConflicts:true})" style="margin-left:4px">Kill &amp; Start</button> `
+ `<button class="btn btn-secondary btn-sm" onclick="startAfterRebuild('${escAttr(name)}', {force:true})" style="margin-left:4px">Start Anyway</button>`;
return;
}
if (data.ok) {
status.innerHTML = '<span class="success-msg">Rebuilt and started!</span>';
setTimeout(loadWorkspaces, 3000);
} else {
status.textContent = 'Start failed: ' + (data.error || 'unknown');
}
}).catch(err => {
status.textContent = 'Start error: ' + err.message;
});
}

Expand Down Expand Up @@ -1824,7 +1819,7 @@ <h3 style="color:#f44747;margin-bottom:12px">Delete workspace "${esc(name)}"?</h

// ── Start container ────────────────────────────────────

async function startContainer() {
async function startContainer(opts) {
const btn = document.getElementById('start-btn');
const status = document.getElementById('start-status');
btn.disabled = true;
Expand All @@ -1834,18 +1829,34 @@ <h3 style="color:#f44747;margin-bottom:12px">Delete workspace "${esc(name)}"?</h
const resp = await fetch('/api/docker/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workspace: wizardData.workspaceName }),
body: JSON.stringify({ workspace: wizardData.workspaceName, ...opts }),
});
const data = await resp.json();

// Proactive port conflict detection
if (data.portConflicts && data.portConflicts.length > 0) {
const c = data.portConflicts[0];
const procInfo = c.process ? `${esc(c.process)} (PID ${esc(String(c.pid))})` : 'unknown process';
status.innerHTML = `<span class="error-msg">Port ${esc(String(c.port))} is in use by ${procInfo}.</span> `
+ `<button class="btn btn-danger btn-sm" onclick="startContainer({killConflicts:true})">Kill &amp; Start</button> `
+ `<button class="btn btn-secondary btn-sm" onclick="startContainer({force:true})">Start Anyway</button>`;
btn.textContent = 'Start Container';
btn.disabled = false;
return;
}

if (data.ok) {
status.innerHTML = `<span class="success-msg">Started! <a href="/index.html" onclick="WorkspaceContext.onSelect('${escAttr(wizardData.workspaceName)}')" style="color:#569cd6">Open Dashboard</a></span>`;
} else {
const errMsg = data.error || 'unknown';
// Fallback: detect port conflict from Docker error
if (errMsg.includes('address already in use')) {
const portMatch = errMsg.match(/host port for [^:]*:(\d+)/);
const port = portMatch ? portMatch[1] : null;
if (port) {
status.innerHTML = `<span class="error-msg">Port ${esc(port)} is in use.</span> <button class="btn btn-danger btn-sm" onclick="killPortAndRetryWizard(${port})">Kill blocking process & retry</button>`;
status.innerHTML = `<span class="error-msg">Port ${esc(port)} is in use.</span> `
+ `<button class="btn btn-danger btn-sm" onclick="startContainer({killConflicts:true})">Kill &amp; Start</button> `
+ `<button class="btn btn-secondary btn-sm" onclick="startContainer({force:true})">Start Anyway</button>`;
} else {
status.textContent = 'Failed: ' + errMsg;
}
Expand All @@ -1860,27 +1871,6 @@ <h3 style="color:#f44747;margin-bottom:12px">Delete workspace "${esc(name)}"?</h
}
}

async function killPortAndRetryWizard(port) {
const status = document.getElementById('start-status');
status.innerHTML = '<span class="scan-spinner"></span> Killing process...';
try {
const checkResp = await fetch(`/api/port-check/${port}`);
const checkData = await checkResp.json();
if (checkData.inUse) {
await fetch('/api/port-kill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port }),
});
const procInfo = checkData.process ? ` (${checkData.process} PID ${checkData.pid})` : '';
status.innerHTML = `<span class="success-msg">Killed${esc(procInfo)}. Retrying...</span>`;
}
setTimeout(startContainer, 1500);
} catch (err) {
status.innerHTML = `<span class="error-msg">Failed to kill process: ${esc(err.message)}</span>`;
}
}

// ── Install hooks ──────────────────────────────────────

function commonParentDir(paths) {
Expand Down
Loading