mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-02-15 04:41:50 +00:00
QML: Backend service is fully merged with the existing code
This commit is contained in:
parent
7307a1111c
commit
9fe8e36d22
2 changed files with 323 additions and 13 deletions
16
main.qml
16
main.qml
|
@ -134,6 +134,13 @@ ApplicationWindow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
// Material.background: Material.Teal
|
// Material.background: Material.Teal
|
||||||
// Material.foreground: Material.Grey
|
// Material.foreground: Material.Grey
|
||||||
|
onClicked: {
|
||||||
|
if (!backend.virtScreenCreated) {
|
||||||
|
backend.createVirtScreen();
|
||||||
|
} else {
|
||||||
|
backend.deleteVirtScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +194,13 @@ ApplicationWindow {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
// Material.background: Material.Teal
|
// Material.background: Material.Teal
|
||||||
// Material.foreground: Material.Grey
|
// Material.foreground: Material.Grey
|
||||||
|
onClicked: {
|
||||||
|
if (backend.vncState == 'Off') {
|
||||||
|
backend.startVNC()
|
||||||
|
} else {
|
||||||
|
backend.stopVNC()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +243,7 @@ ApplicationWindow {
|
||||||
menu: Labs.Menu {
|
menu: Labs.Menu {
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: qsTr("&Quit")
|
text: qsTr("&Quit")
|
||||||
onTriggered: Qt.quit()
|
onTriggered: backend.quitProgram()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
320
virtscreen.py
320
virtscreen.py
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import sys, os
|
import sys, os, subprocess, signal, re, atexit
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
@ -9,14 +9,213 @@ from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
|
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
|
||||||
|
|
||||||
|
from twisted.internet import protocol, error
|
||||||
|
from netifaces import interfaces, ifaddresses, AF_INET
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# file path definitions
|
# file path definitions
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
|
HOME_PATH = os.getenv('HOME', None)
|
||||||
|
if HOME_PATH is not None:
|
||||||
|
HOME_PATH = HOME_PATH + "/.virtscreen"
|
||||||
|
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
|
||||||
|
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
|
||||||
|
CONFIG_PATH = HOME_PATH + "/config"
|
||||||
|
|
||||||
PROGRAM_PATH = "."
|
PROGRAM_PATH = "."
|
||||||
ICON_PATH = PROGRAM_PATH + "/icon/icon.png"
|
ICON_PATH = PROGRAM_PATH + "/icon/icon.png"
|
||||||
ICON_TABLET_OFF_PATH = PROGRAM_PATH + "/icon/icon_tablet_off.png"
|
ICON_TABLET_OFF_PATH = PROGRAM_PATH + "/icon/icon_tablet_off.png"
|
||||||
ICON_TABLET_ON_PATH = PROGRAM_PATH + "/icon/icon_tablet_on.png"
|
ICON_TABLET_ON_PATH = PROGRAM_PATH + "/icon/icon_tablet_on.png"
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Subprocess wrapper
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
class SubprocessWrapper:
|
||||||
|
def __init__(self, stdout:str=os.devnull, stderr:str=os.devnull):
|
||||||
|
self.stdout: str = stdout
|
||||||
|
self.stderr: str = stderr
|
||||||
|
|
||||||
|
def call(self, arg) -> None:
|
||||||
|
with open(os.devnull, "w") as f:
|
||||||
|
subprocess.call(arg.split(), stdout=f, stderr=f)
|
||||||
|
|
||||||
|
def check_call(self, arg) -> None:
|
||||||
|
with open(os.devnull, "w") as f:
|
||||||
|
subprocess.check_call(arg.split(), stdout=f, stderr=f)
|
||||||
|
|
||||||
|
def run(self, arg: str) -> str:
|
||||||
|
return subprocess.run(arg.split(), stdout=subprocess.PIPE).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Display properties
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
class DisplayProperty:
|
||||||
|
def __init__(self):
|
||||||
|
self.name: str
|
||||||
|
self.width: int
|
||||||
|
self.height: int
|
||||||
|
self.x_offset: int
|
||||||
|
self.y_offset: int
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Screen adjustment class
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
class XRandR(SubprocessWrapper):
|
||||||
|
def __init__(self):
|
||||||
|
super(XRandR, self).__init__()
|
||||||
|
self.mode_name: str
|
||||||
|
self.scrren_suffix = "_virt"
|
||||||
|
# Thoese will be created in set_virtual_screen()
|
||||||
|
self.virt = DisplayProperty()
|
||||||
|
self.virt.name = "VIRTUAL1"
|
||||||
|
# Primary display
|
||||||
|
self.primary = DisplayProperty()
|
||||||
|
self._update_primary_screen()
|
||||||
|
|
||||||
|
def _add_screen_mode(self) -> None:
|
||||||
|
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
||||||
|
try:
|
||||||
|
self.check_call(args_addmode)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# When failed create mode and then add again
|
||||||
|
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
|
||||||
|
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
|
||||||
|
# Create new screen mode
|
||||||
|
self.check_call(f"xrandr --newmode {self.mode_name} {mode}")
|
||||||
|
# Add mode again
|
||||||
|
self.check_call(args_addmode)
|
||||||
|
# After adding mode the program should delete the mode automatically on exit
|
||||||
|
atexit.register(self.delete_virtual_screen)
|
||||||
|
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
||||||
|
signal.signal(sig, self._signal_handler)
|
||||||
|
|
||||||
|
def _update_primary_screen(self) -> None:
|
||||||
|
output = self.run("xrandr")
|
||||||
|
match = re.search(r"^(\w*)\s+.*primary\s*(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
||||||
|
self.primary.name = match.group(1)
|
||||||
|
self.primary.width = int(match.group(2))
|
||||||
|
self.primary.height = int(match.group(3))
|
||||||
|
self.primary.x_offset = int(match.group(4))
|
||||||
|
self.primary.y_offset = int(match.group(5))
|
||||||
|
|
||||||
|
def _update_virtual_screen(self) -> None:
|
||||||
|
output = self.run("xrandr")
|
||||||
|
match = re.search(r"^" + self.virt.name + r"\s+.*\s+(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
||||||
|
self.virt.width = int(match.group(1))
|
||||||
|
self.virt.height = int(match.group(2))
|
||||||
|
self.virt.x_offset = int(match.group(3))
|
||||||
|
self.virt.y_offset = int(match.group(4))
|
||||||
|
|
||||||
|
def _signal_handler(self, signum=None, frame=None) -> None:
|
||||||
|
self.delete_virtual_screen()
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def get_virtual_screen(self) -> DisplayProperty:
|
||||||
|
self._update_virtual_screen()
|
||||||
|
return self.virt
|
||||||
|
|
||||||
|
def set_virtual_screen(self, width, height, portrait=False, hidpi=False):
|
||||||
|
self.virt.width = width
|
||||||
|
self.virt.height = height
|
||||||
|
if portrait:
|
||||||
|
self.virt.width = height
|
||||||
|
self.virt.height = width
|
||||||
|
if hidpi:
|
||||||
|
self.virt.width = 2 * self.virt.width
|
||||||
|
self.virt.height = 2 * self.virt.height
|
||||||
|
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.scrren_suffix
|
||||||
|
|
||||||
|
def create_virtual_screen(self) -> None:
|
||||||
|
self._add_screen_mode()
|
||||||
|
self.check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||||
|
self.check_call("sleep 5")
|
||||||
|
self.check_call(f"xrandr --output {self.virt.name} --auto")
|
||||||
|
self._update_primary_screen()
|
||||||
|
self._update_virtual_screen()
|
||||||
|
|
||||||
|
def delete_virtual_screen(self) -> None:
|
||||||
|
try:
|
||||||
|
self.virt.name
|
||||||
|
self.mode_name
|
||||||
|
except AttributeError:
|
||||||
|
return
|
||||||
|
self.call(f"xrandr --output {self.virt.name} --off")
|
||||||
|
self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
# Twisted class
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
class ProcessProtocol(protocol.ProcessProtocol):
|
||||||
|
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
||||||
|
self.onConnected = onConnected
|
||||||
|
self.onOutReceived = onOutReceived
|
||||||
|
self.onErrRecevied = onErrRecevied
|
||||||
|
self.onProcessEnded = onProcessEnded
|
||||||
|
self.logfile = logfile
|
||||||
|
|
||||||
|
def run(self, arg: str):
|
||||||
|
"""Spawn a process
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
arg {str} -- arguments in string
|
||||||
|
"""
|
||||||
|
|
||||||
|
args = arg.split()
|
||||||
|
reactor.spawnProcess(self, args[0], args=args, env=os.environ)
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
"""Kill a spawned process
|
||||||
|
"""
|
||||||
|
self.transport.signalProcess('INT')
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
print("connectionMade!")
|
||||||
|
self.onConnected()
|
||||||
|
self.transport.closeStdin() # No more input
|
||||||
|
|
||||||
|
def outReceived(self, data):
|
||||||
|
print("outReceived! with %d bytes!" % len(data))
|
||||||
|
self.onOutReceived(data)
|
||||||
|
if self.logfile is not None:
|
||||||
|
self.logfile.write(data)
|
||||||
|
|
||||||
|
def errReceived(self, data):
|
||||||
|
print("outReceived! with %d bytes!" % len(data))
|
||||||
|
self.onErrRecevied(data)
|
||||||
|
if self.logfile is not None:
|
||||||
|
self.logfile.write(data)
|
||||||
|
|
||||||
|
def inConnectionLost(self):
|
||||||
|
print("inConnectionLost! stdin is closed! (we probably did it)")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def outConnectionLost(self):
|
||||||
|
print("outConnectionLost! The child closed their stdout!")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def errConnectionLost(self):
|
||||||
|
print("errConnectionLost! The child closed their stderr.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def processExited(self, reason):
|
||||||
|
exitCode = reason.value.exitCode
|
||||||
|
if exitCode is None:
|
||||||
|
print("Unknown exit")
|
||||||
|
return
|
||||||
|
print("processEnded, status", exitCode)
|
||||||
|
|
||||||
|
def processEnded(self, reason):
|
||||||
|
if self.logfile is not None:
|
||||||
|
self.logfile.close()
|
||||||
|
exitCode = reason.value.exitCode
|
||||||
|
if exitCode is None:
|
||||||
|
print("Unknown exit")
|
||||||
|
self.onProcessEnded(1)
|
||||||
|
return
|
||||||
|
print("processEnded, status", exitCode)
|
||||||
|
print("quitting")
|
||||||
|
self.onProcessEnded(exitCode)
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# QML Backend class
|
# QML Backend class
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
|
@ -29,31 +228,31 @@ class VNCState(Enum):
|
||||||
class Backend(QObject):
|
class Backend(QObject):
|
||||||
""" Backend class for QML frontend """
|
""" Backend class for QML frontend """
|
||||||
# Signals
|
# Signals
|
||||||
width_changed = pyqtSignal(int)
|
|
||||||
virtScreenChanged = pyqtSignal(bool)
|
virtScreenChanged = pyqtSignal(bool)
|
||||||
vncChanged = pyqtSignal(str)
|
vncStateChanged = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(Backend, self).__init__(parent)
|
super(Backend, self).__init__(parent)
|
||||||
# Virtual screen properties
|
# Virtual screen properties
|
||||||
self._width = 1368
|
self._width = 1368
|
||||||
self._height = 1024
|
self._height = 1024
|
||||||
self._portrait = True
|
self._portrait = False
|
||||||
self._hidpi = False
|
self._hidpi = False
|
||||||
self._virtScreenCreated = False
|
self._virtScreenCreated = False
|
||||||
# VNC server properties
|
# VNC server properties
|
||||||
self._vncPort = 5900
|
self._vncPort = 5900
|
||||||
self._vncPassword = ""
|
self._vncPassword = ""
|
||||||
self._vncState = VNCState.OFF
|
self._vncState = VNCState.OFF
|
||||||
|
# objects
|
||||||
|
self.xrandr = XRandR()
|
||||||
|
|
||||||
# Qt properties
|
# Qt properties
|
||||||
@pyqtProperty(int, notify=width_changed)
|
@pyqtProperty(int)
|
||||||
def width(self):
|
def width(self):
|
||||||
return self._width
|
return self._width
|
||||||
@width.setter
|
@width.setter
|
||||||
def width(self, width):
|
def width(self, width):
|
||||||
self._width = width
|
self._width = width
|
||||||
self.width_changed.emit(self._width)
|
|
||||||
|
|
||||||
@pyqtProperty(int)
|
@pyqtProperty(int)
|
||||||
def height(self):
|
def height(self):
|
||||||
|
@ -77,15 +276,19 @@ class Backend(QObject):
|
||||||
self._hidpi = hidpi
|
self._hidpi = hidpi
|
||||||
|
|
||||||
@pyqtProperty(bool)
|
@pyqtProperty(bool)
|
||||||
def virtScreenCreated(self):
|
def virtScreenCreated(self, notify=virtScreenChanged):
|
||||||
return self._virtScreenCreated
|
return self._virtScreenCreated
|
||||||
|
@virtScreenCreated.setter
|
||||||
|
def virtScreenCreated(self, value):
|
||||||
|
self._virtScreenCreated = value
|
||||||
|
self.virtScreenChanged.emit(value)
|
||||||
|
|
||||||
@pyqtProperty(int)
|
@pyqtProperty(int)
|
||||||
def vncPort(self):
|
def vncPort(self):
|
||||||
return self._vncPort
|
return self._vncPort
|
||||||
@vncPort.setter
|
@vncPort.setter
|
||||||
def vncPort(self, vncPort):
|
def vncPort(self, port):
|
||||||
self._vncPort = vncPort
|
self._vncPort = port
|
||||||
|
|
||||||
@pyqtProperty(str)
|
@pyqtProperty(str)
|
||||||
def vncPassword(self):
|
def vncPassword(self):
|
||||||
|
@ -96,10 +299,74 @@ class Backend(QObject):
|
||||||
print(self._vncPassword)
|
print(self._vncPassword)
|
||||||
|
|
||||||
@pyqtProperty(str)
|
@pyqtProperty(str)
|
||||||
def vncState(self):
|
def vncState(self, notify=vncStateChanged):
|
||||||
return self._vncState.value
|
return self._vncState.value
|
||||||
|
@vncState.setter
|
||||||
|
def vncState(self, state):
|
||||||
|
self._vncState = state
|
||||||
|
self.vncStateChanged.emit(self._vncState.value)
|
||||||
|
|
||||||
# Qt Slots
|
# Qt Slots
|
||||||
|
@pyqtSlot()
|
||||||
|
def createVirtScreen(self):
|
||||||
|
print("Creating a Virtual Screen...")
|
||||||
|
self.xrandr.set_virtual_screen(self.width, self.height, self.portrait, self.hidpi)
|
||||||
|
self.xrandr.create_virtual_screen()
|
||||||
|
self.virtScreenCreated = True
|
||||||
|
|
||||||
|
# Qt Slots
|
||||||
|
@pyqtSlot()
|
||||||
|
def deleteVirtScreen(self):
|
||||||
|
print("Deleting the Virtual Screen...")
|
||||||
|
self.xrandr.delete_virtual_screen()
|
||||||
|
self.virtScreenCreated = False
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def startVNC(self):
|
||||||
|
# Check if a virtual screen created
|
||||||
|
if not self.virtScreenCreated:
|
||||||
|
print("Virtual Screen not crated.")
|
||||||
|
return
|
||||||
|
# define callbacks
|
||||||
|
def _onConnected():
|
||||||
|
print("VNC started.")
|
||||||
|
self.vncState = VNCState.WAITING
|
||||||
|
def _onReceived(data):
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
for line in data.splitlines():
|
||||||
|
# TODO: Update state of the server
|
||||||
|
pass
|
||||||
|
def _onEnded(exitCode):
|
||||||
|
print("VNC Exited.")
|
||||||
|
self.vncState = VNCState.OFF
|
||||||
|
# Set password
|
||||||
|
password = False
|
||||||
|
if self.vncPassword:
|
||||||
|
print("There is password. Creating.")
|
||||||
|
password = True
|
||||||
|
p = SubprocessWrapper()
|
||||||
|
try:
|
||||||
|
p.run(f"x11vnc -storepasswd {self.vncPassword} {X11VNC_PASSWORD_PATH}")
|
||||||
|
except:
|
||||||
|
password = False
|
||||||
|
logfile = open(X11VNC_LOG_PATH, "wb")
|
||||||
|
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
|
||||||
|
port = self.vncPort
|
||||||
|
virt = self.xrandr.get_virtual_screen()
|
||||||
|
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
||||||
|
arg = f"x11vnc -multiptr -repeat -rfbport {port} -clip {clip}"
|
||||||
|
if password:
|
||||||
|
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
||||||
|
self.vncServer.run(arg)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def stopVNC(self):
|
||||||
|
print(self.vncState)
|
||||||
|
if self.vncState in (VNCState.WAITING.value, VNCState.CONNECTED.value):
|
||||||
|
self.vncServer.kill()
|
||||||
|
else:
|
||||||
|
print("stopVNC called while it is not running")
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def quitProgram(self):
|
def quitProgram(self):
|
||||||
QApplication.instance().quit()
|
QApplication.instance().quit()
|
||||||
|
@ -110,9 +377,37 @@ class Backend(QObject):
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
|
||||||
|
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
QMessageBox.critical(None, "VirtScreen",
|
||||||
|
"I couldn't detect any system tray on this system.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
|
||||||
|
QMessageBox.critical(None, "VirtScreen",
|
||||||
|
"Currently Wayland is not supported")
|
||||||
|
sys.exit(1)
|
||||||
|
if HOME_PATH is None:
|
||||||
|
QMessageBox.critical(None, "VirtScreen",
|
||||||
|
"VirtScreen cannot detect $HOME")
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.exists(HOME_PATH):
|
||||||
|
try:
|
||||||
|
os.makedirs(HOME_PATH)
|
||||||
|
except:
|
||||||
|
QMessageBox.critical(None, "VirtScreen",
|
||||||
|
"VirtScreen cannot create ~/.virtscreen")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
import qt5reactor # pylint: disable=E0401
|
||||||
|
qt5reactor.install()
|
||||||
|
from twisted.internet import utils, reactor # pylint: disable=E0401
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(ICON_PATH))
|
app.setWindowIcon(QIcon(ICON_PATH))
|
||||||
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
|
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
|
||||||
|
|
||||||
# Register the Python type. Its URI is 'People', it's v1.0 and the type
|
# Register the Python type. Its URI is 'People', it's v1.0 and the type
|
||||||
# will be called 'Person' in QML.
|
# will be called 'Person' in QML.
|
||||||
|
@ -125,3 +420,4 @@ if __name__ == '__main__':
|
||||||
print("Failed to load qml")
|
print("Failed to load qml")
|
||||||
exit(1)
|
exit(1)
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
reactor.run()
|
||||||
|
|
Loading…
Reference in a new issue