1
0
Fork 0
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:
Bumsik Kim 2018-05-07 11:36:12 -04:00
parent 7307a1111c
commit 9fe8e36d22
No known key found for this signature in database
GPG key ID: E31041C8EC5B01C6
2 changed files with 323 additions and 13 deletions

View file

@ -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()
} }
} }
} }

View file

@ -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()