mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-03-09 15:40:18 +00:00
Backend: switched to asyncio from Twisted
This commit is contained in:
parent
8393dab1a5
commit
c09fffe6e8
3 changed files with 88 additions and 89 deletions
|
@ -8,7 +8,7 @@ arch=("i686" "x86_64")
|
||||||
url="https://github.com/kbumsik/VirtScreen"
|
url="https://github.com/kbumsik/VirtScreen"
|
||||||
license=('GPL')
|
license=('GPL')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'python-twisted' 'python-netifaces' 'python-qt5reactor')
|
depends=('xorg-xrandr' 'x11vnc' 'python-pyqt5' 'python-quamash-git' 'python-netifaces')
|
||||||
makedepends=('python-pip')
|
makedepends=('python-pip')
|
||||||
optdepends=(
|
optdepends=(
|
||||||
'arandr: for display settings option'
|
'arandr: for display settings option'
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -145,8 +145,7 @@ setup(
|
||||||
# For an analysis of "install_requires" vs pip's requirements files see:
|
# For an analysis of "install_requires" vs pip's requirements files see:
|
||||||
# https://packaging.python.org/en/latest/requirements.html
|
# https://packaging.python.org/en/latest/requirements.html
|
||||||
install_requires=['PyQt5>=5.10.1',
|
install_requires=['PyQt5>=5.10.1',
|
||||||
'Twisted>=17.9.0',
|
'Quamash>=0.6.0',
|
||||||
'qt5reactor>=0.5',
|
|
||||||
'netifaces>=0.10.6'], # Optional
|
'netifaces>=0.10.6'], # Optional
|
||||||
|
|
||||||
# List additional groups of dependencies here (e.g. development
|
# List additional groups of dependencies here (e.g. development
|
||||||
|
|
|
@ -11,9 +11,11 @@ import time
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import argparse
|
||||||
|
import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Dict, Callable
|
from typing import List, Dict, Callable
|
||||||
|
import asyncio
|
||||||
|
|
||||||
# Import OpenGL library for Nvidia driver
|
# Import OpenGL library for Nvidia driver
|
||||||
# https://github.com/Ultimaker/Cura/pull/131#issuecomment-176088664
|
# https://github.com/Ultimaker/Cura/pull/131#issuecomment-176088664
|
||||||
|
@ -26,8 +28,7 @@ from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||||
from PyQt5.QtGui import QIcon, QCursor
|
from PyQt5.QtGui import QIcon, QCursor
|
||||||
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
|
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
|
||||||
# Twisted and netifaces
|
from quamash import QEventLoop
|
||||||
from twisted.internet import protocol, error
|
|
||||||
from netifaces import interfaces, ifaddresses, AF_INET
|
from netifaces import interfaces, ifaddresses, AF_INET
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
|
@ -75,92 +76,88 @@ class SubprocessWrapper:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_output(self, arg) -> None:
|
def check_output(self, arg) -> None:
|
||||||
return subprocess.check_output(arg.split(), stderr=subprocess.STDOUT).decode('utf-8')
|
return subprocess.check_output(shlex.split(arg), stderr=subprocess.STDOUT).decode('utf-8')
|
||||||
|
|
||||||
def run(self, arg: str, input: str = None, check=False) -> str:
|
def run(self, arg: str, input: str = None, check=False) -> str:
|
||||||
if input:
|
if input:
|
||||||
input = input.encode('utf-8')
|
input = input.encode('utf-8')
|
||||||
return subprocess.run(arg.split(), input=input, stdout=subprocess.PIPE,
|
return subprocess.run(shlex.split(arg), input=input, stdout=subprocess.PIPE,
|
||||||
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
|
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# Twisted class
|
# Asynchronous subprocess wrapper class
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
class ProcessProtocol(protocol.ProcessProtocol):
|
class AsyncSubprocess():
|
||||||
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
class Protocol(asyncio.SubprocessProtocol):
|
||||||
self.onConnected = onConnected
|
def __init__(self, outer):
|
||||||
self.onOutReceived = onOutReceived
|
self.outer = outer
|
||||||
self.onErrRecevied = onErrRecevied
|
self.transport: asyncio.SubprocessTransport
|
||||||
self.onProcessEnded = onProcessEnded
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
print("connectionMade!")
|
||||||
|
self.outer.connected()
|
||||||
|
self.transport = transport
|
||||||
|
transport.get_pipe_transport(0).close() # No more input
|
||||||
|
|
||||||
|
def pipe_data_received(self, fd, data):
|
||||||
|
if fd == 1: # stdout
|
||||||
|
self.outer.out_recevied(data)
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.write(data)
|
||||||
|
elif fd == 2: # stderr
|
||||||
|
self.outer.err_recevied(data)
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.write(data)
|
||||||
|
|
||||||
|
def pipe_connection_lost(self, fd, exc):
|
||||||
|
if fd == 0: # stdin
|
||||||
|
print("stdin is closed. (we probably did it)")
|
||||||
|
elif fd == 1: # stdout
|
||||||
|
print("The child closed their stdout.")
|
||||||
|
elif fd == 2: # stderr
|
||||||
|
print("The child closed their stderr.")
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
print("Subprocess connection lost.")
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
if self.outer.logfile is not None:
|
||||||
|
self.outer.logfile.close()
|
||||||
|
self.transport.close()
|
||||||
|
return_code = self.transport.get_returncode()
|
||||||
|
if return_code is None:
|
||||||
|
print("Unknown exit")
|
||||||
|
self.outer.ended(1)
|
||||||
|
return
|
||||||
|
print("processEnded, status", return_code)
|
||||||
|
self.outer.ended(return_code)
|
||||||
|
|
||||||
|
def __init__(self, connected, out_recevied, err_recevied, ended, logfile=None):
|
||||||
|
self.connected = connected
|
||||||
|
self.out_recevied = out_recevied
|
||||||
|
self.err_recevied = err_recevied
|
||||||
|
self.ended = ended
|
||||||
self.logfile = logfile
|
self.logfile = logfile
|
||||||
# We cannot import this at the top of the file because qt5reactor should
|
self.transport: asyncio.SubprocessTransport
|
||||||
# be installed in the main function first.
|
self.protocol: self.Protocol
|
||||||
from twisted.internet import reactor # pylint: disable=E0401
|
|
||||||
self.reactor = reactor
|
async def _run(self, arg: str, loop: asyncio.AbstractEventLoop):
|
||||||
|
self.transport, self.protocol = await loop.subprocess_exec(
|
||||||
|
lambda: self.Protocol(self), *shlex.split(arg), env=os.environ)
|
||||||
|
|
||||||
def run(self, arg: str):
|
def run(self, arg: str):
|
||||||
"""Spawn a process
|
"""Spawn a process.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
arg {str} -- arguments in string
|
arg {str} -- arguments in string
|
||||||
"""
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self._run(arg, loop))
|
||||||
|
|
||||||
args = arg.split()
|
def close(self):
|
||||||
self.reactor.spawnProcess(self, args[0], args=args, env=os.environ)
|
"""Kill a spawned process."""
|
||||||
|
self.transport.send_signal(signal.SIGINT)
|
||||||
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("errReceived! 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)
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
|
@ -428,7 +425,7 @@ class Backend(QObject):
|
||||||
self._vncUsePassword: bool = False
|
self._vncUsePassword: bool = False
|
||||||
self._vncState: self.VNCState = self.VNCState.OFF
|
self._vncState: self.VNCState = self.VNCState.OFF
|
||||||
# Primary screen and mouse posistion
|
# Primary screen and mouse posistion
|
||||||
self.vncServer: ProcessProtocol
|
self.vncServer: AsyncSubprocess
|
||||||
# Check config file
|
# Check config file
|
||||||
# and initialize if needed
|
# and initialize if needed
|
||||||
need_init = False
|
need_init = False
|
||||||
|
@ -589,11 +586,11 @@ class Backend(QObject):
|
||||||
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
|
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
|
||||||
|
|
||||||
# define callbacks
|
# define callbacks
|
||||||
def _onConnected():
|
def _connected():
|
||||||
print("VNC started.")
|
print("VNC started.")
|
||||||
self.vncState = self.VNCState.WAITING
|
self.vncState = self.VNCState.WAITING
|
||||||
|
|
||||||
def _onReceived(data):
|
def _received(data):
|
||||||
data = data.decode("utf-8")
|
data = data.decode("utf-8")
|
||||||
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
||||||
print("VNC connected.")
|
print("VNC connected.")
|
||||||
|
@ -602,7 +599,7 @@ class Backend(QObject):
|
||||||
print("VNC disconnected.")
|
print("VNC disconnected.")
|
||||||
self.vncState = self.VNCState.WAITING
|
self.vncState = self.VNCState.WAITING
|
||||||
|
|
||||||
def _onEnded(exitCode):
|
def _ended(exitCode):
|
||||||
if exitCode is not 0:
|
if exitCode is not 0:
|
||||||
self.vncState = self.VNCState.ERROR
|
self.vncState = self.VNCState.ERROR
|
||||||
self.onError.emit('X11VNC: Error occurred.\n'
|
self.onError.emit('X11VNC: Error occurred.\n'
|
||||||
|
@ -626,7 +623,7 @@ class Backend(QObject):
|
||||||
options += str(value['arg']) + ' '
|
options += str(value['arg']) + ' '
|
||||||
# Sart x11vnc, turn settings object into VNC arguments format
|
# Sart x11vnc, turn settings object into VNC arguments format
|
||||||
logfile = open(X11VNC_LOG_PATH, "wb")
|
logfile = open(X11VNC_LOG_PATH, "wb")
|
||||||
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
|
self.vncServer = AsyncSubprocess(_connected, _received, _received, _ended, logfile)
|
||||||
try:
|
try:
|
||||||
virt = self.xrandr.get_virtual_screen()
|
virt = self.xrandr.get_virtual_screen()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
@ -643,13 +640,13 @@ class Backend(QObject):
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def openDisplaySetting(self, app: str = "arandr"):
|
def openDisplaySetting(self, app: str = "arandr"):
|
||||||
# define callbacks
|
# define callbacks
|
||||||
def _onConnected():
|
def _connected():
|
||||||
print("External Display Setting opened.")
|
print("External Display Setting opened.")
|
||||||
|
|
||||||
def _onReceived(data):
|
def _received(data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _onEnded(exitCode):
|
def _ended(exitCode):
|
||||||
print("External Display Setting closed.")
|
print("External Display Setting closed.")
|
||||||
self.onDisplaySettingClosed.emit()
|
self.onDisplaySettingClosed.emit()
|
||||||
if exitCode is not 0:
|
if exitCode is not 0:
|
||||||
|
@ -660,10 +657,10 @@ class Backend(QObject):
|
||||||
self.onError.emit('Wrong display settings program')
|
self.onError.emit('Wrong display settings program')
|
||||||
return
|
return
|
||||||
program_list = [data[app]['args'], "arandr"]
|
program_list = [data[app]['args'], "arandr"]
|
||||||
program = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, None)
|
program = AsyncSubprocess(_connected, _received, _received, _ended, None)
|
||||||
running_program = ''
|
running_program = ''
|
||||||
for arg in program_list:
|
for arg in program_list:
|
||||||
if not shutil.which(arg.split()[0]):
|
if not shutil.which(shlex.split(arg)[0]):
|
||||||
continue
|
continue
|
||||||
running_program = arg
|
running_program = arg
|
||||||
program.run(arg)
|
program.run(arg)
|
||||||
|
@ -679,10 +676,10 @@ class Backend(QObject):
|
||||||
def stopVNC(self, force=False):
|
def stopVNC(self, force=False):
|
||||||
if force:
|
if force:
|
||||||
# Usually called from atexit().
|
# Usually called from atexit().
|
||||||
self.vncServer.kill()
|
self.vncServer.close()
|
||||||
time.sleep(3) # Make sure X11VNC shutdown before execute next atexit().
|
time.sleep(3) # Make sure X11VNC shutdown before execute next atexit().
|
||||||
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
||||||
self.vncServer.kill()
|
self.vncServer.close()
|
||||||
else:
|
else:
|
||||||
self.onError.emit("stopVNC called while it is not running")
|
self.onError.emit("stopVNC called while it is not running")
|
||||||
|
|
||||||
|
@ -809,6 +806,8 @@ def check_env(msg: Callable[[str], None]) -> None:
|
||||||
def main_gui():
|
def main_gui():
|
||||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
loop = QEventLoop(app)
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
# Check environment first
|
# Check environment first
|
||||||
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
|
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
|
||||||
|
@ -842,9 +841,11 @@ def main_gui():
|
||||||
dialog("Failed to load QML")
|
dialog("Failed to load QML")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
reactor.run()
|
with loop:
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
def main_cli(args: argparse.Namespace):
|
def main_cli(args: argparse.Namespace):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
for key, value in vars(args).items():
|
for key, value in vars(args).items():
|
||||||
print(key, ": ", value)
|
print(key, ": ", value)
|
||||||
# Check the environment
|
# Check the environment
|
||||||
|
@ -886,9 +887,8 @@ def main_cli(args: argparse.Namespace):
|
||||||
if state is backend.VNCState.OFF:
|
if state is backend.VNCState.OFF:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
backend.onVncStateChanged.connect(handle_vnc_changed)
|
backend.onVncStateChanged.connect(handle_vnc_changed)
|
||||||
from twisted.internet import reactor # pylint: disable=E0401
|
|
||||||
backend.startVNC(config['vnc']['port'])
|
backend.startVNC(config['vnc']['port'])
|
||||||
reactor.run()
|
loop.run_forever()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue