Compare commits

...

16 Commits

Author SHA1 Message Date
Jeremy Grossmann
c776d4ef37 Merge pull request #3838 from GNS3/dependabot/pip/pytest-9.0.3
Bump pytest from 9.0.2 to 9.0.3
2026-08-16 15:16:41 +02:00
Jeremy Grossmann
38ecc9c4ed Merge pull request #3847 from prajwal-raj/patch-2
Remove deleted project from recent projects immediately
2026-08-09 22:06:42 +02:00
Jeremy Grossmann
30e34073c8 Merge pull request #3846 from prajwal-raj/patch-1
Fix vmrun detection for native 64-bit VMware Workstation (26H1+)
2026-08-09 19:21:31 +02:00
Prajwal Raj
a403512fa7 Remove deleted project from recent projects immediately
Project.destroy() (used only when a project is actually deleted, not
when it's simply closed) issued a raw DELETE request and never
refreshed the controller's cached project list. Since "recent
projects" filtering in the GUI checks each entry against that cached
list, a deleted project stayed visible under Recent Projects - and
could still be clicked, which threw a GUI freeze - until the app was
restarted and the list was freshly refetched.

destroy() now uses its own callback, _projectDestroyedCallback, which
does everything the previous callback did and additionally calls
Controller.refreshProjectList() on success. This matches the same
pattern already used by Controller.deleteProject() elsewhere in the
codebase. The ordinary project-close path (close()) is untouched.

Fixes #3815
2026-08-09 22:26:42 +05:30
Prajwal Raj
8a97b300ca Fix vmrun detection for native 64-bit VMware Workstation (26H1+)
VMware Workstation 26H1 dropped the 32-bit build and is now
native 64-bit only. Its install path is written to the native
64-bit registry view (SOFTWARE\VMware, Inc.\VMware Workstation)
instead of the WOW64 view (SOFTWARE\Wow6432Node\VMware, Inc.\...)
that findVmrun() previously checked exclusively.

This meant GNS3 could no longer auto-detect vmrun.exe on hosts
running VMware Workstation 26H1+, throwing "VMware vmrun tool
could not be found" even though it was installed, requiring
users to manually set the path in Preferences.

This adds a check of the native 64-bit registry key before
falling back to the Wow6432Node key, for both the Workstation
and VIX lookups. No other behavior changes.

Fixes #3845
2026-08-09 22:04:38 +05:30
Jeremy Grossmann
957c4f5de0 Merge pull request #3842 from GNS3/release/v2.2.61
Release v2.2.61
2026-07-30 19:15:45 +02:00
grossmj
5182bc3841 Release v2.2.61 2026-07-30 12:52:29 +02:00
grossmj
04b90c6aab Merge branch 'master' into 2.2 2026-07-29 17:53:21 +02:00
grossmj
424ef9d60e Development on 2.2.61.dev1 2026-07-15 20:45:39 +02:00
dependabot[bot]
a4984a396a Bump pytest from 9.0.2 to 9.0.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 18:44:43 +00:00
Jeremy Grossmann
bfc1ed525c Merge pull request #3837 from GNS3/release/v2.2.60
Release v2.2.60
2026-07-15 20:43:39 +02:00
grossmj
6c5f78c6cd Release v2.2.60 2026-07-15 18:36:27 +02:00
Jeremy Grossmann
0e46bbaf81 Merge pull request #3835 from GNS3/internal-tail
Live packet capture with internal tail
2026-07-15 08:52:08 +02:00
grossmj
de1b8e0c59 feat(capture): update internal tail command 2026-07-15 08:36:47 +02:00
grossmj
5c2640d3ff feat(capture): implement live packet capture with internal tail 2026-07-14 22:30:07 +02:00
grossmj
0136c8e85a Add support for Ptyxis terminal. Fixes #3834 2026-07-12 09:27:57 +02:00
9 changed files with 156 additions and 13 deletions

View File

@@ -1,5 +1,14 @@
# Change Log
## 2.2.61 30/07/2026
* No changes
## 2.2.60 15/07/2026
* feat(capture): implement live packet capture with internal tail
* Add support for Ptyxis terminal. Fixes #3834
## 2.2.59 08/05/2026
* Remove psutil version check

View File

@@ -1,5 +1,5 @@
-rrequirements.txt
pytest==8.4.2; python_version == '3.9' # version 8.4.2 is the last one supporting Python 3.9
pytest==9.0.2; python_version >= '3.10'
pytest==9.0.3; python_version >= '3.10'
pytest-timeout==2.4.0

View File

@@ -50,7 +50,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://dd662ce99d7e4a04714a89939ec523c9@o19455.ingest.us.sentry.io/38506"
DSN = "https://52512702e506197edfb413c1d7e13397@o19455.ingest.us.sentry.io/38506"
_instance = None
def __init__(self):

View File

@@ -78,9 +78,14 @@ class VMware(Module):
vmrun_path = shutil.which("vmrun")
if vmrun_path is None:
# look for vmrun.exe using the VMware Workstation directory listed in the registry
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
# (native 64-bit key first; VMware Workstation 26H1+ dropped the 32-bit build)
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\VMware, Inc.\VMware Workstation")
if vmrun_path is None:
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
if vmrun_path is None:
# look for vmrun.exe using the VIX directory listed in the registry
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\VMware, Inc.\VMware VIX")
if vmrun_path is None:
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware VIX")
elif sys.platform.startswith("darwin"):
vmware_fusion_vmrun_path = None

View File

@@ -25,6 +25,7 @@ from .local_config import LocalConfig
from .settings import PACKET_CAPTURE_SETTINGS
from .dialogs.capture_dialog import CaptureDialog
from .topology import Topology
from .pcap_to_wireshark import PCAPToWireshark
import logging
log = logging.getLogger(__name__)
@@ -38,6 +39,7 @@ class PacketCapture:
def __init__(self):
self._tail_process = {}
self._capture_reader_process = {}
self._capture_stream_thread = {}
# Auto start the capture program for this link
self._autostart = {}
@@ -56,6 +58,11 @@ class PacketCapture:
self._tail_process = {}
self._capture_reader_process = {}
for thread in list(self._capture_stream_thread.values()):
thread.stop()
thread.wait()
self._capture_stream_thread = {}
def topology(self):
from .topology import Topology
return Topology.instance()
@@ -92,7 +99,7 @@ class PacketCapture:
link = self.topology().getLink(link_id)
if link:
if link.capturing():
if self._autostart.get(link) and link not in self._tail_process:
if self._autostart.get(link) and (link not in self._tail_process and link not in self._capture_stream_thread):
log.debug("Starting packet capture reader for link {}".format(link.link_id()))
self.startPacketCaptureReader(link)
else:
@@ -128,6 +135,13 @@ class PacketCapture:
pass
del self._tail_process[link]
if link in self._capture_stream_thread:
log.debug("Stopping packet capture stream thread for link {}".format(link.link_id()))
self._capture_stream_thread[link].stop()
self._capture_stream_thread[link].wait()
self._capture_stream_thread[link].error_signal.disconnect()
del self._capture_stream_thread[link]
def startPacketCaptureAnalyzer(self, link):
"""
Starts the packet capture analyzer
@@ -207,6 +221,12 @@ class PacketCapture:
except (PermissionError, OSError):
pass
del self._capture_reader_process[link]
if link in self._capture_stream_thread:
log.debug("Stopping packet capture stream thread for link {}".format(link.link_id()))
self._capture_stream_thread[link].stop()
self._capture_stream_thread[link].wait()
self._capture_stream_thread[link].error_signal.disconnect()
del self._capture_stream_thread[link]
# PCAP capture file path
command = command.replace("%c", '"' + capture_file_path + '"')
@@ -227,6 +247,15 @@ class PacketCapture:
if "|" in command:
# live traffic capture (using tail)
command1, command2 = command.split("|", 1)
if '<internal_tail>' in command1:
# Start the background capture streaming thread if using the internal tail implementation
log.debug("Starting background capture streaming thread for link {}".format(link.link_id()))
self._capture_stream_thread[link] = PCAPToWireshark(capture_file_path, command2)
self._capture_stream_thread[link].error_signal.connect(lambda msg: QtWidgets.QMessageBox.critical(self.parent(), "Packet capture", msg))
self._capture_stream_thread[link].start()
return
info = None
if sys.platform.startswith("win"):
# hide tail window on Windows

79
gns3/pcap_to_wireshark.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2026 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import time
import shlex
import subprocess
from .qt import QtCore
class PCAPToWireshark(QtCore.QThread):
error_signal = QtCore.pyqtSignal(str)
def __init__(self, pcap_path, wireshark_cmd):
super().__init__()
self._pcap_path = pcap_path
self._wireshark_cmd = wireshark_cmd
self._running = True
self._wireshark_proc = None
def run(self):
if not os.path.exists(self._pcap_path):
self.error_signal.emit(f"Error: {self._pcap_path} not found.")
return
try:
wireshark_cmd = shlex.split(self._wireshark_cmd)
except ValueError as e:
self.error_signal.emit(f"Invalid Wireshark command {self._wireshark_cmd}: {str(e)}")
return
try:
self._wireshark_proc = subprocess.Popen(
wireshark_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except FileNotFoundError:
self.error_signal.emit("Error: Wireshark not found in $PATH")
return
with open(self._pcap_path, 'rb') as f:
while self._running and self._wireshark_proc.poll() is None:
chunk = f.read(4096)
if chunk:
try:
self._wireshark_proc.stdin.write(chunk)
self._wireshark_proc.stdin.flush()
except BrokenPipeError:
break # Wireshark has been closed
else:
time.sleep(0.1)
def stop(self):
self._running = False
if self._wireshark_proc:
try:
self._wireshark_proc.stdin.close()
self._wireshark_proc.kill()
except Exception as e:
print(e)
pass

View File

@@ -600,7 +600,15 @@ class Project(QtCore.QObject):
Delete the project from all servers
"""
self.project_about_to_close_signal.emit()
Controller.instance().delete("/projects/{project_id}".format(project_id=self._id), self._projectClosedCallback, progressText="Delete the project")
Controller.instance().delete("/projects/{project_id}".format(project_id=self._id), self._projectDestroyedCallback, progressText="Delete the project")
def _projectDestroyedCallback(self, result, error=False, server=None, **kwargs):
self._projectClosedCallback(result, error=error, server=server, **kwargs)
if not error or ("status" in result and result["status"] == 404):
# Refresh the controller's project list so a deleted project disappears
# immediately from menus such as "recent projects" instead of only
# after restarting the GUI.
Controller.instance().refreshProjectList()
def _projectClosedCallback(self, result, error=False, server=None, **kwargs):

View File

@@ -154,6 +154,7 @@ elif sys.platform.startswith("darwin"):
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Xterm': 'xterm -T "{name}" -e "telnet {host} {port}"',
'Putty': 'putty -telnet {host} {port} -title "{name}" -sl 2500 -fg SALMON1 -bg BLACK',
'Ptyxis': 'ptyxis --tab --title="{name}" -- telnet {host} {port}',
'Gnome Terminal': 'gnome-terminal --tab -t "{name}" -- telnet {host} {port}',
'Xfce4 Terminal': 'xfce4-terminal --tab -T "{name}" -e "telnet {host} {port}"',
'ROXTerm': 'roxterm -n "{name}" --tab -e "telnet {host} {port}"',
@@ -172,6 +173,8 @@ else:
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "Linux Mint":
if shutil.which("mate-terminal"):
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Mate Terminal"]
elif shutil.which("ptyxis"):
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Ptyxis"]
else:
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
@@ -245,30 +248,40 @@ else:
# Pre-configured packet capture reader commands on various OSes
WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL = "Wireshark Live Traffic Capture (with internal tail)"
if sys.platform.startswith("win"):
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: r'{}\Wireshark\wireshark.exe {{pcap_file}} --capture-comment "{{project}} {{link_description}}"'.format(program_files),
WIRESHARK_LIVE_TRAFFIC_CAPTURE: r'tail.exe -f -c +0b {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files)}
WIRESHARK_LIVE_TRAFFIC_CAPTURE: r'tail.exe -f -c +0b {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files),
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: r'<internal_tail> {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files)}
elif sys.platform.startswith("darwin"):
# Mac OS X
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: '/usr/bin/open -a /Applications/Wireshark.app {pcap_file} --capture-comment {project} {link_description}"',
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
elif sys.platform.startswith("freebsd"):
# FreeBSD
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'gtail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'gtail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
elif sys.platform.startswith("openbsd"):
# OpenBSD
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
else:
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
if sys.platform.startswith("linux"):
# only use the internal live traffic capture version on Linux by default
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL]
else:
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = ""
if sys.platform.startswith("win"):

View File

@@ -23,8 +23,8 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.60.dev1"
__version_info__ = (2, 2, 60, 99)
__version__ = "2.2.61"
__version_info__ = (2, 2, 61, 0)
if "dev" in __version__:
try: