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
9 changes: 4 additions & 5 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@
origin.replace(':4000', ':4001'),
origin.replace(':4000', ':4002')
])
# if DEBUG:
# CORS_ALLOWED_ORIGINS = [
# env.str('VISUAL_CIRCUIT_FRONTEND_HOST'),
# f"{env.str('VISUAL_CIRCUIT_FRONTEND_HOST')}:{DESIRED_PORT}",
# ]

# Allow frontend to read the Content-Disposition header to get the correct .zip filename
CORS_EXPOSE_HEADERS = ['Content-Disposition']

49 changes: 46 additions & 3 deletions backend/staticfiles/synthesis/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@ def clean_shared_memory(signum, frame, names, processes):
sys.exit(0)


def process_wrapper(method, inputs, outputs, parameters, sync):
"""
Wrapper that restores sys.stdin before running a block's main().

On macOS / Windows, multiprocessing uses 'spawn' which starts a fresh
Python interpreter where sys.stdin is None. Calling input() in that
state raises EOFError immediately (Issue #359).

Strategy:
1. If fd 0 is a real TTY, open /dev/tty so the block gets a proper
interactive terminal (works even inside Docker with -it).
2. Otherwise fall back to os.fdopen(0) for piped / non-TTY contexts.
3. If both fail, leave sys.stdin unchanged and let the block handle it.
"""
import sys
import os

if os.isatty(0):
# fd 0 is connected to a real terminal — give the block full TTY access
try:
sys.stdin = open('/dev/tty', 'r')
except OSError:
# /dev/tty not available (rare); fall back to wrapping fd 0 directly
try:
sys.stdin = os.fdopen(0)
except OSError:
pass # leave sys.stdin as-is; block may not need input()
# If fd 0 is NOT a tty (pipe / redirect / Docker without -it),
# we do NOT restore stdin — input() will raise EOFError, which is the
# correct, expected behaviour in a non-interactive environment.

method(inputs, outputs, parameters, sync)


def main():
"""
Main function
Expand Down Expand Up @@ -117,14 +151,23 @@ def main():
for block_id, block in blocks.items():
name = BLOCK_DIRECTORY + "." + block["name"]
mod = importlib.import_module(name)
method = method = getattr(mod, FUNCTION_NAME)
method = getattr(mod, FUNCTION_NAME)

# Ensure block_data entry exists even for blocks with no wires / parameters
block_data[block_id] = block_data.get(
block_id, {"inputs": {}, "outputs": {}, "parameters": {}}
)

inputs = Inputs(block_data[block_id]["inputs"])
outputs = Outputs(block_data[block_id]["outputs"])
parameters = Parameters(block_data[block_id]["parameters"])
freq = block_data[block_id]["frequency"]
# Default frequency to 30 Hz if not specified by any wire / synchronize_frequency entry
freq = block_data[block_id].get("frequency", 30)
processes.append(
multiprocessing.Process(target=method, args=(inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30))))
multiprocessing.Process(
target=process_wrapper,
args=(method, inputs, outputs, parameters, Synchronise(1 / (freq if freq != 0 else 30)))
)
)

# Register handler for Ctrl+C
Expand Down
4 changes: 3 additions & 1 deletion frontend/.env.development
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
REACT_APP_BACKEND_HOST=http://localhost:8080/api/
REACT_APP_BACKEND_HOST=http://localhost:8080/api/
HOST=localhost
DANGEROUSLY_DISABLE_HOST_CHECK=true
6 changes: 0 additions & 6 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 50 additions & 3 deletions frontend/src/components/dialogs/project-info-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, TextField } from '@material-ui/core';
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, TextField, MenuItem } from '@material-ui/core';
import React, { ChangeEvent, useState } from 'react';
import { create, InstanceProps } from 'react-modal-promise';
import { ProjectInfo } from '../../core/constants';
Expand All @@ -19,7 +19,7 @@ interface ProjectInfoDialogProps extends InstanceProps<ProjectInfo>, Partial<Pro
* }
*/
const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
name, version, description, author, image }: ProjectInfoDialogProps) => {
name, version, description, author, image, category, tags }: ProjectInfoDialogProps) => {

// Name of package. Use empty string if not defined
const [nameInput, setName] = useState(name || '');
Expand All @@ -31,6 +31,19 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
const [authorInput, setAuthor] = useState(author || '');
// Icon of package. Use empty string if not defined
const [imageInput, setImage] = useState(image || '');
// Category of the package
const [categoryInput, setCategory] = useState(category || '');
// Tags of the package (comma separated)
const [tagsInput, setTags] = useState(tags ? tags.join(', ') : '');

const ALLOWED_CATEGORIES = [
"Computer Vision",
"Control Systems",
"Locomotion",
"Machine Learning",
"Utilities",
"ROS2"
];

const fileReader = new FileReader();
fileReader.onload = (event) => {
Expand Down Expand Up @@ -62,7 +75,9 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
version: versionInput,
description: descriptionInput,
author: authorInput,
image: imageInput
image: imageInput,
category: categoryInput,
tags: tagsInput.split(',').map(t => t.trim()).filter(t => t.length > 0)
});
}

Expand Down Expand Up @@ -125,6 +140,38 @@ const ProjectInfoDialog = ({ isOpen, onResolve, onReject,
onChange={(event) => setAuthor(event.target.value)}
fullWidth
/>

<DialogContentText>
Category
</DialogContentText>
<TextField
select
margin="dense"
variant='outlined'
value={categoryInput}
onChange={(event) => setCategory(event.target.value)}
fullWidth
>
{ALLOWED_CATEGORIES.map((cat) => (
<MenuItem key={cat} value={cat}>
{cat}
</MenuItem>
))}
</TextField>

<DialogContentText>
Tags (comma separated)
</DialogContentText>
<TextField
margin="dense"
type="text"
variant='outlined'
value={tagsInput}
onChange={(event) => setTags(event.target.value)}
placeholder="e.g. cv2, camera, face detection"
fullWidth
/>

<DialogContentText>
Image
</DialogContentText>
Expand Down
Loading
Loading