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
45 changes: 44 additions & 1 deletion server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
get_launcher_json_for_workflow_json,
get_launcher_state,
get_project_port,
get_project_args,
is_launcher_json_format,
is_port_in_use,
run_command,
Expand Down Expand Up @@ -273,6 +274,7 @@ def start_project(id):

# find a free port
port = get_project_port(id)
args = get_project_args(id)
assert port, "No free port found"
assert not is_port_in_use(port), f"Port {port} is already in use"

Expand All @@ -285,6 +287,9 @@ def start_project(id):
# start the project
command = f"python main.py --port {port} --listen 0.0.0.0"

if args is not None:
command += " " + args

# check if gpus are available, if they aren't, use the cpu
mps_available = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
if not torch.cuda.is_available() and not mps_available:
Expand All @@ -294,7 +299,7 @@ def start_project(id):
if os.name == "nt":
command = f"start \"\" cmd /c \"{command}\""

print(f"USING COMMAND: {command}. PORT: {port}")
print(f"USING COMMAND: {command}. PORT: {port}. ARGS: {args}")

pid = run_command_in_project_comfyui_venv(
project_path, command, in_bg=True
Expand Down Expand Up @@ -339,6 +344,44 @@ def stop_project(id):
set_launcher_state_data(project_path, {"state": "ready", "status_message" : "Ready", "port": None, "pid": None})
return jsonify({"success": True})

@app.route("/api/projects/<id>/set-project-settings", methods=["POST"])
def set_project_settings(id):
project_path = os.path.join(PROJECTS_DIR, id)
args_file_path = os.path.join(project_path, "args.txt")
port_file_path = os.path.join(project_path, "port.txt")

if not os.path.exists(project_path):
return jsonify({"error": f"Project with id {id} does not exist"}), 404

data = request.get_json()
arguments = data.get("arguments", "")
port = data.get("port", "")

try:
if arguments:
with open(args_file_path, "w") as args_file:
args_file.write(arguments)
else:
os.remove(args_file_path)
except FileNotFoundError:
pass

try:
if port and int(port) > 0:
with open(port_file_path, "w") as port_file:
port_file.write(str(port))
else:
os.remove(port_file_path)
except FileNotFoundError:
pass

return jsonify({"success": True})

@app.route("/api/projects/<id>/get-project-settings", methods=["POST"])
def get_project_settings(id):
args = get_project_args(id)
port = get_project_port(id)
return jsonify({"arguments": args, "port": port})

@app.route("/api/projects/<id>/delete", methods=["POST"])
def delete_project(id):
Expand Down
7 changes: 7 additions & 0 deletions server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,13 @@ def get_project_port(id):
return int(f.read().strip())
return find_free_port(PROJECT_MIN_PORT, PROJECT_MAX_PORT)

def get_project_args(id):
project_path = os.path.join(PROJECTS_DIR, id)
if os.path.exists(os.path.join(project_path, "args.txt")):
with open(os.path.join(project_path, "args.txt"), "r") as f:
return f.read().strip()
return None

def is_port_in_use(port: int) -> bool:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
Expand Down
1 change: 0 additions & 1 deletion web/dist/assets/index-BcHurkhF.css

This file was deleted.

1 change: 1 addition & 0 deletions web/dist/assets/index-CFIoonmy.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ComfyUI Launcher</title>
<script type="module" crossorigin src="/assets/index-B7I_54js.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BcHurkhF.css">
<script type="module" crossorigin src="/assets/index-XBbIeSHG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CFIoonmy.css">
</head>
<body>
<div id="root"></div>
Expand Down
104 changes: 101 additions & 3 deletions web/src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { Project, Settings } from '@/lib/types'
import { Button } from './ui/button'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Textarea } from '@/components/ui/textarea'
import { Input } from '@/components/ui/input'
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'
import React, { useEffect } from 'react'
import {
AlertDialog,
Expand All @@ -14,7 +16,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog'
import { Loader2Icon } from 'lucide-react'
import { DialogDescription } from '@radix-ui/react-dialog'

Expand Down Expand Up @@ -50,6 +52,12 @@ function ProjectCard({ item, settings }: ProjectCardProps) {

const [deleteProjectDialogOpen, setDeleteProjectDialogOpen] =
React.useState(false)
const [projectSettings, setProjectSettings] =
React.useState(false)
const [portNumber, setPortNumber] =
React.useState<{ [key: string]: string }>({})
const [argumentsText, setArgumentsText] =
React.useState<{ [key: string]: string }>({})

const [projectOperation, setProjectOperation] = React.useState<
'launch' | 'stop' | 'delete'
Expand Down Expand Up @@ -88,6 +96,39 @@ function ProjectCard({ item, settings }: ProjectCardProps) {
queryClient.invalidateQueries({ queryKey: ['projects'] })
},
})
const { refetch: refetchArguments } = useQuery({
queryKey: ['projectArguments', item.id],
queryFn: async () => {
const response = await fetch(`/api/projects/${item.id}/get-project-settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
setArgumentsText(prev => ({ ...prev, [item.id]: data.arguments }));
setPortNumber(prev => ({ ...prev, [item.id]: data.port }));
return data.arguments || "", data.port || "";
},

enabled: projectSettings,
});

const setProjectArguments = useMutation({
mutationFn: async () => {
const response = await fetch(`/api/projects/${item.id}/set-project-settings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ arguments: argumentsText[item.id], port: portNumber[item.id] }),
})
const data = await response.json()
return data
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] })
refetchArguments()
},
})

const deleteProjectMutation = useMutation({
mutationFn: async () => {
Expand Down Expand Up @@ -179,6 +220,50 @@ function ProjectCard({ item, settings }: ProjectCardProps) {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog open={projectSettings} onOpenChange={setProjectSettings}>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<div className="settings-section">
<h3 className="mb-1">Port</h3>
<Input
name="port"
placeholder="Enter Port Number"
type="number"
value={portNumber[item.id] || ""}
onChange={(e) => setPortNumber(prev => ({
...prev,
[item.id]: e.target.value
}))}
/>
</div>
<div className="settings-section">
<h3 className="mb-1">Launch Arguments</h3>
<Textarea
name="arguments"
placeholder="Enter Launch Arguments"
value={argumentsText[item.id] || ""}
onChange={(e) => setArgumentsText(prev => ({
...prev,
[item.id]: e.target.value
}))}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setProjectSettings(false)}>Cancel</Button>
<Button
onClick={(e) => {
e.preventDefault()
setProjectSettings(false)
setProjectArguments.mutate()
}}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div
className={
'rounded-md p-5 border ' +
Expand Down Expand Up @@ -209,7 +294,20 @@ function ProjectCard({ item, settings }: ProjectCardProps) {
>
Launch
</Button>
)}
)}{
item.state.state === 'ready' && (
(
<Button
onClick={(e) => {
e.preventDefault()
setProjectSettings(true)
}}
variant="outline">
Settings
</Button>
)
)
}
{item.state.state === 'running' &&
!!item.state.port && (
<Button variant="default" asChild>
Expand Down
24 changes: 24 additions & 0 deletions web/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"

export { Textarea }