From d4ee572eccbdfbc0597e67ce283d8878d3dc3bc6 Mon Sep 17 00:00:00 2001 From: robertvoy Date: Tue, 15 Jul 2025 08:19:13 +1000 Subject: [PATCH] Runpod support --- distributed.py | 8 +++- web/main.js | 29 +++++++++--- web/ui.js | 125 ++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 128 insertions(+), 34 deletions(-) diff --git a/distributed.py b/distributed.py index e9a7c0f..1a1a14e 100644 --- a/distributed.py +++ b/distributed.py @@ -297,6 +297,11 @@ async def update_worker_endpoint(request): worker.pop("extra_args", None) else: worker["extra_args"] = data["extra_args"] + + # Handle type field + if "type" in data: + worker["type"] = data["type"] + worker_found = True break @@ -310,7 +315,8 @@ async def update_worker_endpoint(request): "port": data["port"], "cuda_device": data["cuda_device"], "enabled": data.get("enabled", False), - "extra_args": data.get("extra_args", "") + "extra_args": data.get("extra_args", ""), + "type": data.get("type", "local") } if "workers" not in config: config["workers"] = [] diff --git a/web/main.js b/web/main.js index ecd7efe..efc1a63 100644 --- a/web/main.js +++ b/web/main.js @@ -266,13 +266,17 @@ class DistributedExtension { getWorkerUrl(worker, endpoint = '') { const host = worker.host || window.location.hostname; - // Simple rule: port 443 = HTTPS, anything else = HTTP - const useHttps = worker.port === 443; + // Cloud workers always use HTTPS + const isCloud = worker.type === 'cloud'; + // Simple rule: port 443 = HTTPS, cloud workers = HTTPS, anything else = HTTP + const useHttps = isCloud || worker.port === 443; const protocol = useHttps ? 'https' : 'http'; + // Cloud workers use default HTTPS port (443) + const effectivePort = isCloud ? 443 : worker.port; // Don't add port for standard HTTPS/HTTP ports const defaultPort = useHttps ? 443 : 80; - const port = worker.port === defaultPort ? '' : `:${worker.port}`; + const port = effectivePort === defaultPort ? '' : `:${effectivePort}`; return `${protocol}://${host}${port}${endpoint}`; } @@ -695,10 +699,19 @@ class DistributedExtension { } isRemoteWorker(worker) { + // Check if explicitly marked as cloud worker + if (worker.type === "cloud") { + return true; + } + // Otherwise check by host (backward compatibility) const host = worker.host || window.location.hostname; return host !== "localhost" && host !== "127.0.0.1" && host !== window.location.hostname; } + isCloudWorker(worker) { + return worker.type === "cloud"; + } + getMasterUrl() { // Always use the detected/configured master IP for consistency if (this.config?.master?.host) { @@ -873,7 +886,9 @@ class DistributedExtension { // Get form values const name = document.getElementById(`name-${workerId}`).value; - const isRemote = document.getElementById(`remote-${workerId}`).checked; + const workerType = document.getElementById(`worker-type-${workerId}`).value; + const isRemote = workerType === 'remote' || workerType === 'cloud'; + const isCloud = workerType === 'cloud'; const host = isRemote ? document.getElementById(`host-${workerId}`).value : window.location.hostname; const port = parseInt(document.getElementById(`port-${workerId}`).value); const cudaDevice = isRemote ? undefined : parseInt(document.getElementById(`cuda-${workerId}`).value); @@ -890,7 +905,7 @@ class DistributedExtension { return; } - if (isRemote && !host.trim()) { + if ((workerType === 'remote' || workerType === 'cloud') && !host.trim()) { app.extensionManager.toast.add({ severity: "error", summary: "Validation Error", @@ -900,7 +915,7 @@ class DistributedExtension { return; } - if (isNaN(port) || port < 1 || port > 65535) { + if (!isCloud && (isNaN(port) || port < 1 || port > 65535)) { app.extensionManager.toast.add({ severity: "error", summary: "Validation Error", @@ -963,6 +978,7 @@ class DistributedExtension { try { await this.api.updateWorker(workerId, { name: name.trim(), + type: workerType, host: isRemote ? host.trim() : null, port: port, cuda_device: isRemote ? null : cudaDevice, @@ -971,6 +987,7 @@ class DistributedExtension { // Update local config worker.name = name.trim(); + worker.type = workerType; if (isRemote) { worker.host = host.trim(); delete worker.cuda_device; diff --git a/web/ui.js b/web/ui.js index de41240..2421cdf 100644 --- a/web/ui.js +++ b/web/ui.js @@ -49,7 +49,12 @@ const cardConfigs = { }, infoText: (data, extension) => { const isRemote = extension.isRemoteWorker(data); - if (isRemote) { + const isCloud = data.type === 'cloud'; + + if (isCloud) { + // For cloud workers, don't show port (it's always 443) + return `${data.name}
${data.host}`; + } else if (isRemote) { return `${data.name}
${data.host}:${data.port}`; } else { const cudaInfo = data.cuda_device !== undefined ? `CUDA ${data.cuda_device} • ` : ''; @@ -525,66 +530,130 @@ export class DistributedUI { const nameGroup = this.createFormGroup("Name:", worker.name, `name-${worker.id}`); form.appendChild(nameGroup.group); - // Remote checkbox - const remoteGroup = document.createElement("div"); - remoteGroup.style.cssText = "display: flex; align-items: center; gap: 8px; margin: 5px 0;"; + // Worker type dropdown + const typeGroup = document.createElement("div"); + typeGroup.style.cssText = "display: flex; flex-direction: column; gap: 4px; margin: 5px 0;"; - const remoteCheckbox = document.createElement("input"); - remoteCheckbox.type = "checkbox"; - remoteCheckbox.id = `remote-${worker.id}`; - remoteCheckbox.checked = extension.isRemoteWorker(worker); - remoteCheckbox.onchange = (e) => { - const isRemote = e.target.checked; + const typeLabel = document.createElement("label"); + typeLabel.htmlFor = `worker-type-${worker.id}`; + typeLabel.textContent = "Worker Type:"; + typeLabel.style.cssText = "font-size: 12px; color: #ccc;"; + + const typeSelect = document.createElement("select"); + typeSelect.id = `worker-type-${worker.id}`; + typeSelect.style.cssText = "padding: 4px 8px; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; font-size: 12px;"; + + // Create options + const localOption = document.createElement("option"); + localOption.value = "local"; + localOption.textContent = "Local"; + + const remoteOption = document.createElement("option"); + remoteOption.value = "remote"; + remoteOption.textContent = "Remote"; + + const cloudOption = document.createElement("option"); + cloudOption.value = "cloud"; + cloudOption.textContent = "Cloud"; + + typeSelect.appendChild(localOption); + typeSelect.appendChild(remoteOption); + typeSelect.appendChild(cloudOption); + + // Create powered by Runpod text (initially hidden) + const runpodText = document.createElement("a"); + runpodText.id = `runpod-text-${worker.id}`; + runpodText.href = "https://www.runpod.io"; + runpodText.target = "_blank"; + runpodText.textContent = "Deploy cloud worker with Runpod"; + runpodText.style.cssText = "font-size: 12px; color: #4a90e2; text-decoration: none; margin-top: 4px; display: none; cursor: pointer;"; + + typeSelect.onchange = (e) => { + const workerType = e.target.value; // Show/hide relevant fields const hostGroup = document.getElementById(`host-group-${worker.id}`); + const hostInput = document.getElementById(`host-${worker.id}`); + const portGroup = document.getElementById(`port-group-${worker.id}`); + const portInput = document.getElementById(`port-${worker.id}`); const cudaGroup = document.getElementById(`cuda-group-${worker.id}`); const argsGroup = document.getElementById(`args-group-${worker.id}`); + const runpodTextElem = document.getElementById(`runpod-text-${worker.id}`); - if (isRemote) { + if (workerType === "local") { + hostGroup.style.display = "none"; + portGroup.style.display = "flex"; + cudaGroup.style.display = "flex"; + argsGroup.style.display = "flex"; + runpodTextElem.style.display = "none"; + } else if (workerType === "remote") { hostGroup.style.display = "flex"; + portGroup.style.display = "flex"; cudaGroup.style.display = "none"; argsGroup.style.display = "none"; + runpodTextElem.style.display = "none"; + // Update placeholder for remote workers + hostInput.placeholder = "e.g., 192.168.1.100"; // If switching to remote and host is localhost, clear it - const hostInput = document.getElementById(`host-${worker.id}`); if (hostInput.value === "localhost" || hostInput.value === "127.0.0.1") { hostInput.value = ""; } - } else { - hostGroup.style.display = "none"; - cudaGroup.style.display = "flex"; - argsGroup.style.display = "flex"; + } else if (workerType === "cloud") { + hostGroup.style.display = "flex"; + portGroup.style.display = "flex"; // Keep port visible for cloud workers + cudaGroup.style.display = "none"; + argsGroup.style.display = "none"; + runpodTextElem.style.display = "block"; + // Update placeholder for cloud workers + hostInput.placeholder = "e.g., your-cloud-worker.trycloudflare.com"; + // Set port to 443 for cloud workers + portInput.value = "443"; + // If switching to cloud and host is localhost, clear it + if (hostInput.value === "localhost" || hostInput.value === "127.0.0.1") { + hostInput.value = ""; + } } }; - const remoteLabel = document.createElement("label"); - remoteLabel.htmlFor = `remote-${worker.id}`; - remoteLabel.textContent = "Remote Worker"; - remoteLabel.style.cssText = "font-size: 12px; color: #ccc; cursor: pointer;"; + typeGroup.appendChild(typeLabel); + typeGroup.appendChild(typeSelect); + typeGroup.appendChild(runpodText); + form.appendChild(typeGroup); - remoteGroup.appendChild(remoteCheckbox); - remoteGroup.appendChild(remoteLabel); - form.appendChild(remoteGroup); + // Set initial value based on worker type + if (worker.type === "cloud") { + typeSelect.value = "cloud"; + // Show Runpod text immediately for cloud workers + runpodText.style.display = "block"; + } else if (extension.isRemoteWorker(worker)) { + typeSelect.value = "remote"; + } else { + typeSelect.value = "local"; + } + + // Trigger initial state + typeSelect.dispatchEvent(new Event('change')); // Host field (only for remote workers) const hostGroup = this.createFormGroup("Host:", worker.host || "", `host-${worker.id}`, "text", "e.g., 192.168.1.100"); hostGroup.group.id = `host-group-${worker.id}`; - hostGroup.group.style.display = extension.isRemoteWorker(worker) ? "flex" : "none"; + hostGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "flex" : "none"; form.appendChild(hostGroup.group); // Port field const portGroup = this.createFormGroup("Port:", worker.port, `port-${worker.id}`, "number"); + portGroup.group.id = `port-group-${worker.id}`; form.appendChild(portGroup.group); // CUDA Device field (only for local workers) const cudaGroup = this.createFormGroup("CUDA Device:", worker.cuda_device || 0, `cuda-${worker.id}`, "number"); cudaGroup.group.id = `cuda-group-${worker.id}`; - cudaGroup.group.style.display = extension.isRemoteWorker(worker) ? "none" : "flex"; + cudaGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "none" : "flex"; form.appendChild(cudaGroup.group); // Extra Args field (only for local workers) const argsGroup = this.createFormGroup("Extra Args:", worker.extra_args || "", `args-${worker.id}`); argsGroup.group.id = `args-group-${worker.id}`; - argsGroup.group.style.display = extension.isRemoteWorker(worker) ? "none" : "flex"; + argsGroup.group.style.display = (extension.isRemoteWorker(worker) || worker.type === "cloud") ? "none" : "flex"; form.appendChild(argsGroup.group); // Buttons @@ -728,7 +797,9 @@ export class DistributedUI { if (config.dynamic && data) { if (isRemote) { - const remoteInfo = this.createButton("Remote worker", null, BUTTON_STYLES.info); + const isCloud = data.type === 'cloud'; + const workerTypeText = isCloud ? "Cloud worker" : "Remote worker"; + const remoteInfo = this.createButton(workerTypeText, null, BUTTON_STYLES.info); remoteInfo.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl + BUTTON_STYLES.info + " color: #999; cursor: default;"; remoteInfo.disabled = true; controlsWrapper.appendChild(remoteInfo);