From 157151466234ba9f776d15f9bb376c0e867eb60e Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 25 Apr 2018 15:14:11 -0400 Subject: [PATCH] initial program --- .pylintrc | 4 + .vscode/.gitignore | 1 + .vscode/launch.json | 23 +++ .vscode/settings.json | 7 + icon.png | Bin 0 -> 2496 bytes requirements.txt | 4 + virtscreen.py | 448 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 487 insertions(+) create mode 100644 .pylintrc create mode 100644 .vscode/.gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 icon.png create mode 100644 requirements.txt create mode 100755 virtscreen.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..cd9fe60 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +[MASTER] +extension-pkg-whitelist= + PyQt5, + netifaces diff --git a/.vscode/.gitignore b/.vscode/.gitignore new file mode 100644 index 0000000..55cf735 --- /dev/null +++ b/.vscode/.gitignore @@ -0,0 +1 @@ +tags \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..aa3de90 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "pythonPath": "${config:python.pythonPath}" + }, + { + "name": "Python: Terminal (integrated)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "pythonPath": "${config:python.pythonPath}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0467ce7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.venvPath": "${workspaceFolder}/ENV", + "[python]": { + "editor.formatOnSave": true + }, + "python.pythonPath": "${workspaceFolder}/ENV/bin/python" +} diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c8701a241a458d257e2e71c3d1af06007927fb86 GIT binary patch literal 2496 zcmZ`*cQ_S_8-A@EBxGb|ud^)M5P!1g#**la`B%_oOp(0tiX^3klA>AVs zHx<>vEpci1xqscie(&>s@B4h?&+mJl@2riL87I3iI{;4d0g~OO7jH%bfoW3{+MRSa zjUmL&%ouggMCUgXHtGS_kj>#g{|yFXg)0%8Mb=QVr3vdK6B}VCktAnz17IArv2ZZi zZ2dnlFo2N(OpIV=1Pc?kY?2wQ%pkCUjRovmz`=^Gtl%VI8v$Hw;ARI8JGOIxmjirT z!OsZ+PVCqQ!EF%Yf-pBkxFO1eojefR4sl*c@IsOgQhbo+hl~JZ1t7Nr@;jg)h+RV1 zEd)hjC<#MZ1S+CX6$No8)OO-GF{q0}LmZkC(2{_*By^;pD+N7i?2*P^8R*NxKoM8c^E5TzXC|RV6qFQyJ4mXvJws|!CV;@DzH?6l`5=NVMBzi8tl|y{~H|D;j94{ zO}J{pO$&##;jWFtIyj;O4_$cb!AlR`dvHu2eg^P2!0~-JVTb@jP>c|0gdk&_+z;x0 z1d|YAf>2Y0nIhZ_r_B&aMid#*2NAPLbHth>&I0ikNU%hrC6cU=Y>gCaq}m|O7U_04 zXO9efWIN)#6LOq!(FJ*~xa5j_HxwR1kvocy;OY@v^FWCQNo-kKZ=T@sPx7S zA5{6^rY~;$q1GRFj-&nr?w&wH02%|(L_u>PS_07;g!@#qQSl%c4}L9V=)|ykvNRT<3&8i67Vtse|gGVB035pznTaAxwH;v&L}L+>ErO*}CWeY_TJgj2t3f(TmqS@*MfPYj67s20GZZ zGyieMgyjimpUrTuyBmLVmrTFLfX2YV!TymKcAKivuzj%K;;zI60?FQR#d3wgw8-oI z)p*I5h3^h8oLV53JQx}(UX3e?e6xD(YjH@G$3B(+;5@eTbX)p*M*CFFDW0jWnV?;* zKHeoa4Fy!XMyhtdrHW>EK6z4fSg?*bYxY-VSmsQ>vBN9-6pHJkJ%t^PDJ>g=7t~3V z9jUh*t_XEFo(T(zTX@V~HKQDN?L>&wX%vBk3*xKU5_33%5mVax$U|ZgEMb096 zaa!P9V_-d1TRixH%fk_#@n=!AGz0IHlz>B`XD~c8e|L)(gcr~$o zo-0@iMeH z!D>EXS4);vv7}g{mPekl?R8PTnqtNv`P|=msFo>o5zd4DE<$RPDx^=c_3erD^exI7 ztqI%<-ag(6_0O~E0h}lNZ47uO<23IF@;mr+hPxz-318T}Pwr&PO7`YQk&i+<*}e@u zN!@d<*>KNc-C?uWYf?kqDdsP#iJEuI_bjVx230SobJzS`RtYS+&^9WNI-+qnHFV>gAX*=9Cmea&M0{XLsw9m-z?88 zYs@>lT{CH7FPZqpveb*}&#uWbhm-d6F}2W~B&4fm;;1c^TZkODeT9h=rdC#Dsn6GhfyzA7aY~i)p9$F&T-L2;O8l@-ZF41c#g)y>f&Nlv@^=!XdJ6 zYsQ_f6g6vP^w>7k@(OAtXC&9qj_?<@41evmsSo59rN}l?u3Ej~L1qlm##U_Pw7n>AA_r%SmXesH8o|t4Y z^;!Axy~H!k*XwmiN7MtBnBSsLe&QmYOtbi0-q96WZJI7@*OHj+=$Ri`9=aq)?BOT#_4cb?XLvIzpySaynx&erVUQ9=yQCR@ zJc5!i*=?`n?ay|nw;`6r)*)U#wobBw^s<`oWq+$~n&;1_8R_)cGN~Q?`|4u#cl)-P(H*>5nTy~<}eaLe1o8P+2;WsCI4!L$8t9~mo zYneOnkRI|WCy-g__Ibi>{=%k$y5+5=PrJPvb)~pZ`o1Vs{4Csicjvh2^-n?_=hf8$ zjthikx(yDyE{&~|ds!Ra=r6}KZJ`~hBKVC+8q|!Qrb+&zAq_$mLcS!S@aW@iO$WLsmeVj2D{ww*m`+wNj|C6Jr(ml>B*wgsm Omx^p+MXEFQO!^ None: + with open(os.devnull, "w") as nulldev: + subprocess.call(arg.split(), stdout=nulldev, stderr=nulldev) + + def _check_call(self, arg) -> None: + with open(os.devnull, "w") as nulldev: + subprocess.check_call(arg.split(), stdout=nulldev, stderr=nulldev) + + def _run(self, arg: str) -> str: + return subprocess.run(arg.split(), stdout=subprocess.PIPE).stdout.decode('utf-8') + + 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 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, position) -> 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} {position} {self.primary.name}") + self._update_primary_screen() + self._update_virtual_screen() + + def delete_virtual_screen(self) -> None: + 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, onOutReceived, onErrRecevied, onProcessEnded): + self.onOutReceived = onOutReceived + self.onErrRecevied = onErrRecevied + self.onProcessEnded = onProcessEnded + + 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.transport.closeStdin() # No more input + + def outReceived(self, data): + print("outReceived! with %d bytes!" % len(data)) + self.onOutReceived(data) + + def errReceived(self, data): + print("outReceived! with %d bytes!" % len(data)) + self.onErrRecevied(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): + exitCode = reason.value.exitCode + if exitCode is None: + print("Unknown exit") + self.onProcessEnded(1) + return + print("processEnded, status", exitCode) + print("quitting") + self.onProcessEnded(exitCode) + +#------------------------------------------------------------------------------- +# Qt Window class +#------------------------------------------------------------------------------- +class Window(QDialog): + def __init__(self): + super(Window, self).__init__() + # Create objects + self.createDisplayGroupBox() + self.createVNCGroupBox() + self.createActions() + self.createTrayIcon() + self.xrandr = XRandR() + # Additional attributes + self.isVNCRunning = False + # Update UI + self.update_ip_address() + # Put togather + mainLayout = QVBoxLayout() + mainLayout.addWidget(self.displayGroupBox) + mainLayout.addWidget(self.VNCGroupBox) + self.setLayout(mainLayout) + # Events + self.trayIcon.activated.connect(self.iconActivated) + self.startVNCButton.pressed.connect(self.startPressed) + self.VNCMessageListWidget.model().rowsInserted.connect( + self.VNCMessageListWidget.scrollToBottom) + # Show + icon = QIcon("icon.png") + self.trayIcon.setIcon(icon) + self.setWindowIcon(icon) + self.trayIcon.show() + self.trayIcon.setToolTip("VirtScreen") + self.setWindowTitle("VirtScreen") + self.resize(400, 300) + + def setVisible(self, visible): + """Override of setVisible(bool) + + Arguments: + visible {bool} -- true to show, false to hide + """ + self.openAction.setEnabled(self.isMaximized() or not visible) + super(Window, self).setVisible(visible) + + def closeEvent(self, event): + """Override of closeEvent() + + Arguments: + event {QCloseEvent} -- QCloseEvent + """ + if self.trayIcon.isVisible(): + self.hide() + self.showMessage() + event.ignore() + else: + QApplication.instance().quit() + + @pyqtSlot() + def startPressed(self): + if self.isVNCRunning: + self.VNCServer.kill() + else: + self.startVNC() + + @pyqtSlot('QSystemTrayIcon::ActivationReason') + def iconActivated(self, reason): + if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick): + if self.isVisible(): + self.hide() + else: + self.showNormal() + elif reason == QSystemTrayIcon.MiddleClick: + self.showMessage() + + @pyqtSlot() + def showMessage(self): + icon = QSystemTrayIcon.MessageIcon(QSystemTrayIcon.Information) + self.trayIcon.showMessage("VirtScreen is running", + "The program will keep running in the system tray. To \n" + "terminate the program, choose \"Quit\" in the \n" + "context menu of the system tray entry.", + icon, + 7 * 1000) + + def createDisplayGroupBox(self): + self.displayGroupBox = QGroupBox("Virtual Display Settings") + + # First row + positionLabel = QLabel("Position:") + + self.displayPositionComboBox = QComboBox() + self.displayPositionComboBox.addItem("Right", "--right-of") + self.displayPositionComboBox.addItem("Left", "--left-of") + self.displayPositionComboBox.addItem("Above", "--above") + self.displayPositionComboBox.addItem("Below", "--below") + self.displayPositionComboBox.setCurrentIndex(0) + + self.displayPortraitCheckBox = QCheckBox("Portrait Mode") + self.displayPortraitCheckBox.setChecked(False) + + # Second row + resolutionLabel = QLabel("Resolution:") + + self.displayWidthSpinBox = QSpinBox() + self.displayWidthSpinBox.setRange(640, 1920) + self.displayWidthSpinBox.setSuffix(" px") + self.displayWidthSpinBox.setValue(1368) + + xLabel = QLabel("x") + + self.displayHeightSpinBox = QSpinBox() + self.displayHeightSpinBox.setRange(360, 1080) + self.displayHeightSpinBox.setSuffix(" px") + self.displayHeightSpinBox.setValue(1024) + + self.displayHIDPICheckBox = QCheckBox("HiDPI (2x resolution)") + self.displayHIDPICheckBox.setChecked(False) + + # Putting them togather + layout = QGridLayout() + # Display Position row + layout.addWidget(positionLabel, 0, 0) + layout.addWidget(self.displayPositionComboBox, 0, 1, 1, 2) + layout.addWidget(self.displayPortraitCheckBox, 0, 6, 1, 2, Qt.AlignLeft) + # Resolution row + layout.addWidget(resolutionLabel, 1, 0) + layout.addWidget(self.displayWidthSpinBox, 1, 1, 1, 2) + layout.addWidget(xLabel, 1, 3, Qt.AlignHCenter) + layout.addWidget(self.displayHeightSpinBox, 1, 4, 1, 2) + layout.addWidget(self.displayHIDPICheckBox, 1, 6, 1, 2, Qt.AlignLeft) + # Set strectch + layout.setColumnStretch(1, 0) + layout.setColumnStretch(3, 0) + # layout.setRowStretch(4, 1) + + self.displayGroupBox.setLayout(layout) + + def createVNCGroupBox(self): + self.VNCGroupBox = QGroupBox("VNC Server") + + portLabel = QLabel("Port:") + self.VNCPortSpinBox = QSpinBox() + self.VNCPortSpinBox.setRange(5900, 6000) + self.VNCPortSpinBox.setValue(5900) + + IPLabel = QLabel("Connect a VNC client to one of:") + self.VNCIPListWidget = QListWidget() + + self.startVNCButton = QPushButton("Start VNC Server") + self.startVNCButton.setDefault(False) + + messageLabel = QLabel("Server Messages") + self.VNCMessageListWidget = QListWidget() + self.VNCMessageListWidget.setEnabled(False) + + # Set Overall layout + layout = QVBoxLayout() + portLayout = QHBoxLayout() + portLayout.addWidget(portLabel) + portLayout.addWidget(self.VNCPortSpinBox) + portLayout.addStretch() + layout.addLayout(portLayout) + layout.addWidget(IPLabel) + layout.addWidget(self.VNCIPListWidget) + layout.addWidget(self.startVNCButton) + layout.addSpacing(15) + layout.addWidget(messageLabel) + layout.addWidget(self.VNCMessageListWidget) + self.VNCGroupBox.setLayout(layout) + + def createActions(self): + self.minimizeAction = QAction("&Start sharing", self) + self.minimizeAction.triggered.connect(self.hide) + + self.maximizeAction = QAction("S&top sharing", self) + self.maximizeAction.triggered.connect(self.showMaximized) + + self.openAction = QAction("&Open VirtScreen", self) + self.openAction.triggered.connect(self.showNormal) + + self.quitAction = QAction("&Quit", self) + self.quitAction.triggered.connect(QApplication.instance().quit) + + def createTrayIcon(self): + self.trayIconMenu = QMenu(self) + self.trayIconMenu.addAction(self.minimizeAction) + self.trayIconMenu.addAction(self.maximizeAction) + self.trayIconMenu.addSeparator() + self.trayIconMenu.addAction(self.openAction) + self.trayIconMenu.addSeparator() + self.trayIconMenu.addAction(self.quitAction) + + self.trayIcon = QSystemTrayIcon(self) + self.trayIcon.setContextMenu(self.trayIconMenu) + + def update_ip_address(self): + self.VNCIPListWidget.clear() + for interface in interfaces(): + if interface == 'lo': + continue + addresses = ifaddresses(interface).get(AF_INET, None) + if addresses is None: + continue + for link in addresses: + if link is not None: + self.VNCIPListWidget.addItem(link['addr']) + + def startVNC(self): + def _onReceived(data): + data = data.decode("utf-8") + for line in data.splitlines(): + self.VNCMessageListWidget.addItem(line) + def _onEnded(exitCode): + self.startVNCButton.setEnabled(False) + self.xrandr.delete_virtual_screen() + self.isVNCRunning = False + self.VNCMessageListWidget.setEnabled(False) + self.startVNCButton.setText("Start VNC Server") + self.startVNCButton.setEnabled(True) + # Setting UI before starting + self.VNCMessageListWidget.clear() + self.startVNCButton.setEnabled(False) + self.startVNCButton.setText("Running...") + # Create virtual screen + width = self.displayWidthSpinBox.value() + height = self.displayHeightSpinBox.value() + portrait = self.displayPortraitCheckBox.isChecked() + hidpi = self.displayHIDPICheckBox.isChecked() + position = self.displayPositionComboBox.currentData() + self.xrandr.set_virtual_screen(width, height, portrait, hidpi) + self.xrandr.create_virtual_screen(position) + # Run VNC server + self.isVNCRunning = True + self.VNCServer = ProcessProtocol(_onReceived, _onReceived, _onEnded) + port = self.VNCPortSpinBox.value() + virt = self.xrandr.virt + clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}" + arg = f"x11vnc -multiptr -repeat -rfbport {port} -clip {clip}" + self.VNCServer.run(arg) + # Change UI + self.VNCMessageListWidget.setEnabled(True) + self.startVNCButton.setEnabled(True) + self.startVNCButton.setText("Stop") + +#------------------------------------------------------------------------------- +# Main Code +#------------------------------------------------------------------------------- +if __name__ == '__main__': + + import sys + + app = QApplication(sys.argv) + + if not QSystemTrayIcon.isSystemTrayAvailable(): + QMessageBox.critical(None, "Systray", + "I couldn't detect any system tray on this system.") + sys.exit(1) + + if os.environ['XDG_SESSION_TYPE'] == 'wayland': + QMessageBox.critical(None, "Wayland Session", + "Currently Wayland is not supported") + sys.exit(1) + + import qt5reactor # pylint: disable=E0401 + qt5reactor.install() + from twisted.internet import utils, reactor # pylint: disable=E0401 + + QApplication.setQuitOnLastWindowClosed(False) + window = Window() + window.show() + sys.exit(app.exec_()) + reactor.run() \ No newline at end of file