Compare commits

...

48 Commits

Author SHA1 Message Date
grossmj
3527e5551c Release v2.2.42 2023-08-09 21:11:57 +10:00
grossmj
72960f8f2b Enable system certificate store later in the code and bump version to 2.2.42.dev4 2023-08-08 17:22:27 +10:00
grossmj
8abb502c72 Use the system's certificate store for SSL connections 2023-08-07 21:33:25 +10:00
grossmj
08c729e83a Upgrade dependencies 2023-08-06 20:39:27 +10:00
grossmj
aac004bd2f Use certifi to get SSL root certificates 2023-08-06 20:37:10 +10:00
grossmj
70677d8f18 Bump version to 2.2.42.dev3 2023-08-06 18:14:22 +10:00
Jeremy Grossmann
fba1ff4208 Merge pull request #3502 from GNS3/use-bundled-cacert
Use bundled cacert file for frozen app
2023-08-05 22:37:08 +10:00
grossmj
e4edbefc23 Use bundled cacert file on Windows and macOS 2023-08-05 22:21:08 +10:00
grossmj
d93f9afe74 Bump version to 2.2.42.dev2 2023-08-05 20:16:30 +10:00
grossmj
162d197e36 Give a node some time to start before opening the console (for console auto start). Fixes #3474 2023-08-02 11:08:58 +10:00
grossmj
5c21dd8a2f Merge branch 'master' into 2.2 2023-08-01 15:50:51 +10:00
Jeremy Grossmann
aa9b9d3b0b Merge pull request #3364 from AbdelbakiBoukerche/feature_rounded_rectangle
Rounded Rectangle
2023-08-01 15:46:32 +10:00
grossmj
eae9eec15b Support for horizontal and vertical corner radius 2023-08-01 15:34:30 +10:00
Jeremy Grossmann
a3bf832721 Merge branch 'master' into feature_rounded_rectangle 2023-07-31 18:34:50 +10:00
Jeremy Grossmann
67890d74d9 Merge pull request #3500 from GNS3/fix/3449
Support for gnome-terminal tabs to be opened in the same window
2023-07-31 18:30:22 +10:00
grossmj
ab4325f951 Remove warning to set open new terminals in tabs for gnome-terminal 2023-07-31 12:32:49 +10:00
grossmj
2a947b9cc5 Add comments for gnome-terminal special case 2023-07-31 01:44:39 +10:00
grossmj
601c082288 Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint. 2023-07-31 01:40:20 +10:00
grossmj
7701d57bd0 Check that gnome-terminal is configured to open new terminals in tabs. 2023-07-31 01:34:59 +10:00
grossmj
f0b4148a20 Support for gnome-terminal tabs to be opened in the same window. 2023-07-30 22:15:38 +10:00
grossmj
2fdcbafbc1 Revert "Support for Python 3.12"
This reverts commit 5d82cea935.
2023-07-30 17:50:21 +10:00
grossmj
5d82cea935 Support for Python 3.12 2023-07-30 17:48:30 +10:00
grossmj
b0e3e93c41 Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498 2023-07-30 17:42:52 +10:00
grossmj
535f53737d Merge remote-tracking branch 'origin/master' into 2.2 2023-07-23 12:35:21 +10:00
Jeremy Grossmann
354f3eecec Merge pull request #3497 from kevinchevreuil/master
Add import sys in sudo.py
2023-07-23 12:04:29 +10:00
Kevin Chevreuil - Kaisen
35a6a5c8c7 Add import sys in sudo.py 2023-07-22 23:12:34 +02:00
grossmj
23cba0a28d Development on 2.2.42.dev1 2023-07-12 18:26:26 +10:00
Jeremy Grossmann
9c3d7bc95a Merge pull request #3495 from GNS3/release-v2.2.41
Release v2.2.41
2023-07-12 18:24:05 +10:00
grossmj
cf2802b15a Release v2.2.41 2023-07-12 17:07:39 +10:00
grossmj
b162c55078 Merge remote-tracking branch 'origin/master' into 2.2 2023-07-12 17:03:50 +10:00
grossmj
bb42b0ed0b Change chown in authorize_ubridge.py 2023-07-12 13:28:24 +10:00
grossmj
e108b5194d Bump version to 2.2.41.dev3 2023-07-12 13:26:22 +10:00
grossmj
e9ef8735be Use a small executable to set the correct permissions on uBridge on macOS 2023-07-12 12:40:53 +10:00
grossmj
e8e90bb16a Add debugging for AuthorizationExecuteWithPrivileges 2023-07-11 21:23:15 +10:00
grossmj
49f77930f4 Use alternative method to setuid uBridge on macOS 2023-07-11 18:27:19 +10:00
Jeremy Grossmann
4f32619ed8 Merge pull request #3493 from GNS3/remove-analytics
Remove sending usage stats
2023-07-09 19:04:40 +10:00
grossmj
20740748c1 Remove sending stats to GA 2023-07-09 18:55:31 +10:00
grossmj
e11ce27f7b Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483 2023-07-06 17:16:05 +10:00
Jeremy Grossmann
2987bcf91a Merge pull request #3489 from GNS3/backport-uefi-boot-mode
Backport UEFI boot mode support for Qemu VMs
2023-06-23 11:31:49 +09:30
grossmj
5e97bc0f86 Backport UEFI boot mode support for Qemu VMs 2023-06-23 11:18:25 +09:30
grossmj
e3a3de5df7 Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242 2023-06-20 14:49:25 +09:30
grossmj
e1693ce113 Developement on v2.2.41.dev2 2023-06-10 21:40:39 +09:30
Jeremy Grossmann
522091d219 Merge pull request #3481 from GNS3/release-v2.2.40.1
Release v2.2.40.1
2023-06-10 21:36:42 +09:30
grossmj
1446748934 Release v2.2.40.1 2023-06-10 20:06:40 +09:30
grossmj
3f0ce380e8 Merge branch 'master' into 2.2 2023-06-10 17:22:50 +09:30
grossmj
bd71383354 Development on v2.2.41.dev1 2023-06-06 12:43:20 +09:30
Jeremy Grossmann
9649895378 Merge pull request #3479 from GNS3/release-v2.2.40
Release v2.2.40
2023-06-06 12:41:46 +09:30
Abdelbaki Boukerche
181bf3f360 Rounded Rectangle 2022-08-04 18:20:54 +01:00
33 changed files with 394 additions and 288 deletions

View File

@@ -1,5 +1,27 @@
# Change Log
## 2.2.42 09/08/2023
* Use the system's certificate store for SSL connections
* Give a node some time to start before opening the console (for console auto start). Fixes #3474
* Use Mate Terminal by default if installed on Debian, Ubuntu and Linux Mint.
* Support for gnome-terminal tabs to be opened in the same window.
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
* Add import sys in sudo.py
* Rounded Rectangle support
## 2.2.41 12/07/2023
* Use alternative method to set the correct permissions for uBridge on macOS
* Remove sending stats to GA
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
* Backport UEFI boot mode support for Qemu VMs
* Add debug for dropEvent. Ref https://github.com/GNS3/gns3-server/issues/2242
## 2.2.40.1 10/06/2023
* No changes
## 2.2.40 06/06/2023
* Change log messages for Websocket errors

View File

@@ -15,12 +15,6 @@
# 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 sys
import os
import platform
import struct
import distro
try:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
@@ -29,7 +23,12 @@ except ImportError:
# Sentry SDK is not installed with deb package in order to simplify packaging
SENTRY_SDK_AVAILABLE = False
from .utils.get_resource import get_resource
import sys
import os
import platform
import struct
import distro
from .version import __version__, __version_info__
import logging
@@ -51,7 +50,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://60c5c2bb187449cb8c56478e86a4c516@o19455.ingest.sentry.io/38506"
DSN = "https://bae0411a1718612ee8c25cdb12ec7f02@o19455.ingest.sentry.io/38506"
_instance = None
def __init__(self):
@@ -64,22 +63,16 @@ class CrashReport:
self._sentry_initialized = False
if SENTRY_SDK_AVAILABLE:
cacert = None
if hasattr(sys, "frozen"):
cacert_resource = get_resource("cacert.pem")
if cacert_resource is not None and os.path.isfile(cacert_resource):
cacert = cacert_resource
else:
log.error("The SSL certificate bundle file '{}' could not be found".format(cacert_resource))
# Don't send log records as events.
sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None)
sentry_sdk.init(dsn=CrashReport.DSN,
release=__version__,
ca_certs=cacert,
default_integrations=False,
integrations=[sentry_logging])
try:
sentry_sdk.init(dsn=CrashReport.DSN,
release=__version__,
default_integrations=False,
integrations=[sentry_logging])
except Exception as e:
log.error("Crash report could not be sent: {}".format(e))
return
tags = {
"os:name": platform.system(),

View File

@@ -22,6 +22,7 @@ Style editor to edit Shape items.
from ..qt import QtCore, QtWidgets, QtGui
from ..ui.style_editor_dialog_ui import Ui_StyleEditorDialog
from ..items.shape_item import ShapeItem
from ..items.rectangle_item import RectangleItem
class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
@@ -70,6 +71,13 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
if isinstance(first_item, RectangleItem):
# use the horizontal corner radius first and then the vertical one if it's not set
# maybe we allow configuring them separately in the future
corner_radius = first_item.horizontalCornerRadius()
if not corner_radius:
corner_radius = first_item.verticalCornerRadius()
self.uiCornerRadiusSpinBox.setValue(corner_radius)
self.uiRotationSpinBox.setValue(int(first_item.rotation()))
self.uiBorderWidthSpinBox.setValue(pen.width())
index = self.uiBorderStyleComboBox.findData(pen.style())
@@ -116,10 +124,16 @@ class StyleEditorDialog(QtWidgets.QDialog, Ui_StyleEditorDialog):
for item in self._items:
item.setPen(pen)
# on multiselection it's possible to select many type of items
# on multi-selection it's possible to select many type of items
# but brush can be applied only on ShapeItem,
if brush and isinstance(item, ShapeItem):
item.setBrush(brush)
if isinstance(item, RectangleItem):
corner_radius = self.uiCornerRadiusSpinBox.value()
# use the corner radius for both horizontal (rx) and vertical (ry)
# maybe we support setting them separately in the future
item.setHorizontalCornerRadius(corner_radius)
item.setVerticalCornerRadius(corner_radius)
item.setRotation(self.uiRotationSpinBox.value())
def done(self, result):

View File

@@ -54,7 +54,9 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
self.uiColorLabel.hide()
self.uiColorPushButton.hide()
self._color = None
self.uiCornerRadiusLabel.hide()
self.uiCornerRadiusSpinBox.hide()
self.uiRotationLabel.hide()
self.uiRotationSpinBox.hide()

View File

@@ -713,6 +713,7 @@ class GraphicsView(QtWidgets.QGraphicsView):
:param event: QDropEvent instance
"""
log.debug("Drop event received with mime data: {}".format(event.mimeData().formats()))
# check if what has been dropped is handled by this view
if event.mimeData().hasFormat("application/x-gns3-template"):
template_id = event.mimeData().data("application/x-gns3-template").data().decode()

View File

@@ -32,8 +32,22 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
"""
def __init__(self, width=200, height=100, **kws):
self._rx = 0
self._ry = 0
super().__init__(width=width, height=height, **kws)
def setHorizontalCornerRadius(self, radius: int):
self._rx = radius
def horizontalCornerRadius(self):
return self._rx
def setVerticalCornerRadius(self, radius: int):
self._ry = radius
def verticalCornerRadius(self):
return self._ry
def paint(self, painter, option, widget=None):
"""
Paints the contents of an item in local coordinates.
@@ -43,7 +57,9 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
:param widget: QWidget instance
"""
super().paint(painter, option, widget)
painter.setPen(self.pen())
painter.setBrush(self.brush())
painter.drawRoundedRect(self.rect(), self._rx, self._ry)
self.drawLayerInfo(painter)
def toSvg(self):
@@ -57,7 +73,27 @@ class RectangleItem(QtWidgets.QGraphicsRectItem, ShapeItem):
rect = ET.SubElement(svg, "rect")
rect.set("width", str(int(self.rect().width())))
rect.set("height", str(int(self.rect().height())))
if self._rx:
rect.set("rx", str(self._rx))
if self._ry:
rect.set("ry", str(self._ry))
rect = self._styleSvg(rect)
return ET.tostring(svg, encoding="utf-8").decode("utf-8")
def fromSvg(self, svg):
svg_elem = ET.fromstring(svg)
if len(svg_elem):
# handle horizontal corner radius and vertical corner radius (specific to rectangles)
rx = svg_elem[0].get("rx")
ry = svg_elem[0].get("ry")
if rx:
self._rx = int(rx)
elif ry:
self._rx = int(ry) # defaults to ry if it is specified
if ry:
self._ry = int(ry)
elif rx:
self._ry = int(rx) # defaults to rx if it is specified
super().fromSvg(svg)

View File

@@ -173,7 +173,7 @@ class ShapeItem(DrawingItem):
def fromSvg(self, svg):
"""
Import element informations from an SVG
Import element information from SVG
"""
svg = ET.fromstring(svg)
width = float(svg.get("width", self.rect().width()))

View File

@@ -193,7 +193,11 @@ class LocalServer(QtCore.QObject):
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if proceed == QtWidgets.QMessageBox.Yes:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
from gns3.utils.macos_ubridge_setuid import macos_ubridge_setuid
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
macos_ubridge_setuid()
else:
sudo(["chown", "root:admin", path], ["chmod", "4750", path])
except OSError as e:
QtWidgets.QMessageBox.critical(self.parent(), "uBridge", "Can't set root permissions to uBridge {}: {}".format(path, str(e)))
return False

View File

@@ -60,12 +60,12 @@ from gns3.local_config import LocalConfig
from gns3.application import Application
from gns3.utils import parse_version
from gns3.dialogs.profile_select import ProfileSelectDialog
from gns3.version import __version__
import logging
log = logging.getLogger(__name__)
from gns3.version import __version__
def locale_check():
"""
@@ -135,6 +135,13 @@ def main():
if options.project:
options.project = os.path.abspath(options.project)
try:
import truststore
truststore.inject_into_ssl()
log.info("Using system certificate store for SSL connections")
except ImportError:
pass
if hasattr(sys, "frozen"):
# We add to the path where the OS search executable our binary location starting by GNS3
# packaged binary

View File

@@ -49,7 +49,6 @@ from .topology import Topology
from .http_client import HTTPClient
from .progress import Progress
from .update_manager import UpdateManager
from .utils.analytics import AnalyticsClient
from .dialogs.appliance_wizard import ApplianceWizard
from .dialogs.new_template_wizard import NewTemplateWizard
from .dialogs.notif_dialog import NotifDialog, NotifDialogHandler
@@ -133,7 +132,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self._local_config_timer = QtCore.QTimer(self)
self._local_config_timer.timeout.connect(local_config.checkConfigChanged)
self._local_config_timer.start(1000) # milliseconds
self._analytics_client = AnalyticsClient()
self._template_manager = TemplateManager().instance()
self._appliance_manager = ApplianceManager().instance()
@@ -378,13 +376,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.uiConsoleDockWidget.setFloating(False)
self.uiNodesDockWidget.setFloating(False)
def analyticsClient(self):
"""
Return the analytics client
"""
return self._analytics_client
def _newProjectActionSlot(self):
"""
Slot called to create a new project.
@@ -1144,8 +1135,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
progress.setCancelButtonText("Force quit")
log.debug("Close the Main Window")
self._analytics_client.sendScreenView("Main Window", session_start=False)
self._finish_application_closing(close_windows=False)
event.accept()
self.uiConsoleTextEdit.closeIO()
@@ -1234,7 +1223,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
Controller.instance().connected_signal.connect(self._controllerConnectedSlot)
Controller.instance().project_list_updated_signal.connect(self.updateRecentProjectActions)
self._analytics_client.sendScreenView("Main Window")
self.uiGraphicsView.setEnabled(False)
# show the setup wizard

View File

@@ -579,6 +579,7 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
self.uiQemuOptionsLineEdit.setText(settings["options"])
self.uiUsageTextEdit.setPlainText(settings["usage"])
self.uiTPMCheckBox.setChecked(settings["tpm"])
self.uiUEFICheckBox.setChecked(settings["uefi"])
def saveSettings(self, settings, node=None, group=False):
"""
@@ -694,4 +695,5 @@ class QemuVMConfigurationPage(QtWidgets.QWidget, Ui_QemuVMConfigPageWidget):
settings["options"] = self.uiQemuOptionsLineEdit.text()
settings["usage"] = self.uiUsageTextEdit.toPlainText()
settings["tpm"] = self.uiTPMCheckBox.isChecked()
settings["uefi"] = self.uiUEFICheckBox.isChecked()
return settings

View File

@@ -75,6 +75,7 @@ class QemuVM(Node):
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
"replicate_network_connection_state": QEMU_VM_SETTINGS["replicate_network_connection_state"],
"tpm": QEMU_VM_SETTINGS["tpm"],
"uefi": QEMU_VM_SETTINGS["uefi"],
"create_config_disk": QEMU_VM_SETTINGS["create_config_disk"],
"platform": QEMU_VM_SETTINGS["platform"],
"on_close": QEMU_VM_SETTINGS["on_close"],

View File

@@ -58,6 +58,7 @@ QEMU_VM_SETTINGS = {
"legacy_networking": False,
"replicate_network_connection_state": True,
"tpm": False,
"uefi": False,
"create_config_disk": False,
"on_close": "power_off",
"platform": "",

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>478</width>
<height>550</height>
<height>579</height>
</rect>
</property>
<property name="windowTitle">
@@ -867,13 +867,6 @@
<string>Additional settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="uiQemuOptionsLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="uiQemuOptionsLineEdit">
<property name="toolTip">
@@ -890,7 +883,7 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="uiBaseVMCheckBox">
<property name="enabled">
<bool>true</bool>
@@ -907,11 +900,26 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="uiQemuOptionsLabel">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="uiUEFICheckBox">
<property name="text">
<string>Enable UEFI boot mode</string>
</property>
</widget>
</item>
</layout>
<zorder>uiQemuOptionsLineEdit</zorder>
<zorder>uiQemuOptionsLabel</zorder>
<zorder>uiBaseVMCheckBox</zorder>
<zorder>uiTPMCheckBox</zorder>
<zorder>uiUEFICheckBox</zorder>
</widget>
</item>
<item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_QemuVMConfigPageWidget(object):
def setupUi(self, QemuVMConfigPageWidget):
QemuVMConfigPageWidget.setObjectName("QemuVMConfigPageWidget")
QemuVMConfigPageWidget.resize(478, 550)
QemuVMConfigPageWidget.resize(478, 579)
self.verticalLayout = QtWidgets.QVBoxLayout(QemuVMConfigPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiQemutabWidget = QtWidgets.QTabWidget(QemuVMConfigPageWidget)
@@ -428,23 +428,27 @@ class Ui_QemuVMConfigPageWidget(object):
self.groupBox.setObjectName("groupBox")
self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox)
self.gridLayout_3.setObjectName("gridLayout_3")
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
self.uiQemuOptionsLineEdit = QtWidgets.QLineEdit(self.groupBox)
self.uiQemuOptionsLineEdit.setObjectName("uiQemuOptionsLineEdit")
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 1, 1, 1)
self.uiBaseVMCheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiBaseVMCheckBox.setEnabled(True)
self.uiBaseVMCheckBox.setObjectName("uiBaseVMCheckBox")
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 2, 0, 1, 2)
self.gridLayout_3.addWidget(self.uiBaseVMCheckBox, 3, 0, 1, 2)
self.uiTPMCheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiTPMCheckBox.setObjectName("uiTPMCheckBox")
self.gridLayout_3.addWidget(self.uiTPMCheckBox, 1, 0, 1, 2)
self.uiQemuOptionsLabel = QtWidgets.QLabel(self.groupBox)
self.uiQemuOptionsLabel.setObjectName("uiQemuOptionsLabel")
self.gridLayout_3.addWidget(self.uiQemuOptionsLabel, 0, 0, 1, 1)
self.uiUEFICheckBox = QtWidgets.QCheckBox(self.groupBox)
self.uiUEFICheckBox.setObjectName("uiUEFICheckBox")
self.gridLayout_3.addWidget(self.uiUEFICheckBox, 2, 0, 1, 2)
self.uiQemuOptionsLineEdit.raise_()
self.uiQemuOptionsLabel.raise_()
self.uiBaseVMCheckBox.raise_()
self.uiTPMCheckBox.raise_()
self.uiUEFICheckBox.raise_()
self.verticalLayout_2.addWidget(self.groupBox)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem4)
@@ -549,7 +553,6 @@ class Ui_QemuVMConfigPageWidget(object):
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low"))
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low"))
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Additional settings"))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
self.uiQemuOptionsLineEdit.setToolTip(_translate("QemuVMConfigPageWidget", "<html><head/><body><p>Variable replacements:</p>\n"
"<ul>\n"
"<li>%vm-name% =VM name</li>\n"
@@ -562,5 +565,7 @@ class Ui_QemuVMConfigPageWidget(object):
"</body></html>"))
self.uiBaseVMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use as a linked base VM"))
self.uiTPMCheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable Trusted Platform Module (TPM)"))
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:"))
self.uiUEFICheckBox.setText(_translate("QemuVMConfigPageWidget", "Enable UEFI boot mode"))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiAdvancedSettingsTab), _translate("QemuVMConfigPageWidget", "Advanced"))
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.uiUsageTab), _translate("QemuVMConfigPageWidget", "Usage"))

View File

@@ -689,7 +689,8 @@ class Node(BaseNode):
return
super().setStatus(status)
if status == self.started and "console_auto_start" in self.settings() and self.settings()["console_auto_start"]:
self.openConsole()
# give the node some time to start before opening the console
QtCore.QTimer.singleShot(1000, self.openConsole)
def openConsole(self, command=None, aux=False):
"""

View File

@@ -301,7 +301,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
self.uiImagesPathLineEdit.setText(local_server["images_path"])
self.uiConfigsPathLineEdit.setText(local_server["configs_path"])
self.uiAppliancesPathLineEdit.setText(local_server["appliances_path"])
self.uiStatsCheckBox.setChecked(settings["send_stats"])
self.uiOverlayNotificationsCheckBox.setChecked(settings["overlay_notifications"])
self.uiCrashReportCheckBox.setChecked(local_server["report_errors"])
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
@@ -411,7 +410,6 @@ class GeneralPreferencesPage(QtWidgets.QWidget, Ui_GeneralPreferencesPageWidget)
"vnc_console_command": self.uiVNCConsoleCommandLineEdit.text(),
"spice_console_command": self.uiSPICEConsoleCommandLineEdit.text(),
"delay_console_all": self.uiDelayConsoleAllSpinBox.value(),
"send_stats": self.uiStatsCheckBox.isChecked(),
"multi_profiles": self.uiMultiProfilesCheckBox.isChecked(),
"direct_file_upload": self.uiDirectFileUpload.isChecked()
}

View File

@@ -195,54 +195,6 @@ if hasattr(sys, '_called_from_test'):
QtCore.pyqtSignal = FakeQtSignal
class StatsQtWidgetsQWizard(QtWidgets.QWizard):
"""
Send stats from all the QWizard
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QWizard = StatsQtWidgetsQWizard
class StatsQtWidgetsQMainWindow(QtWidgets.QMainWindow):
"""
Send stats from all the QMainWindow
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QMainWindow = StatsQtWidgetsQMainWindow
class StatsQtWidgetsQDialog(QtWidgets.QDialog):
"""
Send stats from all the QWizard
"""
def __init__(self, *args):
super().__init__(*args)
from ..utils.analytics import AnalyticsClient
name = self.__class__.__name__
name = re.sub(r"([A-Z])", r" \1", name).strip()
AnalyticsClient.instance().sendScreenView(name)
QtWidgets.QDialog = StatsQtWidgetsQDialog
def qpartial(func, *args, **kwargs):
"""
A functools partial that you can use on qobject. If the targeted qobject is

View File

@@ -24,6 +24,7 @@ from ..qt import QtCore, QtWidgets, QtNetwork
from ..controller import Controller
from .config import Config, ConfigException
import logging
log = logging.getLogger(__name__)

View File

@@ -354,6 +354,10 @@
"type": "boolean",
"title": "Enable the Trusted Platform Module (TPM)"
},
"uefi": {
"type": "boolean",
"title": "Enable the UEFI boot mode"
},
"on_close": {
"title": "Action to execute on the VM is closed",
"enum": ["power_off", "shutdown_signal", "save_vm_state"]

View File

@@ -152,12 +152,12 @@ elif sys.platform.startswith("darwin"):
else:
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Xterm': 'xterm -T "%d" -e "telnet %h %p"',
'Putty': 'putty -telnet %h %p -title "%d" -sl 2500 -fg SALMON1 -bg BLACK',
'Gnome Terminal': 'gnome-terminal -t "%d" -e "telnet %h %p"',
'Gnome Terminal': 'gnome-terminal --tab -t "%d" -- telnet %h %p',
'Xfce4 Terminal': 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"',
'ROXTerm': 'roxterm -n "%d" --tab -e "telnet %h %p"',
'KDE Konsole': 'konsole --new-tab -p tabtitle="%d" -e "telnet %h %p"',
'SecureCRT': 'SecureCRT /T /N "%d" /TELNET %h %p',
'Mate Terminal': 'mate-terminal --tab -e "telnet %h %p" -t "%d"',
'Mate Terminal': 'mate-terminal --tab -e "telnet %h %p" -t "%d"',
'terminator': 'terminator -e "telnet %h %p" -T "%d"',
'urxvt': 'urxvt -title %d -e telnet %h %p',
'kitty': 'kitty -T %d telnet %h %p'}
@@ -168,7 +168,10 @@ else:
if sys.platform.startswith("linux"):
distro_name = distro.name()
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "LinuxMint":
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
if shutil.which("mate-terminal"):
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Mate Terminal"]
else:
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
# Pre-configured VNC console commands on various OSes
if sys.platform.startswith("win"):
@@ -284,7 +287,6 @@ GENERAL_SETTINGS = {
"check_for_update": True,
"overlay_notifications": True,
"experimental_features": False,
"send_stats": True,
"stats_visitor_id": str(uuid.uuid4()), # An anonymous id for stats
"last_check_for_update": 0,
"telnet_console_command": DEFAULT_TELNET_CONSOLE_COMMAND,

View File

@@ -25,6 +25,8 @@ import os
import sys
import shlex
import subprocess
import psutil
from .main_window import MainWindow
from .controller import Controller
@@ -34,6 +36,39 @@ log = logging.getLogger(__name__)
console_mutex = QtCore.QMutex()
def gnome_terminal_env():
uid = os.getuid()
# get list of processes of current user
procs = [p.info for p in psutil.process_iter(
attrs=['name', 'pid', 'ppid', 'create_time', 'uids']
) if p.info['uids'].real == uid]
# get pid of gnome-terminal-server process
gnome_terminal_server_pid = [p['pid'] for p in procs if p['name'] == "gnome-terminal-server"]
if not gnome_terminal_server_pid:
return {}
gnome_terminal_server_pid = gnome_terminal_server_pid[0]
# get subprocesses of gnome-terminal-server
gnome_terminal_server_children = [p for p in procs if p['ppid'] == gnome_terminal_server_pid]
gnome_terminal_server_children.sort(key=lambda p: p['create_time'], reverse=True)
# return the gnome-terminal environment variables of the first subprocess named telnet
for proc in gnome_terminal_server_children:
if proc['name'] == "telnet":
try:
env = psutil.Process(proc['pid']).environ()
if 'GNOME_TERMINAL_SERVICE' in env and \
'GNOME_TERMINAL_SCREEN' in env:
return {'GNOME_TERMINAL_SERVICE': env['GNOME_TERMINAL_SERVICE'],
'GNOME_TERMINAL_SCREEN': env['GNOME_TERMINAL_SCREEN']}
except psutil.Error:
pass
return {}
class ConsoleThread(QtCore.QThread):
consoleError = QtCore.pyqtSignal(str)
@@ -60,7 +95,14 @@ class ConsoleThread(QtCore.QThread):
except ValueError:
self.consoleError.emit("Syntax error in command: '{}'".format(command))
return
subprocess.call(args, env=os.environ)
env = os.environ.copy()
# special case to force gnome-terminal to correctly use tabs on Linux
if sys.platform.startswith("linux") and "gnome-terminal" in args[0] and "--tab" in command:
# inject gnome-terminal environment variables
if "GNOME_TERMINAL_SERVICE" not in env or "GNOME_TERMINAL_SCREEN" not in env:
env.update(gnome_terminal_env())
subprocess.call(args, env=env)
def run(self):

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>941</width>
<height>910</height>
<width>512</width>
<height>652</height>
</rect>
</property>
<property name="windowTitle">
@@ -223,7 +223,16 @@
<string>Binary images</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -373,7 +382,16 @@
<string>Console applications</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -474,7 +492,16 @@
<string>VNC</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -943,7 +970,16 @@
<string>Miscellaneous</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
@@ -966,16 +1002,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiStatsCheckBox">
<property name="text">
<string>Send anonymous usage statistics</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="uiOverlayNotificationsCheckBox">
<property name="text">
@@ -1098,7 +1124,6 @@
<tabstop>uiDefaultNoteColorPushButton</tabstop>
<tabstop>uiCheckForUpdateCheckBox</tabstop>
<tabstop>uiCrashReportCheckBox</tabstop>
<tabstop>uiStatsCheckBox</tabstop>
<tabstop>uiOverlayNotificationsCheckBox</tabstop>
<tabstop>uiExperimentalFeaturesCheckBox</tabstop>
<tabstop>uiHdpiCheckBox</tabstop>

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/general_preferences_page.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GeneralPreferencesPageWidget(object):
def setupUi(self, GeneralPreferencesPageWidget):
GeneralPreferencesPageWidget.setObjectName("GeneralPreferencesPageWidget")
GeneralPreferencesPageWidget.resize(941, 910)
GeneralPreferencesPageWidget.resize(512, 652)
self.verticalLayout = QtWidgets.QVBoxLayout(GeneralPreferencesPageWidget)
self.verticalLayout.setObjectName("verticalLayout")
self.uiMiscTabWidget = QtWidgets.QTabWidget(GeneralPreferencesPageWidget)
@@ -446,10 +447,6 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiCrashReportCheckBox.setChecked(True)
self.uiCrashReportCheckBox.setObjectName("uiCrashReportCheckBox")
self.verticalLayout_2.addWidget(self.uiCrashReportCheckBox)
self.uiStatsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiStatsCheckBox.setChecked(True)
self.uiStatsCheckBox.setObjectName("uiStatsCheckBox")
self.verticalLayout_2.addWidget(self.uiStatsCheckBox)
self.uiOverlayNotificationsCheckBox = QtWidgets.QCheckBox(self.uiMiscTab)
self.uiOverlayNotificationsCheckBox.setObjectName("uiOverlayNotificationsCheckBox")
self.verticalLayout_2.addWidget(self.uiOverlayNotificationsCheckBox)
@@ -520,8 +517,7 @@ class Ui_GeneralPreferencesPageWidget(object):
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteFontPushButton, self.uiDefaultNoteColorPushButton)
GeneralPreferencesPageWidget.setTabOrder(self.uiDefaultNoteColorPushButton, self.uiCheckForUpdateCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCheckForUpdateCheckBox, self.uiCrashReportCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiStatsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiStatsCheckBox, self.uiOverlayNotificationsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiCrashReportCheckBox, self.uiOverlayNotificationsCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiOverlayNotificationsCheckBox, self.uiExperimentalFeaturesCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiExperimentalFeaturesCheckBox, self.uiHdpiCheckBox)
GeneralPreferencesPageWidget.setTabOrder(self.uiHdpiCheckBox, self.uiMultiProfilesCheckBox)
@@ -601,7 +597,6 @@ class Ui_GeneralPreferencesPageWidget(object):
self.uiMiscTabWidget.setTabText(self.uiMiscTabWidget.indexOf(self.uiSceneTab), _translate("GeneralPreferencesPageWidget", "Topology view"))
self.uiCheckForUpdateCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Automatically check for update"))
self.uiCrashReportCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous crash reports"))
self.uiStatsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Send anonymous usage statistics"))
self.uiOverlayNotificationsCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Display error, warning and info in an overlay popup"))
self.uiExperimentalFeaturesCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable experimental features (dangerous, restart required)"))
self.uiHdpiCheckBox.setText(_translate("GeneralPreferencesPageWidget", "Enable HDPI mode (this may crash on Linux, restart required)"))

View File

@@ -2,6 +2,14 @@
<ui version="4.0">
<class>StyleEditorDialog</class>
<widget class="QDialog" name="StyleEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>270</width>
<height>294</height>
</rect>
</property>
<property name="windowTitle">
<string>Style editor</string>
</property>
@@ -76,14 +84,14 @@
<item row="3" column="1">
<widget class="QComboBox" name="uiBorderStyleComboBox"/>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="uiRotationLabel">
<property name="text">
<string>Rotation:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="uiRotationSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -106,6 +114,23 @@ editing (notes only) with ALT and '+' (or P) / ALT and '-' (or M)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="uiCornerRadiusLabel">
<property name="text">
<string>Corner radius:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="uiCornerRadiusSpinBox">
<property name="suffix">
<string>°</string>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/style_editor_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,6 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_StyleEditorDialog(object):
def setupUi(self, StyleEditorDialog):
StyleEditorDialog.setObjectName("StyleEditorDialog")
StyleEditorDialog.resize(270, 294)
StyleEditorDialog.setModal(True)
self.verticalLayout = QtWidgets.QVBoxLayout(StyleEditorDialog)
self.verticalLayout.setObjectName("verticalLayout")
@@ -52,7 +53,7 @@ class Ui_StyleEditorDialog(object):
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.uiBorderStyleComboBox)
self.uiRotationLabel = QtWidgets.QLabel(self.uiStyleSettingsGroupBox)
self.uiRotationLabel.setObjectName("uiRotationLabel")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.uiRotationLabel)
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.uiRotationLabel)
self.uiRotationSpinBox = QtWidgets.QSpinBox(self.uiStyleSettingsGroupBox)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -62,7 +63,14 @@ class Ui_StyleEditorDialog(object):
self.uiRotationSpinBox.setMinimum(-360)
self.uiRotationSpinBox.setMaximum(360)
self.uiRotationSpinBox.setObjectName("uiRotationSpinBox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.uiRotationSpinBox)
self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.uiRotationSpinBox)
self.uiCornerRadiusLabel = QtWidgets.QLabel(self.uiStyleSettingsGroupBox)
self.uiCornerRadiusLabel.setObjectName("uiCornerRadiusLabel")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.uiCornerRadiusLabel)
self.uiCornerRadiusSpinBox = QtWidgets.QSpinBox(self.uiStyleSettingsGroupBox)
self.uiCornerRadiusSpinBox.setMaximum(100)
self.uiCornerRadiusSpinBox.setObjectName("uiCornerRadiusSpinBox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.uiCornerRadiusSpinBox)
self.verticalLayout.addWidget(self.uiStyleSettingsGroupBox)
self.uiButtonBox = QtWidgets.QDialogButtonBox(StyleEditorDialog)
self.uiButtonBox.setOrientation(QtCore.Qt.Horizontal)
@@ -73,8 +81,8 @@ class Ui_StyleEditorDialog(object):
self.verticalLayout.addItem(spacerItem)
self.retranslateUi(StyleEditorDialog)
self.uiButtonBox.accepted.connect(StyleEditorDialog.accept)
self.uiButtonBox.rejected.connect(StyleEditorDialog.reject)
self.uiButtonBox.accepted.connect(StyleEditorDialog.accept) # type: ignore
self.uiButtonBox.rejected.connect(StyleEditorDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(StyleEditorDialog)
def retranslateUi(self, StyleEditorDialog):
@@ -90,4 +98,6 @@ class Ui_StyleEditorDialog(object):
self.uiRotationSpinBox.setToolTip(_translate("StyleEditorDialog", "Rotation can be ajusted on the scene for a selected item while\n"
"editing (notes only) with ALT and \'+\' (or P) / ALT and \'-\' (or M)"))
self.uiRotationSpinBox.setSuffix(_translate("StyleEditorDialog", "°"))
self.uiCornerRadiusLabel.setText(_translate("StyleEditorDialog", "Corner radius:"))
self.uiCornerRadiusSpinBox.setSuffix(_translate("StyleEditorDialog", "°"))
from . import resources_rc

View File

@@ -24,7 +24,6 @@ import re
from gns3.utils import parse_version
from gns3 import version
from gns3.qt import QtNetwork, QtCore, QtWidgets, QtGui, qslot
from gns3.local_config import LocalConfig

View File

@@ -1,134 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 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 platform
import sys
from datetime import datetime
from urllib.parse import quote
from ..version import __version__
from ..qt import QtCore, QtNetwork, QtWidgets
from ..local_config import LocalConfig
from ..settings import GENERAL_SETTINGS
import logging
log = logging.getLogger(__name__)
class AnalyticsClient(QtCore.QObject):
"""
Google analytics client to send events.
"""
_property_id = "UA-55817127-3"
def __init__(self):
super().__init__()
self._visitor_id = None
self._manager = QtNetwork.QNetworkAccessManager(self)
def finished(network_reply):
try:
error = network_reply.error()
except TypeError:
# For unknow reason sometimes error is transform to a signal
# we receive few crash report about that, but we are not able
# to reproduce. We suspect the problem happen when the
# application is closing.
#
# https://github.com/GNS3/gns3-gui/issues/2011
return
if error != QtNetwork.QNetworkReply.NoError:
log.debug("Error when pushing to Google Analytics %s", network_reply.errorString())
self._manager.finished.connect(finished)
#
# We need to build a user agent for Universal Analytics in order to
# let analytics guess the OS
# this could break by analytics at anytime :(
if sys.platform.startswith("darwin"):
self._user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X {release}) AppleWebKit/537.36 (KHTML, like Gecko) GNS3/{version}".format(release=platform.mac_ver()[0].replace(".", "_"), version=__version__)
elif sys.platform.startswith("win"):
self._user_agent = "Mozilla/5.0 (Windows NT {release}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(release=platform.release(), version=__version__)
else:
self._user_agent = "Mozilla/5.0 (X11; Linux {arch}) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 GNS3/{version}".format(arch=platform.machine(), version=__version__)
self._rate_limit = {}
def sendScreenView(self, screen, session_start=None):
"""
:params session_start: True session start, None during session, False session stop
"""
if session_start is not False and screen in self._rate_limit:
if self._rate_limit[screen] + 60 * 1 > datetime.utcnow().timestamp():
log.debug("Ignore call %s to Google Analytics because of rate limiting", screen)
return
self._rate_limit[screen] = datetime.utcnow().timestamp()
settings = LocalConfig.instance().loadSectionSettings("MainWindow", GENERAL_SETTINGS)
if settings["send_stats"] is False:
log.debug("Stats is turn off ignore call %s", screen)
return
body = "v=1" # Version
body += "&tid={}".format(self._property_id) # Tracking ID / Property ID
body += "&cid={}".format(settings["stats_visitor_id"]) # Anonymous Client ID
body += "&aip=1" # Anonymize IP
body += "&t=screenview" # Screenview hit type
body += "&an=GNS3" # App name
body += "&av={}".format(quote(__version__)) # App version.
body += "&ua={}".format(quote(self._user_agent)) # User agent
body += "&cd={}".format(quote(screen)) # Category
body += "&ds=gns3-gui" # Data source
if session_start is True:
body += "&sc=start" # Session start
elif session_start is False:
body += "&sc=end" # Session end
screen = QtWidgets.QApplication.desktop().screenGeometry()
body += "&sr={}x{}".format(screen.width(), screen.height()) # Screen resolution
locale = QtCore.QLocale.system().name().lower()
if locale:
body += "&ul={}".format(locale) # User language
# TODO: HTTPS when possible because it's broken for the moment with Qt on OSX:
# https://bugreports.qt.io/browse/QTBUG-45487
if sys.platform.startswith("darwin"):
url = QtCore.QUrl('http://www.google-analytics.com/collect')
else:
url = QtCore.QUrl('https://www.google-analytics.com/collect')
request_qt = QtNetwork.QNetworkRequest(url)
request_qt.setRawHeader(b"Content-Type", b"application/x-www-form-urlencoded")
request_qt.setRawHeader(b"User-Agent", self._user_agent.encode())
self._manager.post(request_qt, body.encode())
log.debug("Send stats to Google Analytics: %s", body)
@staticmethod
def instance():
"""
Singleton to return only on instance of AnalyticsClient.
:returns: instance of AnalyticsClient
"""
if not hasattr(AnalyticsClient, '_instance') or AnalyticsClient._instance is None:
AnalyticsClient._instance = AnalyticsClient()
return AnalyticsClient._instance

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 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/>.
# This script is intended to be built as a small executable for macOS to set the correct permissions on uBridge
import os
import shutil
import sys
def authorize_ubridge():
path = shutil.which("ubridge", path=os.path.dirname(sys.executable))
if path is None:
raise SystemExit("Could not find ubridge executable at {}".format(path))
try:
shutil.chown(path, "root", "admin")
os.chmod(path, 0o4750)
except OSError as e:
raise SystemExit("Could not authorize {}: {}".format(path, str(e)))
if __name__ == '__main__':
authorize_ubridge()

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 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 sys
import shutil
import logging
log = logging.getLogger(__name__)
def macos_ubridge_setuid():
# AuthorizationExecuteWithPrivileges() has been deprecated since macOS 10.7 but it still works
# and much simpler than using SMJobBless() which requires a separate helper tool
import ctypes
import ctypes.util
from ctypes import byref
authorize_ubridge = shutil.which("authorize_ubridge", path=os.path.dirname(sys.executable))
if authorize_ubridge is None:
raise OSError("Could not find the authorize_ubridge executable")
# https://developer.apple.com/documentation/security
sec = ctypes.cdll.LoadLibrary(ctypes.util.find_library("Security"))
try:
sec.AuthorizationCreate
except AttributeError:
raise OSError("macOS security library does not support AuthorizationCreate")
kAuthorizationFlagDefaults = 0
auth = ctypes.c_void_p()
r_auth = byref(auth)
err = sec.AuthorizationCreate(None, None, kAuthorizationFlagDefaults, r_auth)
if err:
raise OSError("Could not create authorization: {}".format(err))
exe = [authorize_ubridge]
log.info("Executing '{}' with privileges".format(exe))
args = (ctypes.c_char_p * len(exe))()
for i, arg in enumerate(exe[1:]):
args[i] = arg.encode('utf8')
io = ctypes.c_void_p()
err = sec.AuthorizationExecuteWithPrivileges(auth, exe[0].encode('utf8'), 0, args, byref(io))
if err:
raise OSError("Could not authorize uBridge: {}".format(err))
else:
log.info("Successfully authorized uBridge")

View File

@@ -15,9 +15,9 @@
# 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 sys
import shlex
import subprocess
import sys
from gns3.qt import QtWidgets
from gns3.utils.progress_dialog import ProgressDialog

View File

@@ -23,9 +23,8 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.40"
__version_info__ = (2, 2, 40, 0)
__version__ = "2.2.42"
__version_info__ = (2, 2, 42, 0)
if "dev" in __version__:
try:
import os

View File

@@ -1,7 +1,8 @@
jsonschema>=4.17.3,<4.18; python_version >= '3.7'
jsonschema==3.2.0; python_version < '3.7' # v3.2.0 is the last version to support Python 3.6
sentry-sdk==1.17.0,<1.18
psutil==5.9.4
sentry-sdk==1.29.2,<1.30
psutil==5.9.5
distro>=1.8.0
truststore>=0.7.0; python_version >= '3.10'
setuptools>=60.8.1; python_version >= '3.7'
setuptools==59.6.0; python_version < '3.7' # v59.6.0 is the last version to support Python 3.6