mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-02-12 11:21:53 +00:00
Use python logging module #8
This commit is contained in:
parent
f27db06b17
commit
ebbbf97cdf
6 changed files with 106 additions and 58 deletions
15
Makefile
15
Makefile
|
@ -8,10 +8,21 @@ DOCKER_RUN_TTY=docker run --interactive --tty -v $(shell pwd):/app $(DOCKER_NAME
|
|||
|
||||
.ONESHELL:
|
||||
|
||||
.PHONY: run debug run-appimage debug-appimage
|
||||
|
||||
# Run script
|
||||
run:
|
||||
python3 -m virtscreen
|
||||
|
||||
debug:
|
||||
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 python3 -m virtscreen --log=DEBUG
|
||||
|
||||
run-appimage: package/appimage/VirtScreen-x86_64.AppImage
|
||||
$<
|
||||
|
||||
debug-appimage: package/appimage/VirtScreen-x86_64.AppImage
|
||||
QT_DEBUG_PLUGINS=1 QML_IMPORT_TRACE=1 $< --log=DEBUG
|
||||
|
||||
# Docker tools
|
||||
.PHONY: docker docker-build
|
||||
|
||||
|
@ -36,14 +47,14 @@ wheel-clean:
|
|||
.PHONY: appimage-clean
|
||||
.SECONDARY: package/appimage/VirtScreen-x86_64.AppImage
|
||||
|
||||
package/appimage/%.AppImage:
|
||||
package/appimage/VirtScreen-x86_64.AppImage:
|
||||
$(DOCKER_RUN) package/appimage/build.sh
|
||||
$(DOCKER_RUN) chown -R $(shell id -u):$(shell id -u) package/appimage
|
||||
|
||||
appimage-clean:
|
||||
-rm -rf package/appimage/virtscreen.AppDir package/appimage/VirtScreen-x86_64.AppImage
|
||||
|
||||
# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html
|
||||
# For Debian packaging, https://www.debian.org/doc/manuals/maint-guide/index.en.html
|
||||
# https://www.debian.org/doc/manuals/debmake-doc/ch08.en.html#setup-py
|
||||
.PHONY: deb-contents deb-clean
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import signal
|
|||
import json
|
||||
import shutil
|
||||
import argparse
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Callable
|
||||
import asyncio
|
||||
|
||||
|
@ -25,8 +27,12 @@ from quamash import QEventLoop
|
|||
from .display import DisplayProperty
|
||||
from .xrandr import XRandR
|
||||
from .qt_backend import Backend, Cursor, Network
|
||||
from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH
|
||||
from .path import HOME_PATH, ICON_PATH, MAIN_QML_PATH, CONFIG_PATH, LOGGING_PATH
|
||||
|
||||
def error(*args, **kwargs) -> None:
|
||||
"""Error printing"""
|
||||
args = ('Error: ', *args)
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
def main() -> None:
|
||||
"""Start main program"""
|
||||
|
@ -63,22 +69,46 @@ def main() -> None:
|
|||
help='Portrait mode. Width and height of the screen are swapped')
|
||||
parser.add_argument('--hidpi', action='store_true',
|
||||
help='HiDPI mode. Width and height are doubled')
|
||||
parser.add_argument('--log', type=str,
|
||||
help='Python logging level, For example, --log=DEBUG.\n'
|
||||
'Only used for reporting bugs and debugging')
|
||||
# Add signal handler
|
||||
def on_exit(self, signum=None, frame=None):
|
||||
sys.exit(0)
|
||||
for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
||||
signal.signal(sig, on_exit)
|
||||
|
||||
args = vars(parser.parse_args())
|
||||
# Enable logging
|
||||
if args['log'] is None:
|
||||
args['log'] = 'WARNING'
|
||||
log_level = getattr(logging, args['log'].upper(), None)
|
||||
if not isinstance(log_level, int):
|
||||
error('Please choose a correct python logging level')
|
||||
sys.exit(1)
|
||||
# When logging level is INFO or lower, print logs in terminal
|
||||
# Otherwise log to a file
|
||||
log_to_file = True if log_level > logging.INFO else False
|
||||
FORMAT = "[%(levelname)s:%(filename)s:%(lineno)s:%(funcName)s()] %(message)s"
|
||||
logging.basicConfig(level=log_level, format=FORMAT,
|
||||
**({'filename': LOGGING_PATH} if log_to_file else {}))
|
||||
if log_to_file:
|
||||
logger = logging.getLogger()
|
||||
handler = RotatingFileHandler(LOGGING_PATH, mode='a', maxBytes=1024*4, backupCount=1)
|
||||
logger.addHandler(handler)
|
||||
logging.info('logging enabled')
|
||||
del args['log']
|
||||
logging.info(f'{args}')
|
||||
# Start main
|
||||
args = parser.parse_args()
|
||||
if any(vars(args).values()):
|
||||
if any(args.values()):
|
||||
main_cli(args)
|
||||
else:
|
||||
main_gui()
|
||||
print('Program should not reach here.')
|
||||
error('Program should not reach here.')
|
||||
sys.exit(1)
|
||||
|
||||
def check_env(msg: Callable[[str], None]) -> None:
|
||||
"""Check enveironments before start"""
|
||||
"""Check environments before start"""
|
||||
if os.environ.get('XDG_SESSION_TYPE', '').lower() == 'wayland':
|
||||
msg("Currently Wayland is not supported")
|
||||
sys.exit(1)
|
||||
|
@ -94,6 +124,7 @@ def check_env(msg: Callable[[str], None]) -> None:
|
|||
if not shutil.which('x11vnc'):
|
||||
msg("x11vnc is not installed.")
|
||||
sys.exit(1)
|
||||
# Check if xrandr is correctly parsed.
|
||||
try:
|
||||
test = XRandR()
|
||||
except RuntimeError as e:
|
||||
|
@ -105,7 +136,7 @@ def main_gui():
|
|||
app = QApplication(sys.argv)
|
||||
loop = QEventLoop(app)
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
# Check environment first
|
||||
from PyQt5.QtWidgets import QMessageBox, QSystemTrayIcon
|
||||
def dialog(message: str) -> None:
|
||||
|
@ -114,7 +145,7 @@ def main_gui():
|
|||
dialog("Cannot detect system tray on this system.")
|
||||
sys.exit(1)
|
||||
check_env(dialog)
|
||||
|
||||
|
||||
app.setApplicationName("VirtScreen")
|
||||
app.setWindowIcon(QIcon(ICON_PATH))
|
||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||
|
@ -138,38 +169,36 @@ def main_gui():
|
|||
|
||||
def main_cli(args: argparse.Namespace):
|
||||
loop = asyncio.get_event_loop()
|
||||
for key, value in vars(args).items():
|
||||
print(key, ": ", value)
|
||||
# Check the environment
|
||||
check_env(print)
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
print("Configuration file does not exist.\n"
|
||||
error("Configuration file does not exist.\n"
|
||||
"Configure a virtual screen using GUI first.")
|
||||
sys.exit(1)
|
||||
# By instantiating the backend, additional verifications of config
|
||||
# file will be done.
|
||||
# file will be done.
|
||||
backend = Backend()
|
||||
# Get settings
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
# Override settings from arguments
|
||||
position = ''
|
||||
if not args.auto:
|
||||
if not args['auto']:
|
||||
args_virt = ['portrait', 'hidpi']
|
||||
for prop in args_virt:
|
||||
if vars(args)[prop]:
|
||||
if args[prop]:
|
||||
config['virt'][prop] = True
|
||||
args_position = ['left', 'right', 'above', 'below']
|
||||
tmp_args = {k: vars(args)[k] for k in args_position}
|
||||
tmp_args = {k: args[k] for k in args_position}
|
||||
if not any(tmp_args.values()):
|
||||
print("Choose a position relative to the primary monitor. (e.g. --left)")
|
||||
error("Choose a position relative to the primary monitor. (e.g. --left)")
|
||||
sys.exit(1)
|
||||
for key, value in tmp_args.items():
|
||||
if value:
|
||||
position = key
|
||||
# Create virtscreen and Start VNC
|
||||
def handle_error(msg):
|
||||
print('Error: ', msg)
|
||||
error(msg)
|
||||
sys.exit(1)
|
||||
backend.onError.connect(handle_error)
|
||||
backend.createVirtScreen(config['virt']['device'], config['virt']['width'],
|
||||
|
|
|
@ -29,6 +29,7 @@ BASE_PATH = os.path.dirname(__file__) # Location of this script
|
|||
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
|
||||
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
|
||||
CONFIG_PATH = HOME_PATH + "/config.json"
|
||||
LOGGING_PATH = HOME_PATH + "/log.txt"
|
||||
# Path in the program path
|
||||
ICON_PATH = BASE_PATH + "/icon/full_256x256.png"
|
||||
ASSETS_PATH = BASE_PATH + "/assets"
|
||||
|
|
|
@ -5,6 +5,7 @@ import asyncio
|
|||
import signal
|
||||
import shlex
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
class SubprocessWrapper:
|
||||
|
@ -30,7 +31,7 @@ class _Protocol(asyncio.SubprocessProtocol):
|
|||
self.transport: asyncio.SubprocessTransport
|
||||
|
||||
def connection_made(self, transport):
|
||||
print("connectionMade!")
|
||||
logging.info("connectionMade!")
|
||||
self.outer.connected()
|
||||
self.transport = transport
|
||||
transport.get_pipe_transport(0).close() # No more input
|
||||
|
@ -47,14 +48,14 @@ class _Protocol(asyncio.SubprocessProtocol):
|
|||
|
||||
def pipe_connection_lost(self, fd, exc):
|
||||
if fd == 0: # stdin
|
||||
print("stdin is closed. (we probably did it)")
|
||||
logging.info("stdin is closed. (we probably did it)")
|
||||
elif fd == 1: # stdout
|
||||
print("The child closed their stdout.")
|
||||
logging.info("The child closed their stdout.")
|
||||
elif fd == 2: # stderr
|
||||
print("The child closed their stderr.")
|
||||
logging.info("The child closed their stderr.")
|
||||
|
||||
def connection_lost(self, exc):
|
||||
print("Subprocess connection lost.")
|
||||
logging.info("Subprocess connection lost.")
|
||||
|
||||
def process_exited(self):
|
||||
if self.outer.logfile is not None:
|
||||
|
@ -62,10 +63,10 @@ class _Protocol(asyncio.SubprocessProtocol):
|
|||
self.transport.close()
|
||||
return_code = self.transport.get_returncode()
|
||||
if return_code is None:
|
||||
print("Unknown exit")
|
||||
logging.error("Unknown exit")
|
||||
self.outer.ended(1)
|
||||
return
|
||||
print("processEnded, status", return_code)
|
||||
logging.info(f"processEnded, status {return_code}")
|
||||
self.outer.ended(return_code)
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import os
|
|||
import shutil
|
||||
import atexit
|
||||
import time
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||
from PyQt5.QtGui import QCursor
|
||||
|
@ -74,7 +75,7 @@ class Backend(QObject):
|
|||
p = SubprocessWrapper()
|
||||
arg = 'x11vnc -opts'
|
||||
ret = p.run(arg)
|
||||
options = tuple(m.group(1) for m in re.finditer("\s*(-\w+)\s+", ret))
|
||||
options = tuple(m.group(1) for m in re.finditer(r"\s*(-\w+)\s+", ret))
|
||||
# Set/unset available x11vnc options flags in config
|
||||
with open(CONFIG_PATH, 'r') as f, open(DATA_PATH, 'r') as f_data:
|
||||
config = json.load(f)
|
||||
|
@ -93,6 +94,10 @@ class Backend(QObject):
|
|||
with open(CONFIG_PATH, 'w') as f:
|
||||
f.write(json.dumps(config, indent=4, sort_keys=True))
|
||||
|
||||
def promptError(self, msg):
|
||||
logging.error(msg)
|
||||
self.onError.emit(msg)
|
||||
|
||||
# Qt properties
|
||||
@pyqtProperty(str, constant=True)
|
||||
def settings(self):
|
||||
|
@ -118,7 +123,7 @@ class Backend(QObject):
|
|||
try:
|
||||
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
|
||||
except RuntimeError as e:
|
||||
self.onError.emit(str(e))
|
||||
self.promptError(str(e))
|
||||
return QQmlListProperty(DisplayProperty, self, [])
|
||||
|
||||
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
|
||||
|
@ -148,28 +153,28 @@ class Backend(QObject):
|
|||
@pyqtSlot(str, int, int, bool, bool)
|
||||
def createVirtScreen(self, device, width, height, portrait, hidpi, pos=''):
|
||||
self.xrandr.virt_name = device
|
||||
print("Creating a Virtual Screen...")
|
||||
logging.info("Creating a Virtual Screen...")
|
||||
try:
|
||||
self.xrandr.create_virtual_screen(width, height, portrait, hidpi, pos)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
return
|
||||
except RuntimeError as e:
|
||||
self.onError.emit(str(e))
|
||||
self.promptError(str(e))
|
||||
return
|
||||
self.virtScreenCreated = True
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteVirtScreen(self):
|
||||
print("Deleting the Virtual Screen...")
|
||||
logging.info("Deleting the Virtual Screen...")
|
||||
if self.vncState is not self.VNCState.OFF:
|
||||
self.onError.emit("Turn off the VNC server first")
|
||||
self.promptError("Turn off the VNC server first")
|
||||
self.virtScreenCreated = True
|
||||
return
|
||||
try:
|
||||
self.xrandr.delete_virtual_screen()
|
||||
except RuntimeError as e:
|
||||
self.onError.emit(str(e))
|
||||
self.promptError(str(e))
|
||||
return
|
||||
self.virtScreenCreated = False
|
||||
|
||||
|
@ -181,11 +186,11 @@ class Backend(QObject):
|
|||
try:
|
||||
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
self.promptError(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
return
|
||||
self.vncUsePassword = True
|
||||
else:
|
||||
self.onError.emit("Empty password")
|
||||
self.promptError("Empty password")
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteVNCPassword(self):
|
||||
|
@ -193,16 +198,16 @@ class Backend(QObject):
|
|||
os.remove(X11VNC_PASSWORD_PATH)
|
||||
self.vncUsePassword = False
|
||||
else:
|
||||
self.onError.emit("Failed deleting the password file")
|
||||
self.promptError("Failed deleting the password file")
|
||||
|
||||
@pyqtSlot(int)
|
||||
def startVNC(self, port):
|
||||
# Check if a virtual screen created
|
||||
if not self.virtScreenCreated:
|
||||
self.onError.emit("Virtual Screen not crated.")
|
||||
self.promptError("Virtual Screen not crated.")
|
||||
return
|
||||
if self.vncState is not self.VNCState.OFF:
|
||||
self.onError.emit("VNC Server is already running.")
|
||||
self.promptError("VNC Server is already running.")
|
||||
return
|
||||
# regex used in callbacks
|
||||
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
|
||||
|
@ -210,27 +215,27 @@ class Backend(QObject):
|
|||
|
||||
# define callbacks
|
||||
def _connected():
|
||||
print("VNC started.")
|
||||
logging.info("VNC started.")
|
||||
self.vncState = self.VNCState.WAITING
|
||||
|
||||
def _received(data):
|
||||
data = data.decode("utf-8")
|
||||
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
||||
print("VNC connected.")
|
||||
logging.info("VNC connected.")
|
||||
self.vncState = self.VNCState.CONNECTED
|
||||
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
|
||||
print("VNC disconnected.")
|
||||
logging.info("VNC disconnected.")
|
||||
self.vncState = self.VNCState.WAITING
|
||||
|
||||
def _ended(exitCode):
|
||||
if exitCode is not 0:
|
||||
self.vncState = self.VNCState.ERROR
|
||||
self.onError.emit('X11VNC: Error occurred.\n'
|
||||
self.promptError('X11VNC: Error occurred.\n'
|
||||
'Double check if the port is already used.')
|
||||
self.vncState = self.VNCState.OFF # TODO: better handling error state
|
||||
else:
|
||||
self.vncState = self.VNCState.OFF
|
||||
print("VNC Exited.")
|
||||
logging.info("VNC Exited.")
|
||||
atexit.unregister(self.stopVNC)
|
||||
# load settings
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
|
@ -250,7 +255,7 @@ class Backend(QObject):
|
|||
try:
|
||||
virt = self.xrandr.get_virtual_screen()
|
||||
except RuntimeError as e:
|
||||
self.onError.emit(str(e))
|
||||
self.promptError(str(e))
|
||||
return
|
||||
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
||||
arg = f"x11vnc -rfbport {port} -clip {clip} {options}"
|
||||
|
@ -264,20 +269,20 @@ class Backend(QObject):
|
|||
def openDisplaySetting(self, app: str = "arandr"):
|
||||
# define callbacks
|
||||
def _connected():
|
||||
print("External Display Setting opened.")
|
||||
logging.info("External Display Setting opened.")
|
||||
|
||||
def _received(data):
|
||||
pass
|
||||
|
||||
def _ended(exitCode):
|
||||
print("External Display Setting closed.")
|
||||
logging.info("External Display Setting closed.")
|
||||
self.onDisplaySettingClosed.emit()
|
||||
if exitCode is not 0:
|
||||
self.onError.emit(f'Error opening "{running_program}".')
|
||||
self.promptError(f'Error opening "{running_program}".')
|
||||
with open(DATA_PATH, 'r') as f:
|
||||
data = json.load(f)['displaySettingApps']
|
||||
if app not in data:
|
||||
self.onError.emit('Wrong display settings program')
|
||||
self.promptError('Wrong display settings program')
|
||||
return
|
||||
program_list = [data[app]['args'], "arandr"]
|
||||
program = AsyncSubprocess(_connected, _received, _received, _ended, None)
|
||||
|
@ -288,12 +293,12 @@ class Backend(QObject):
|
|||
running_program = arg
|
||||
program.run(arg)
|
||||
return
|
||||
self.onError.emit('Failed to find a display settings program.\n'
|
||||
'Please install ARandR package.\n'
|
||||
'(e.g. sudo apt-get install arandr)\n'
|
||||
'Please issue a feature request\n'
|
||||
'if you wish to add a display settings\n'
|
||||
'program for your Desktop Environment.')
|
||||
self.promptError('Failed to find a display settings program.\n'
|
||||
'Please install ARandR package.\n'
|
||||
'(e.g. sudo apt-get install arandr)\n'
|
||||
'Please issue a feature request\n'
|
||||
'if you wish to add a display settings\n'
|
||||
'program for your Desktop Environment.')
|
||||
|
||||
@pyqtSlot()
|
||||
def stopVNC(self, force=False):
|
||||
|
@ -304,7 +309,7 @@ class Backend(QObject):
|
|||
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
||||
self.vncServer.close()
|
||||
else:
|
||||
self.onError.emit("stopVNC called while it is not running")
|
||||
self.promptError("stopVNC called while it is not running")
|
||||
|
||||
@pyqtSlot()
|
||||
def clearCache(self):
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import re
|
||||
import atexit
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from .display import Display
|
||||
|
@ -53,9 +54,9 @@ class XRandR(SubprocessWrapper):
|
|||
screen.height = int(match.group(7))
|
||||
screen.x_offset = int(match.group(8))
|
||||
screen.y_offset = int(match.group(9))
|
||||
print("Display information:")
|
||||
logging.info("Display information:")
|
||||
for s in self.screens:
|
||||
print("\t", s)
|
||||
logging.info(f"\t{s}")
|
||||
if self.primary_idx is None:
|
||||
raise RuntimeError("There is no primary screen detected.\n"
|
||||
"Go to display settings and set\n"
|
||||
|
@ -108,7 +109,7 @@ class XRandR(SubprocessWrapper):
|
|||
|
||||
def create_virtual_screen(self, width, height, portrait=False, hidpi=False, pos='') -> None:
|
||||
self._update_screens()
|
||||
print("creating: ", self.virt)
|
||||
logging.info(f"creating: {self.virt}")
|
||||
self._add_screen_mode(width, height, portrait, hidpi)
|
||||
arg_pos = ['left', 'right', 'above', 'below']
|
||||
xrandr_pos = ['--left-of', '--right-of', '--above', '--below']
|
||||
|
|
Loading…
Reference in a new issue