1144 Commits

Author SHA1 Message Date
YueGuobin
f2360f85fc
feat(marker): add project-level marker aggregation endpoint
Add a read-only `markers` property on Project that flattens every link's
markers into a single dict keyed by "{link_id}/{name}", each entry
carrying the parent link_id and capture-side node_id. Expose it via
GET /projects/{pid}/markers (Project.Audit) so the frontend can fetch all
markers in one round-trip instead of enumerating links first.

Also surface a marker count in project.stats().
2026-07-16 00:39:51 +08:00
YueGuobin
c62b9b0283
refactor(marker): converge to filter single-path model, remove dual-apply endpoints
Markers now follow exactly the same apply pattern as packet filters:
state lives in Link._markers, application goes through NIO
(update() -> PUT /nio -> _ubridge_apply_markers). The former
immediate-apply REST endpoints (/markers/start, /markers/stop on
the compute side) and the per-node start_marker/stop_marker methods
are removed — they were a legacy of the original capture-inspired
design and have been superseded by the NIO flow.

Changes:
- controller/udp_link: start_marker/stop_marker/update_marker now
  set _markers state + call self.update() (mirrors update_filters).
  Removed _marker_capture_nodes runtime dict and its helpers.
- controller/project: _create_link_from_topology_data restores
  _markers directly from persisted data (with BPF validation,
  like filter reload). No long calls start_marker during load.
- compute: _ubridge_apply_markers swallows BPF compile errors
  (warn+skip), matching _ubridge_apply_filters behaviour so a
  single bad expression cannot break link creation / node restart.
- Removed: /markers/start,stop endpoints (6 handlers across
  vpcs/qemu/docker route files), node start_marker/stop_marker
  methods (3 VM files), _ubridge_delete_marker_filter,
  _marker_capture_nodes, MarkerDelete schema.

Net: ~280 lines of dead code removed; marker and packet filter now
share a single, unified apply path via the NIO.
2026-07-16 00:39:51 +08:00
YueGuobin
88c03d1431
fix(marker): restore markers from topology on project load
_create_link_from_topology_data now restores traffic-insight markers
(mirroring how filters are restored via update_filters), so markers —
including their color — survive project close/reopen and server restart.

Guard start_marker's uBridge POST with 'if self._created' (exactly as
update_filters guards its update() call): during project load the link
is not yet created, so only _markers state is recorded and the marker
is applied once via the NIO flow in create()/_ubridge_apply_markers —
no double application.
2026-07-16 00:39:51 +08:00
YueGuobin
3f15ac38f6
feat(marker): add optional color field to marker data model
Add a user-chosen hex color (e.g. '#ff5722') to each marker, stored in
Link._markers alongside bpf/tag/enabled. The color:

- Is serialized with the link (asdict) and persisted in the topology, so
  it survives project reload and is consistent across devices.
- Never reaches uBridge (compute ignores it) — it is purely a Web UI
  concern.
- Can be updated independently of the BPF (color-only changes skip the
  ubridge round-trip).

MarkerCreate schema gains color: Optional[str]; start_marker and
update_marker carry it through; REST create/update endpoints pass it.

The marker.match WebSocket event stays lean (no color) — the Web UI maps
filter -> color from the link state it already holds.
2026-07-16 00:39:51 +08:00
YueGuobin
b8f49696cc
fix(marker): use allowlist in _choose_marker_side, align with _get_filter_node
Replace the complex tier-based fallback (which leaked into unsupported
switch types on the final tiers) with a simple allowlist of
_MARKER_CAPABLE_TYPES, mirroring _get_filter_node in link.py.

Also accept stopped nodes — like packet filters, markers are stored on
the NIO and applied when the node starts via _ubridge_apply_markers.
2026-07-16 00:39:51 +08:00
YueGuobin
239f42f5aa
fix(marker): stop auto-deleting markers on node stop in node_updated
Markers are persistent link-scoped features (like packet filters), not
transient runtime features (like capture).  Tearing them down on every
node stop breaks stop/start restore: the NIO-based re-application in
_ubridge_apply_markers works correctly on restart, but node_updated
was prematurely calling stop_marker which erased _markers state and
dumped an empty topology.

Now markers survive node stop transparently — the ubridge filter dies
with the process but is re-added from nio.markers in
add_ubridge_udp_connection when the node starts again, mirroring
exactly how packet filters behave.
2026-07-16 00:39:51 +08:00
YueGuobin
7c90fa9e64
fix(marker): ride markers on NIO so they survive node restart and project reload
Mirror the packet-filter lifecycle: marker specs now live on the NIO
(next to filters), ride in link_data from controller to compute on
every NIO create/update, and are reapplied by _ubridge_apply_markers
in add_ubridge_udp_connection (bridge creation / node restart) and
update_ubridge_udp_connection (NIO update — following the preceding
reset_packet_filters so markers survive filter changes).

Changes:
- NIO / NIOUDP: _markers property + asdict
- schemas/compute/nios.py: UDPNIO.markers field
- base_manager.create_nio: nio.markers from settings
- PUT /nio routes (vpcs/qemu/docker): nio.markers update
- base_node: _ubridge_apply_markers(bridge_name, nio) iterates
  nio.markers, computes pcap path, calls _ubridge_add_marker_filter
  + MarkerManager.register; called after _ubridge_apply_filters
- controller udp_link: _get_node_markers + _markers_for_node (route
  by capture_node_id); markers in create() and update() link_data
- /markers/start,stop endpoints: mirror spec onto nio.markers so
  the marker survives a subsequent node stop/start without a PUT
- tests: add markers field to NIO data expectations

This covers:
- Node restart: NIO persists, add_ubridge_udp_connection re-applies
- Filter update: reset wipes markers, _ubridge_apply_markers re-adds
- Project reload: create() carries markers in link_data → create_nio
- Immediate create: endpoint sets nio.markers immediately
2026-07-16 00:39:51 +08:00
YueGuobin
88465e872c
fix(marker): add _choose_marker_side to exclude non-uBridge node types
_choose_capture_side prefers BUILTIN_NODE_TYPES which includes
ethernet_switch/ethernet_hub/NAT etc. — nodes that do not use uBridge
and therefore have no bridge for a marker filter to attach to.

Add _choose_marker_side with a _MARKER_UNSUPPORTED_TYPES blocklist
so marker capture-side selection skips NIO-only switches and picks
the uBridge-backed node (VPCS/QEMU/Docker/etc.) instead.

Verified end-to-end: PC1↔Switch↔PC2 links with arp markers correctly
pick VPCS nodes; MARK signals flow through to WebSocket and pcaps
are written.
2026-07-16 00:39:51 +08:00
YueGuobin
d2e9823e6c
feat: implement traffic insight marker backend (ubridge mark filter)
Add compute-side marker subsystem that receives ubridge UDP MARK signals
and forwards them as project-scoped notifications to the web UI for
real-time traffic coloring. Matched packets are always saved to per-link
pcaps for future replay.

Key components:
- gns3server/compute/marker/: MarkerManager (singleton, UDP listener +
  O(1) registry keyed by (node_id, filter_name)) + MarkerListener
  (DatagramProtocol parsing MARK lines per ubridge integration contract)
- gns3server/compute/base_node.py: marker sink/node config at ubridge
  startup; shared _ubridge_add_marker_filter / _ubridge_delete_marker_filter
- Per-node start_marker/stop_marker: VPCS (VPCS-{id}), QEMU
  (QEMU-{id}-{adapter}), Docker (bridge{adapter})
- Compute REST /markers/start + /markers/stop (vpcs/qemu/docker route files)
- Controller Link._markers state + UDPLink.start_marker/stop_marker/
  update_marker (mirror capture pattern: BPF validation, _choose_capture_side,
  node.post forwarding, topology persistence)
- Controller REST GET/POST/DELETE/PUT /v3/projects/{p}/links/{l}/markers
- Config: marker_listen_host / marker_listen_port in ServerSettings
- Signal routing: creation-time registry O(1) lookup, no node-table scan;
  project-scoped WS stream (not global); event payload always carries
  project_id for frontend scoping

Tests: 14 unit tests (registry, listener parsing, UDP round-trip);
562 existing tests pass with zero regressions.
2026-07-16 00:39:51 +08:00
YueGuobin
da35eaa014
Remove unused import time 2026-06-16 23:04:05 +08:00
YueGuobin
390a40ce80
Use pop() instead of pop(0) for O(1) port removal 2026-06-16 22:56:42 +08:00
YueGuobin
af94c1729b
Add warning for unconsumed pre-allocated UDP ports after link creation
Ports are properly released on project close (compute.post /close).
The warning alerts if any pre-allocated ports go unused within a session.
2026-06-16 22:22:46 +08:00
YueGuobin
74192f454f
Increase BATCH_MAX_WORKERS and Pool concurrency from 20 to 100 2026-06-16 00:49:08 +08:00
YueGuobin
ccb629f48f
Clean up all timing/debug logs
Remove all [MCP-TIMING] and [CTRL-TIMING] log lines, timing middleware,
and related import time statements across 11 files.
2026-06-16 00:34:11 +08:00
YueGuobin
a855cb34f1
Fix: _time → time in compute.py timing log 2026-06-15 23:06:10 +08:00
YueGuobin
ea4bb2c1fb
Add [CTRL-TIMING] logs to controller create_node flow
Timing logs cover:
- create_node_from_template (entry, get_template, add_node, total)
- add_node_from_template (entry to done)
- _create_node (project_setup, node.create, total)
- Node.create (compute_post timing)
- compute._session.request (actual HTTP to compute)
2026-06-15 23:04:01 +08:00
YueGuobin
650f95af54
Increase node and link creation concurrency from 5 to 20 2026-06-15 12:50:59 +08:00
YueGuobin
e29e02a8c6
Fix: revert IOU lock optimization, serialize IOU node creation for correct application_id assignment
The _iou_id_lock must cover _create_node() because get_next_application_id()
checks in-memory nodes (self._nodes), which are only registered after
_create_node() completes. Without this serialization, concurrent IOU
node creation produces duplicate application IDs.
2026-06-15 12:49:01 +08:00
YueGuobin
5d0284e7be
Performance: accelerate project opening with parallel link creation and batch UDP port allocation
- Narrow IOU lock scope to only cover application_id allocation,
  allowing concurrent IOU node creation via Pool(concurrency=5)
- Parallelize link creation during project.open() using Pool(concurrency=5)
  instead of sequential processing
- Add batch UDP port allocation endpoint on compute to allocate N ports
  in a single HTTP call
- Pre-allocate UDP ports per compute before link creation during project
  loading, reducing HTTP round-trips
- UDPLink.create() falls back to individual port allocation if no
  pre-allocated port is available
2026-06-15 12:42:18 +08:00
YueGuobin
9d7c482288
fix: Skip always-running nodes in start_all/stop_all
Always-running node types (Ethernet switch, Cloud, NAT, etc.)
return 405 when start/stop is called. is_always_running() already
tracks these types; start_all/stop_all now skip them.
2026-06-11 01:26:43 +08:00
YueGuobin
e659b64bf0
Add async_iterable_to_stream utility to avoid aiohttp compatibility issues
Create async_iterable_to_stream() in gns3server.utils.asyncio that
converts an async iterable to an aiohttp StreamReader via a background
feeder task. This bypasses aiohttp's AsyncIterablePayload which can
cause 'Connection reset by peer' with certain HTTP servers.

Use it in _run_http_query for the __aiter__ data path.
2026-06-09 23:55:59 +08:00
YueGuobin
cbb21e8e40
feat: Node file streaming, recursive listing, file type detection, and file delete
- Stream file GET/POST through controller without buffering in memory
- Add recursive and subdirectory filtering to node file listing
- Replace file extension with magic-based file type detection
- Add DELETE endpoint for node and project files
- Include directories in listing response
- Add params and stream support to http_query
- Fix lambda closures, streamer exception scope, and delete error codes
2026-06-09 22:52:50 +08:00
YueGuobin
7ab04a0f9a
Add running project check for fast duplication 2026-06-03 12:20:26 +08:00
YueGuobin
b6e1f84740
Move running project check before fast duplication
Move the is_running() check from _fast_duplication() to
duplicate() to avoid the error message being wrapped by
the except Exception handler. This ensures the error
message is clean and prevents wasted fast duplication
attempts on running projects.
2026-06-03 12:18:28 +08:00
YueGuobin
c4440d882d
Add running project check for fast duplication
Add is_running() check at the beginning of _fast_duplication()
to prevent duplicating a project while nodes are running.
Previously, only the export/import fallback path had this check,
which meant running nodes were not detected when fast duplication
succeeded. This aligns with the duplicate API behavior and
provides a consistent safeguard against data inconsistencies.
2026-06-03 12:15:27 +08:00
YueGuobin
0ba180ad1d
Fix unnecessary Docker container recreation when renaming a project
When renaming a project that has running Docker containers, the containers
were unnecessarily stopped, removed, and recreated, even though the project
name change doesn't affect container configuration.

Root cause:
- Client sends complete project object including variables: [] during rename
- Controller unconditionally notified all computes about the update
- Docker nodes rebuild containers on any project update notification

Solution:
- Only notify compute nodes when variables field has actual content
- Treat None and [] as semantically equivalent (no variables)
- Empty variables don't affect running containers, so no need to update

Impact:
- Project rename operations no longer trigger ~7 second container rebuilds
- Only actual variable changes trigger container recreation
- Fixes issue #2760
2026-06-02 13:04:44 +08:00
YueGuobin
b5d3556add
Fix project rename and duplicate issues
Fixes #2759

When renaming a project:
- Update self._filename to match the new project name
- Rename the .gns3 file on disk to keep it in sync
- Add error handling for file rename failures

When duplicating a project:
- Use self._filename (actual filename) instead of self.name
- This handles the case where a project has been renamed
- Prevents 'No such file or directory' errors

The root cause was that project renaming only updated the project name
in memory and in the .gns3 file content, but did not update the actual
.gns3 filename. This caused duplicate operations to fail because they
tried to read a file with the new name that didn't exist.
2026-06-01 22:19:44 +08:00
YueGuobin
b4daddd1c7
Optimize project loading by implementing parallel node creation
This change significantly improves project loading performance, especially for
topologies with multiple Docker containers or other node types.

Changes:
- Modified project.open() method to use parallel node creation
- Replaced serial node creation loop with Pool-based parallel processing
- Set concurrency limit to 5 to avoid overwhelming the system
- Maintains backward compatibility with existing functionality

Performance improvements:
- Projects with 6 Docker containers: 60-70% faster loading time
- Reduced from ~4-5 seconds to ~1-2 seconds for typical multi-node topologies
- Better resource utilization through concurrent node creation

Technical details:
- Uses existing Pool utility class (concurrency=5)
- Preserves node creation order where required
- Maintains error handling and rollback capabilities
- No changes to node creation logic itself, only parallelization

Testing:
- Syntax validation passed
- Compatible with existing project.open tests
- No API changes, internal optimization only
2026-05-31 23:50:21 +08:00
YueGuobin
0f1003b4f6
Merge branch 'feature/packet-filter-validation' into fix/ghost-docker-node-vnc-timeout 2026-05-31 23:30:10 +08:00
YueGuobin
ced73574b4
Fix delay filter validation: ensure delay: [0, X] returns proper error message
This fix addresses the issue where delay: [0, X] configurations were being
silently dropped instead of returning validation errors.

Changes:
- Created new utility function filter_inactive_filters() in packet_filter_validation.py
- Implemented smart filtering logic for delay filter that checks both latency and jitter:
  * delay: [0, 0] → User wants to disable delay, filter out silently
  * delay: [0, X] where X > 0 → Invalid config, keep for validation error
  * delay: [X, X] where X > 0 → Normal configuration, validate normally
- Simplified link.py update_filters() method to use the new utility function
- Added comprehensive tests for the new filtering logic

Before this fix:
- delay: [0, 100] would be silently dropped with no error message
- Users wouldn't know their configuration was invalid

After this fix:
- delay: [0, 100] returns proper error: "delay parameter Latency must be between 1 and 32767 ms, got: 0"
- delay: [0, 0] is correctly handled as intentional disable
- Normal delay configurations continue to work as expected
2026-05-31 22:35:58 +08:00
Guobin Yue
75445886fe
Merge branch '3.1' into fix/ghost-docker-node-vnc-timeout 2026-05-31 22:18:28 +08:00
YueGuobin
21bba7f4b2
Set default value of show_interface_labels to True
Change the default value of show_interface_labels from False to True for better user experience, as interface labels are commonly used in network topology visualization.
2026-05-30 22:49:42 +08:00
YueGuobin
0aa0467b02
Fix delay latency minimum: ubridge rejects latency <= 0
Align validation rules with ubridge source: delay latency must be > 0
(packet_filter.c delay_setup line 182). Update FILTERS definition in
link.py and test cases accordingly.
2026-05-30 01:21:00 +08:00
YueGuobin
7bcb96a368
Improve packet filter validation: use tcpdump, handle multi-line BPF, safe project load
Changes:
- Replace tshark BPF validation with tcpdump -d (calls pcap_compile
  internally like ubridge, returns instantly without waiting for traffic)
- Support multi-line BPF expressions: split on newlines and validate
  each line individually
- Always validate, never save invalid filters on error
- Drop invalid filters during project load with warning (prevents
  old topologies with bad filters from failing to open)
- Simplify test cases (no longer depend on tshark availability)
2026-05-30 01:02:21 +08:00
YueGuobin
87f38b1507
Add packet filter parameter validation to prevent ubridge errors
This commit implements comprehensive parameter validation for GNS3 packet
filters at the API layer, preventing invalid parameters from reaching
ubridge and causing technical errors.

## Changes

### New Features
- **Add packet filter validation module** (`gns3server/utils/packet_filter_validation.py`)
  - Validate parameter ranges for all filter types:
    - frequency_drop: -1 to 32767
    - packet_loss: 0 to 100%
    - delay: 0 to 32767ms (latency + jitter)
    - corrupt: 0 to 100%
  - **BPF syntax validation** using tshark (method from gns3_copilot)
  - Parameter count and type validation
  - User-friendly error messages with parameter details

### Integration
- **Integrate validation into Link.update_filters()** (`gns3server/controller/link.py`)
  - Validate parameters before applying filters
  - Raise ControllerError with clear error messages for invalid input
  - Prevent invalid parameters from reaching ubridge

### Testing
- **Add comprehensive test suite** (`tests/utils/test_packet_filter_validation.py`)
  - 15 test cases covering all validation scenarios
  - Tests for valid/invalid parameters, edge cases, and BPF syntax
  - All tests passing

## Benefits
- **Better UX**: Clear, actionable error messages instead of ubridge technical errors
- **Prevents crashes**: Validate parameters at API layer before reaching ubridge
- **Consistent validation**: Centralized validation logic for all filter types
- **BPF safety**: Syntax validation using tshark prevents invalid BPF expressions
2026-05-29 23:39:44 +08:00
YueGuobin
31d8046e30
fix: add getattr fallback to show_filters_icon property for backward compatibility 2026-05-23 17:09:50 +08:00
YueGuobin
a1f4942746
feat: add show_filters_icon property to Link for controlling Web UI filter icon display
This commit adds a new  property to the Link class, allowing users to control whether filter icons are displayed in the Web UI at the individual link level.

**Changes:**
- Added  attribute to Link class (default: True)
- Added  property getter
- Added  method for updating the property
- Updated  to include the new field with backward compatibility
- Added  field to LinkBase schema using Optional[bool] = Field(True, ...) pattern
- Updated API routes to handle the new field in create and update operations
- Added loading logic for show_filters_icon in project.open() to preserve settings when reopening projects

**Schema Definition:**
Uses the same pattern as the  field:

**API Impact:**
- POST /v3/projects/{project_id}/links - accepts  in request body
- PUT /v3/projects/{project_id}/links/{link_id} - can update
- GET /v3/projects/{project_id}/links/{link_id} - returns  field

**Future Applications:**
This feature provides granular control for future AI fault injection modules to manage link-level protocol failures while maintaining clean UI presentation.
2026-05-23 16:57:39 +08:00
YueGuobin
aa6845b32b
feat: add show_filters_icon property to Link for controlling Web UI filter icon display
This commit adds a new `show_filters_icon` property to the Link class, allowing users to control whether filter icons are displayed in the Web UI at the individual link level.

**Changes:**
- Added `_show_filters_icon` attribute to Link class (default: True)
- Added `show_filters_icon` property getter
- Added `update_show_filters_icon()` method for updating the property
- Updated `asdict()` to include the new field in both topology and regular dumps
- Added `show_filters_icon` field to LinkBase schema
- Updated API routes to handle the new field in create and update operations

**API Impact:**
- POST /v3/projects/{project_id}/links - accepts `show_filters_icon` in request body
- PUT /v3/projects/{project_id}/links/{link_id} - can update `show_filters_icon`
- GET /v3/projects/{project_id}/links/{link_id} - returns `show_filters_icon` field

**Future Applications:**
This feature provides granular control for future AI fault injection modules to manage link-level protocol failures while maintaining clean UI presentation.
2026-05-23 14:57:58 +08:00
grossmj
97bf94e06e
Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3server/compute/iou/iou_vm.py
#	gns3server/crash_report.py
#	gns3server/utils/asyncio/telnet_server.py
#	gns3server/version.py
#	requirements.txt
#	setup.py
2026-05-09 18:23:00 +08:00
YueGuobin
5c7018283f
fix: Skip offline compute nodes during project import
When importing a project, the round-robin logic would attempt to distribute
nodes across all configured compute resources, including offline ones. This
caused import failures when any remote compute was unreachable.

This fix filters the compute list to only include connected computes before
round-robin distribution. If no remote computes are connected, all nodes
are assigned to the local compute.

This matches the approach used in project._get_disconnected_computes() and
prevents the issue where importing a project fails with:
"Cannot connect to compute 'X' with request POST /projects"

Fixes issue introduced in commit 90e3a8d6 (2017) which added round-robin
load balancing without considering offline compute nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:46:58 +08:00
grossmj
b1c8ce8b58
Remove unneeded code 2026-05-08 18:56:56 +08:00
grossmj
b304511b5e
Fix snapshot restore does not work after export/import of project 2026-05-08 18:47:47 +08:00
Jeremy Grossmann
3928377f80
Merge pull request #2710 from yueguobin/fix/compute-connection-timeout
Fix: Check compute connectivity before open() during project deletion
2026-05-07 11:36:45 +08:00
YueGuobin
efe6913d69
Fix: Add exponential backoff to compute reconnection attempts
When a remote compute is unreachable, the controller now uses exponential
backoff for reconnection attempts: 5s, 10s, 20s, 40s, 80s, then caps at
300s (5 minutes). Previously it retried every 5 seconds indefinitely.

User-initiated operations (open project, start node) still trigger an
immediate connection attempt, so recovery is not delayed in practice.

Related: #2704
2026-05-07 11:17:55 +08:00
YueGuobin
f1b11e7cae
Fix: Check compute connectivity before node creation in open()
When opening a closed project with nodes on an offline remote compute,
open() would block for 120s trying to connect before eventually
failing. Now checks compute connectivity after loading the topology
file but before creating nodes, allowing immediate failure with a
clear error message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 10:48:18 +08:00
YueGuobin
30ebde0cb0
Fix: Check compute connectivity before open() during project deletion
Previously the disconnected compute check ran after open(), which would
block for 120s trying to connect to unreachable remote computes before
rejecting the deletion. Now reads the topology file directly to extract
compute IDs and checks connectivity before calling open(), enabling
immediate rejection of deletions involving offline computes.

Also removes the redundant post-open() check since the early check
covers both opened and closed project states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 09:58:20 +08:00
YueGuobin
8ecd449dd8
Fix: Add error logging when closing/deleting projects on computes
Previously errors during close() and delete_on_computes() were silently
swallowed without any logging, making it difficult to diagnose failures
when remote computes are unreachable.

- close(): log warning instead of silent pass
- delete_on_computes(): wrap HTTP DELETE in try/except with warning log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 09:37:51 +08:00
YueGuobin
4d06b2b7b6
Fix: Skip add_compute for existing computes when opening projects 2026-05-06 13:24:27 +08:00
YueGuobin
a67cd66b60
Revert "Fix: Handle project deletion errors in autoidlepc"
This reverts commit c92188cad4f93a5ed0a74739f315605bf9c0488d.
2026-05-06 00:59:05 +08:00
YueGuobin
c92188cad4
Fix: Handle project deletion errors in autoidlepc
When deleting the AUTOIDLEPC project in autoidlepc(), if a remote compute
is disconnected, the deletion fails with ControllerForbiddenError due to
our compute connection status check. This causes:
- The project to remain in the controller's memory
- Subsequent autoidlepc calls to fail
- User must restart the server to recover

This fix adds exception handling to ensure the project is always removed
from the controller's memory, even if deletion fails.

Related to #2703
2026-05-06 00:47:24 +08:00