More pep8 fixes. A tiny bit of refactoring.
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
# attrib:
|
||||
# http://rampex.ihep.su/Linux/linux_howto/html/tutorials/mini/Colour-ls-6.html
|
||||
# 0 = black, 1 = blue, 2 = green, 3 = cyan, 4 = red, 5 = purple, 6 = brown/yellow, 7 = white.
|
||||
@ -10,17 +10,21 @@
|
||||
# blink = 5 if attr & 1 else 0
|
||||
# bold = 1 if attr & 16 else 0
|
||||
|
||||
import subprocess
|
||||
import fcntl
|
||||
import glob
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import termios
|
||||
import time
|
||||
import select
|
||||
import dbus
|
||||
import fcntl
|
||||
from array import array
|
||||
from fcntl import ioctl
|
||||
from struct import unpack_from, unpack, pack
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from struct import unpack_from
|
||||
|
||||
import dbus
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.eventData import FenrirEventType
|
||||
from fenrirscreenreader.core.screenDriver import ScreenDriver as screenDriver
|
||||
@ -28,128 +32,230 @@ from fenrirscreenreader.utils import screen_utils
|
||||
|
||||
|
||||
class driver(screenDriver):
|
||||
"""Linux VCSA (Virtual Console Screen Access) driver for Fenrir screen reader.
|
||||
|
||||
This driver provides access to Linux virtual consoles (TTYs) through the VCSA
|
||||
interface, allowing real-time monitoring of screen content and cursor position.
|
||||
It supports both text content extraction and color/attribute detection.
|
||||
|
||||
The driver monitors multiple virtual consoles simultaneously and can detect:
|
||||
- Screen content changes (text updates)
|
||||
- Cursor movement
|
||||
- TTY switching
|
||||
- Text attributes (colors, bold, etc.)
|
||||
- Session information via D-Bus/logind
|
||||
|
||||
Attributes:
|
||||
ListSessions: D-Bus method for listing login sessions
|
||||
sysBus: D-Bus system bus connection
|
||||
charmap: Character mapping for text decoding
|
||||
bgColorValues: Background color value mappings
|
||||
fgColorValues: Foreground color value mappings
|
||||
hichar: High character mask for Unicode support
|
||||
"""
|
||||
def __init__(self):
|
||||
screenDriver.__init__(self)
|
||||
self.ListSessions = None
|
||||
self.sysBus = None
|
||||
self.charmap = {}
|
||||
self.bgColorValues = {
|
||||
0: 'black',
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
3: 'cyan',
|
||||
4: 'red',
|
||||
5: 'magenta',
|
||||
6: 'brown/yellow',
|
||||
7: 'white'}
|
||||
0: "black",
|
||||
1: "blue",
|
||||
2: "green",
|
||||
3: "cyan",
|
||||
4: "red",
|
||||
5: "magenta",
|
||||
6: "brown/yellow",
|
||||
7: "white",
|
||||
}
|
||||
self.fgColorValues = {
|
||||
0: 'black',
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
3: 'cyan',
|
||||
4: 'red',
|
||||
5: 'magenta',
|
||||
6: 'brown/yellow',
|
||||
7: 'light gray',
|
||||
8: 'dark gray',
|
||||
9: 'light blue',
|
||||
10: 'light green',
|
||||
11: 'light cyan',
|
||||
12: 'light red',
|
||||
13: 'light magenta',
|
||||
14: 'light yellow',
|
||||
15: 'white'}
|
||||
0: "black",
|
||||
1: "blue",
|
||||
2: "green",
|
||||
3: "cyan",
|
||||
4: "red",
|
||||
5: "magenta",
|
||||
6: "brown/yellow",
|
||||
7: "light gray",
|
||||
8: "dark gray",
|
||||
9: "light blue",
|
||||
10: "light green",
|
||||
11: "light cyan",
|
||||
12: "light red",
|
||||
13: "light magenta",
|
||||
14: "light yellow",
|
||||
15: "white",
|
||||
}
|
||||
self.hichar = None
|
||||
try:
|
||||
# set workaround for paste clipboard -> inject_text_to_screen
|
||||
subprocess.run(['sysctl', 'dev.tty.legacy_tiocsti=1'],
|
||||
check=False, capture_output=True, timeout=5)
|
||||
subprocess.run(
|
||||
["sysctl", "dev.tty.legacy_tiocsti=1"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver shutdown: Error running fgconsole: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver shutdown: Error running fgconsole: " + str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
|
||||
def initialize(self, environment):
|
||||
"""Initialize the VCSA driver with the given environment.
|
||||
|
||||
Sets up default attributes, starts the screen monitoring watchdog process,
|
||||
and prepares the driver for screen content monitoring.
|
||||
|
||||
Args:
|
||||
environment: The Fenrir environment dictionary containing runtime managers
|
||||
and configuration settings.
|
||||
"""
|
||||
self.env = environment
|
||||
self.env['runtime']['AttributeManager'].append_default_attributes([
|
||||
self.fgColorValues[7], # fg
|
||||
self.bgColorValues[0], # bg
|
||||
False, # bold
|
||||
False, # italics
|
||||
False, # underscore
|
||||
False, # strikethrough
|
||||
False, # reverse
|
||||
False, # blink
|
||||
'default', # fontsize
|
||||
'default' # fontfamily
|
||||
]) # end attribute )
|
||||
self.env['runtime']['ProcessManager'].add_custom_event_thread(
|
||||
self.update_watchdog, multiprocess=True)
|
||||
self.env["runtime"]["AttributeManager"].append_default_attributes(
|
||||
[
|
||||
self.fgColorValues[7], # fg
|
||||
self.bgColorValues[0], # bg
|
||||
False, # bold
|
||||
False, # italics
|
||||
False, # underscore
|
||||
False, # strikethrough
|
||||
False, # reverse
|
||||
False, # blink
|
||||
"default", # fontsize
|
||||
"default", # fontfamily
|
||||
]
|
||||
) # end attribute )
|
||||
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
||||
self.update_watchdog, multiprocess=True
|
||||
)
|
||||
|
||||
def get_curr_screen(self):
|
||||
self.env['screen']['oldTTY'] = self.env['screen']['newTTY']
|
||||
"""Get the currently active TTY number.
|
||||
|
||||
Reads from /sys/devices/virtual/tty/tty0/active to determine which
|
||||
virtual console is currently active and updates the environment.
|
||||
|
||||
Updates:
|
||||
env['screen']['oldTTY']: Previous TTY number
|
||||
env['screen']['newTTY']: Current TTY number
|
||||
"""
|
||||
self.env["screen"]["oldTTY"] = self.env["screen"]["newTTY"]
|
||||
try:
|
||||
with open('/sys/devices/virtual/tty/tty0/active', 'r') as currScreenFile:
|
||||
self.env['screen']['newTTY'] = str(currScreenFile.read()[3:-1])
|
||||
with open(
|
||||
"/sys/devices/virtual/tty/tty0/active", "r"
|
||||
) as currScreenFile:
|
||||
self.env["screen"]["newTTY"] = str(currScreenFile.read()[3:-1])
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
def inject_text_to_screen(self, text, screen=None):
|
||||
use_screen = "/dev/tty" + self.env['screen']['newTTY']
|
||||
"""Inject text into the specified screen as if typed by user.
|
||||
|
||||
Uses the TIOCSTI ioctl to simulate keystrokes on the target TTY.
|
||||
This is primarily used for clipboard paste functionality.
|
||||
|
||||
Args:
|
||||
text (str): Text to inject into the screen
|
||||
screen (str, optional): Target screen device (e.g., '/dev/tty1').
|
||||
If None, uses current TTY.
|
||||
|
||||
Note:
|
||||
Requires appropriate permissions and may need legacy_tiocsti=1
|
||||
kernel parameter on newer systems.
|
||||
"""
|
||||
use_screen = "/dev/tty" + self.env["screen"]["newTTY"]
|
||||
if screen is not None:
|
||||
use_screen = screen
|
||||
with open(use_screen, 'w') as fd:
|
||||
with open(use_screen, "w") as fd:
|
||||
for c in text:
|
||||
fcntl.ioctl(fd, termios.TIOCSTI, c)
|
||||
|
||||
def get_session_information(self):
|
||||
self.env['screen']['autoIgnoreScreens'] = []
|
||||
"""Retrieve session information via D-Bus logind interface.
|
||||
|
||||
Connects to systemd-logind to gather information about active sessions,
|
||||
including session types and TTY assignments. This helps identify which
|
||||
screens should be automatically ignored (e.g., X11 sessions).
|
||||
|
||||
Updates:
|
||||
env['screen']['autoIgnoreScreens']: List of screens to ignore
|
||||
env['general']['curr_user']: Current user for active session
|
||||
env['general']['prev_user']: Previous user
|
||||
|
||||
Note:
|
||||
Gracefully handles cases where logind is not available.
|
||||
"""
|
||||
self.env["screen"]["autoIgnoreScreens"] = []
|
||||
try:
|
||||
if not self.sysBus:
|
||||
self.sysBus = dbus.SystemBus()
|
||||
obj = self.sysBus.get_object(
|
||||
'org.freedesktop.login1', '/org/freedesktop/login1')
|
||||
inf = dbus.Interface(obj, 'org.freedesktop.login1.Manager')
|
||||
self.ListSessions = inf.get_dbus_method('ListSessions')
|
||||
"org.freedesktop.login1", "/org/freedesktop/login1"
|
||||
)
|
||||
inf = dbus.Interface(obj, "org.freedesktop.login1.Manager")
|
||||
self.ListSessions = inf.get_dbus_method("ListSessions")
|
||||
sessions = self.ListSessions()
|
||||
|
||||
for session in sessions:
|
||||
obj = self.sysBus.get_object(
|
||||
'org.freedesktop.login1', session[4])
|
||||
inf = dbus.Interface(obj, 'org.freedesktop.DBus.Properties')
|
||||
session_type = inf.Get('org.freedesktop.login1.Session', 'Type')
|
||||
screen = str(inf.Get('org.freedesktop.login1.Session', 'VTNr'))
|
||||
if screen == '':
|
||||
"org.freedesktop.login1", session[4]
|
||||
)
|
||||
inf = dbus.Interface(obj, "org.freedesktop.DBus.Properties")
|
||||
session_type = inf.Get(
|
||||
"org.freedesktop.login1.Session", "Type"
|
||||
)
|
||||
screen = str(inf.Get("org.freedesktop.login1.Session", "VTNr"))
|
||||
if screen == "":
|
||||
screen = str(
|
||||
inf.Get(
|
||||
'org.freedesktop.login1.Session',
|
||||
'TTY'))
|
||||
screen = screen[screen.upper().find('TTY') + 3:]
|
||||
if screen == '':
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'No TTY found for session:' + session[4], debug.DebugLevel.ERROR)
|
||||
inf.Get("org.freedesktop.login1.Session", "TTY")
|
||||
)
|
||||
screen = screen[screen.upper().find("TTY") + 3 :]
|
||||
if screen == "":
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"No TTY found for session:" + session[4],
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
return
|
||||
if session_type.upper() != 'TTY':
|
||||
self.env['screen']['autoIgnoreScreens'] += [screen]
|
||||
if screen == self.env['screen']['newTTY']:
|
||||
if self.env['general']['curr_user'] != session[2]:
|
||||
self.env['general']['prev_user'] = self.env['general']['curr_user']
|
||||
self.env['general']['curr_user'] = session[2]
|
||||
if session_type.upper() != "TTY":
|
||||
self.env["screen"]["autoIgnoreScreens"] += [screen]
|
||||
if screen == self.env["screen"]["newTTY"]:
|
||||
if self.env["general"]["curr_user"] != session[2]:
|
||||
self.env["general"]["prev_user"] = self.env["general"][
|
||||
"curr_user"
|
||||
]
|
||||
self.env["general"]["curr_user"] = session[2]
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'get_session_information: Maybe no LoginD:' + str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"get_session_information: Maybe no LoginD:" + str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
# self.env['runtime']['DebugManager'].write_debug_out('get_session_information:' + str(self.env['screen']['autoIgnoreScreens']) + ' ' + str(self.env['general']) ,debug.DebugLevel.INFO)
|
||||
|
||||
def read_file(self, file):
|
||||
d = b''
|
||||
"""Read content from a file handle with error recovery.
|
||||
|
||||
Attempts to read the entire file content, falling back to
|
||||
line-by-line reading if the initial read fails. This is used
|
||||
for reading VCSA/VCSU device files.
|
||||
|
||||
Args:
|
||||
file: Open file handle to read from
|
||||
|
||||
Returns:
|
||||
bytes: File content as bytes
|
||||
"""
|
||||
d = b""
|
||||
file.seek(0)
|
||||
try:
|
||||
d = file.read()
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver get_screen_text: Error reading file: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver get_screen_text: Error reading file: " + str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
file.seek(0)
|
||||
while True:
|
||||
# Read from file
|
||||
@ -162,40 +268,64 @@ class driver(screenDriver):
|
||||
return d
|
||||
|
||||
def update_watchdog(self, active, event_queue):
|
||||
"""Main watchdog loop for monitoring screen changes.
|
||||
|
||||
This is the core monitoring function that runs in a separate process.
|
||||
It uses epoll to efficiently monitor multiple VCSA devices and the
|
||||
active TTY file for changes. When changes are detected, it generates
|
||||
appropriate events for the main Fenrir process.
|
||||
|
||||
The watchdog monitors:
|
||||
- Screen content changes (text updates)
|
||||
- TTY switches (screen changes)
|
||||
- Cursor position changes
|
||||
|
||||
Args:
|
||||
active: Shared boolean value controlling the watchdog loop
|
||||
event_queue: Queue for sending events to the main process
|
||||
|
||||
Events Generated:
|
||||
- FenrirEventType.screen_changed: When switching TTYs
|
||||
- FenrirEventType.screen_update: When screen content changes
|
||||
|
||||
Note:
|
||||
This method runs in a multiprocess context and includes comprehensive
|
||||
cleanup of file handles in the finally block.
|
||||
"""
|
||||
vcsa = {}
|
||||
vcsu = {}
|
||||
tty = None
|
||||
watchdog = None
|
||||
try:
|
||||
use_vcsu = os.access('/dev/vcsu', os.R_OK)
|
||||
vcsa_devices = glob.glob('/dev/vcsa*')
|
||||
use_vcsu = os.access("/dev/vcsu", os.R_OK)
|
||||
vcsa_devices = glob.glob("/dev/vcsa*")
|
||||
vcsu_devices = None
|
||||
last_screen_content = b''
|
||||
last_screen_content = b""
|
||||
|
||||
# Open TTY file with proper cleanup
|
||||
tty = open('/sys/devices/virtual/tty/tty0/active', 'r')
|
||||
tty = open("/sys/devices/virtual/tty/tty0/active", "r")
|
||||
curr_screen = str(tty.read()[3:-1])
|
||||
old_screen = curr_screen
|
||||
|
||||
# Open VCSA devices with proper cleanup tracking
|
||||
for vcsaDev in vcsa_devices:
|
||||
index = str(vcsaDev[9:])
|
||||
vcsa[index] = open(vcsaDev, 'rb')
|
||||
vcsa[index] = open(vcsaDev, "rb")
|
||||
if index == curr_screen:
|
||||
last_screen_content = self.read_file(vcsa[index])
|
||||
|
||||
# Open VCSU devices if available
|
||||
if use_vcsu:
|
||||
vcsu_devices = glob.glob('/dev/vcsu*')
|
||||
vcsu_devices = glob.glob("/dev/vcsu*")
|
||||
for vcsuDev in vcsu_devices:
|
||||
index = str(vcsuDev[9:])
|
||||
vcsu[index] = open(vcsuDev, 'rb')
|
||||
vcsu[index] = open(vcsuDev, "rb")
|
||||
|
||||
self.update_char_map(curr_screen)
|
||||
watchdog = select.epoll()
|
||||
watchdog.register(
|
||||
vcsa[curr_screen],
|
||||
select.POLLPRI | select.POLLERR)
|
||||
vcsa[curr_screen], select.POLLPRI | select.POLLERR
|
||||
)
|
||||
watchdog.register(tty, select.POLLPRI | select.POLLERR)
|
||||
while active.value:
|
||||
changes = watchdog.poll(1)
|
||||
@ -203,43 +333,70 @@ class driver(screenDriver):
|
||||
fileno = change[0]
|
||||
event = change[1]
|
||||
if fileno == tty.fileno():
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'ScreenChange', debug.DebugLevel.INFO)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"ScreenChange", debug.DebugLevel.INFO
|
||||
)
|
||||
tty.seek(0)
|
||||
curr_screen = str(tty.read()[3:-1])
|
||||
if curr_screen != old_screen:
|
||||
try:
|
||||
watchdog.unregister(vcsa[old_screen])
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver update_watchdog: Error unregistering watchdog: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"][
|
||||
"DebugManager"
|
||||
].write_debug_out(
|
||||
"vcsaDriver update_watchdog: Error unregistering watchdog: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
try:
|
||||
watchdog.register(
|
||||
vcsa[curr_screen], select.POLLPRI | select.POLLERR)
|
||||
vcsa[curr_screen],
|
||||
select.POLLPRI | select.POLLERR,
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver update_watchdog: Error registering watchdog: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"][
|
||||
"DebugManager"
|
||||
].write_debug_out(
|
||||
"vcsaDriver update_watchdog: Error registering watchdog: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.update_char_map(curr_screen)
|
||||
old_screen = curr_screen
|
||||
try:
|
||||
vcsa[curr_screen].seek(0)
|
||||
last_screen_content = self.read_file(
|
||||
vcsa[curr_screen])
|
||||
vcsa[curr_screen]
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver update_watchdog: Error reading screen content: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"][
|
||||
"DebugManager"
|
||||
].write_debug_out(
|
||||
"vcsaDriver update_watchdog: Error reading screen content: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
vcsu_content = None
|
||||
if use_vcsu:
|
||||
vcsu[curr_screen].seek(0)
|
||||
vcsu_content = self.read_file(vcsu[curr_screen])
|
||||
event_queue.put({"Type": FenrirEventType.screen_changed, "data": self.create_screen_event_data(
|
||||
curr_screen, last_screen_content, vcsu_content)})
|
||||
vcsu_content = self.read_file(
|
||||
vcsu[curr_screen]
|
||||
)
|
||||
event_queue.put(
|
||||
{
|
||||
"Type": FenrirEventType.screen_changed,
|
||||
"data": self.create_screen_event_data(
|
||||
curr_screen,
|
||||
last_screen_content,
|
||||
vcsu_content,
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'screen_update', debug.DebugLevel.INFO)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"screen_update", debug.DebugLevel.INFO
|
||||
)
|
||||
vcsa[curr_screen].seek(0)
|
||||
time.sleep(0.01)
|
||||
dirty_content = self.read_file(vcsa[curr_screen])
|
||||
@ -247,16 +404,32 @@ class driver(screenDriver):
|
||||
vcsu_content = None
|
||||
timeout = time.time()
|
||||
# error case
|
||||
if screen_content == b'':
|
||||
if screen_content == b"":
|
||||
continue
|
||||
if last_screen_content == b'':
|
||||
if last_screen_content == b"":
|
||||
last_screen_content = screen_content
|
||||
if (abs(int(screen_content[2]) - int(last_screen_content[2])) in [1, 2]) and (
|
||||
int(screen_content[3]) == int(last_screen_content[3])):
|
||||
if (
|
||||
abs(
|
||||
int(screen_content[2])
|
||||
- int(last_screen_content[2])
|
||||
)
|
||||
in [1, 2]
|
||||
) and (
|
||||
int(screen_content[3])
|
||||
== int(last_screen_content[3])
|
||||
):
|
||||
# Skip X Movement
|
||||
pass
|
||||
elif (abs(int(screen_content[3]) - int(last_screen_content[3])) in [1]) and \
|
||||
(int(screen_content[2]) == int(last_screen_content[2])):
|
||||
elif (
|
||||
abs(
|
||||
int(screen_content[3])
|
||||
- int(last_screen_content[3])
|
||||
)
|
||||
in [1]
|
||||
) and (
|
||||
int(screen_content[2])
|
||||
== int(last_screen_content[2])
|
||||
):
|
||||
# Skip Y Movement
|
||||
pass
|
||||
else:
|
||||
@ -268,7 +441,9 @@ class driver(screenDriver):
|
||||
# if not vcsa[curr_screen] in r:
|
||||
# break
|
||||
vcsa[curr_screen].seek(0)
|
||||
dirty_content = self.read_file(vcsa[curr_screen])
|
||||
dirty_content = self.read_file(
|
||||
vcsa[curr_screen]
|
||||
)
|
||||
if screen_content == dirty_content:
|
||||
break
|
||||
if time.time() - timeout >= 0.1:
|
||||
@ -278,11 +453,18 @@ class driver(screenDriver):
|
||||
vcsu[curr_screen].seek(0)
|
||||
vcsu_content = self.read_file(vcsu[curr_screen])
|
||||
last_screen_content = screen_content
|
||||
event_queue.put({"Type": FenrirEventType.screen_update, "data": self.create_screen_event_data(
|
||||
curr_screen, screen_content, vcsu_content)})
|
||||
event_queue.put(
|
||||
{
|
||||
"Type": FenrirEventType.screen_update,
|
||||
"data": self.create_screen_event_data(
|
||||
curr_screen, screen_content, vcsu_content
|
||||
),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'VCSA:update_watchdog:' + str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"VCSA:update_watchdog:" + str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
time.sleep(0.2)
|
||||
finally:
|
||||
# Clean up all file handles
|
||||
@ -290,114 +472,186 @@ class driver(screenDriver):
|
||||
if watchdog:
|
||||
watchdog.close()
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver update_watchdog: Error closing watchdog: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver update_watchdog: Error closing watchdog: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
try:
|
||||
if tty:
|
||||
tty.close()
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver shutdown: Error closing TTY: ' + str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver shutdown: Error closing TTY: " + str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
for handle in vcsa.values():
|
||||
try:
|
||||
handle.close()
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver shutdown: Error closing VCSA handle: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver shutdown: Error closing VCSA handle: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
for handle in vcsu.values():
|
||||
try:
|
||||
handle.close()
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver shutdown: Error closing VCSU handle: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver shutdown: Error closing VCSU handle: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
|
||||
def create_screen_event_data(self, screen, vcsaContent, vcsu_content=None):
|
||||
"""Create standardized screen event data from VCSA content.
|
||||
|
||||
Processes raw VCSA bytes into a structured event data dictionary
|
||||
containing screen dimensions, cursor position, text content, and
|
||||
color attributes.
|
||||
|
||||
Args:
|
||||
screen (str): TTY number (e.g., '1' for tty1)
|
||||
vcsaContent (bytes): Raw VCSA device content
|
||||
vcsu_content (bytes, optional): VCSU content for Unicode support
|
||||
|
||||
Returns:
|
||||
dict: Event data with keys:
|
||||
- bytes: Raw VCSA content
|
||||
- lines: Screen height
|
||||
- columns: Screen width
|
||||
- textCursor: Cursor position {x, y}
|
||||
- screen: TTY number
|
||||
- screenUpdateTime: Timestamp
|
||||
- text: Decoded text content
|
||||
- attributes: Color/formatting attributes
|
||||
"""
|
||||
event_data = {
|
||||
'bytes': vcsaContent,
|
||||
'lines': int(vcsaContent[0]),
|
||||
'columns': int(vcsaContent[1]),
|
||||
'textCursor':
|
||||
{
|
||||
'x': int(vcsaContent[2]),
|
||||
'y': int(vcsaContent[3])
|
||||
},
|
||||
'screen': screen,
|
||||
'screenUpdateTime': time.time(),
|
||||
'text': '',
|
||||
'attributes': [],
|
||||
"bytes": vcsaContent,
|
||||
"lines": int(vcsaContent[0]),
|
||||
"columns": int(vcsaContent[1]),
|
||||
"textCursor": {"x": int(vcsaContent[2]), "y": int(vcsaContent[3])},
|
||||
"screen": screen,
|
||||
"screenUpdateTime": time.time(),
|
||||
"text": "",
|
||||
"attributes": [],
|
||||
}
|
||||
try:
|
||||
event_data['text'], event_data['attributes'] = self.auto_decode_vcsa(
|
||||
vcsaContent[4:], event_data['lines'], event_data['columns'])
|
||||
event_data["text"], event_data["attributes"] = (
|
||||
self.auto_decode_vcsa(
|
||||
vcsaContent[4:], event_data["lines"], event_data["columns"]
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver create_screen_event_data: Error decoding VCSA content: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver create_screen_event_data: Error decoding VCSA content: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
# VCSU seems to give b' ' instead of b'\x00\x00\x00' (tsp),
|
||||
# deactivated until its fixed
|
||||
if vcsu_content is not None:
|
||||
try:
|
||||
vcsu_content_as_text = vcsu_content.decode('UTF-32')
|
||||
event_data['text'] = screen_utils.insert_newlines(
|
||||
vcsu_content_as_text, event_data['columns'])
|
||||
vcsu_content_as_text = vcsu_content.decode("UTF-32")
|
||||
event_data["text"] = screen_utils.insert_newlines(
|
||||
vcsu_content_as_text, event_data["columns"]
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver create_screen_event_data: Error decoding VCSU content: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver create_screen_event_data: Error decoding VCSU content: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
return event_data.copy()
|
||||
|
||||
def update_char_map(self, screen):
|
||||
"""Update character mapping for the specified screen.
|
||||
|
||||
Reads the Unicode font mapping from the TTY to properly decode
|
||||
character data from VCSA. This handles special characters and
|
||||
Unicode properly.
|
||||
|
||||
Args:
|
||||
screen (str): TTY number to update mapping for
|
||||
|
||||
Updates:
|
||||
self.charmap: Dictionary mapping byte values to Unicode characters
|
||||
self.hichar: High character mask for extended characters
|
||||
"""
|
||||
self.charmap = {}
|
||||
try:
|
||||
with open('/dev/tty' + screen, 'rb') as tty:
|
||||
with open("/dev/tty" + screen, "rb") as tty:
|
||||
GIO_UNIMAP = 0x4B66
|
||||
VT_GETHIFONTMASK = 0x560D
|
||||
himask = array("H", (0,))
|
||||
ioctl(tty, VT_GETHIFONTMASK, himask)
|
||||
self.hichar, = unpack_from("@H", himask)
|
||||
(self.hichar,) = unpack_from("@H", himask)
|
||||
sz = 512
|
||||
line = ''
|
||||
line = ""
|
||||
while True:
|
||||
try:
|
||||
unipairs = array("H", [0] * (2 * sz))
|
||||
unimapdesc = array(
|
||||
"B", pack(
|
||||
"@HP", sz, unipairs.buffer_info()[0]))
|
||||
"B", pack("@HP", sz, unipairs.buffer_info()[0])
|
||||
)
|
||||
ioctl(tty.fileno(), GIO_UNIMAP, unimapdesc)
|
||||
break
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'VCSA:update_char_map:scaling up sz=' + str(sz) + ' ' + str(e),
|
||||
debug.DebugLevel.WARNING)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"VCSA:update_char_map:scaling up sz="
|
||||
+ str(sz)
|
||||
+ " "
|
||||
+ str(e),
|
||||
debug.DebugLevel.WARNING,
|
||||
)
|
||||
sz *= 2
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'VCSA:update_char_map:' + str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"VCSA:update_char_map:" + str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
return
|
||||
ncodes, = unpack_from("@H", unimapdesc)
|
||||
(ncodes,) = unpack_from("@H", unimapdesc)
|
||||
utable = unpack_from("@%dH" % (2 * ncodes), unipairs)
|
||||
for u, b in zip(utable[::2], utable[1::2]):
|
||||
if self.charmap.get(b) is None:
|
||||
self.charmap[b] = chr(u)
|
||||
|
||||
def auto_decode_vcsa(self, allData, rows, cols):
|
||||
all_text = ''
|
||||
"""Decode raw VCSA data into text and attributes.
|
||||
|
||||
Processes the character and attribute data from VCSA devices,
|
||||
extracting both the text content and formatting information
|
||||
(colors, bold, blink, etc.).
|
||||
|
||||
Args:
|
||||
allData (bytes): Raw character and attribute data from VCSA
|
||||
rows (int): Number of screen rows
|
||||
cols (int): Number of screen columns
|
||||
|
||||
Returns:
|
||||
tuple: (text_content, attributes)
|
||||
- text_content (str): Decoded text with newlines
|
||||
- attributes (list): List of attribute arrays for each character
|
||||
|
||||
Note:
|
||||
Each character in VCSA is stored as 2 bytes: character + attribute.
|
||||
Attributes encode foreground/background colors, bold, blink, etc.
|
||||
"""
|
||||
all_text = ""
|
||||
all_attrib = []
|
||||
i = 0
|
||||
for y in range(rows):
|
||||
line_text = ''
|
||||
line_text = ""
|
||||
line_attrib = []
|
||||
blink = 0
|
||||
bold = 0
|
||||
ink = 7
|
||||
paper = 0
|
||||
for x in range(cols):
|
||||
data = allData[i: i + 2]
|
||||
data = allData[i : i + 2]
|
||||
i += 2
|
||||
if data == b' \x07':
|
||||
if data == b" \x07":
|
||||
# attr = 7
|
||||
# ink = 7
|
||||
# paper = 0
|
||||
@ -411,10 +665,11 @@ class driver(screenDriver):
|
||||
False, # strikethrough
|
||||
False, # reverse
|
||||
False, # blink
|
||||
'default', # fontsize
|
||||
'default'] # fontfamily
|
||||
"default", # fontsize
|
||||
"default",
|
||||
] # fontfamily
|
||||
line_attrib.append(char_attrib)
|
||||
line_text += ' '
|
||||
line_text += " "
|
||||
continue
|
||||
ch = None
|
||||
try:
|
||||
@ -425,9 +680,11 @@ class driver(screenDriver):
|
||||
if sh & self.hichar:
|
||||
ch |= 0x100
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver auto_decode_vcsa: Error processing character: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver auto_decode_vcsa: Error processing character: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
ch = None
|
||||
if self.hichar == 0x100:
|
||||
attr >>= 1
|
||||
@ -443,13 +700,15 @@ class driver(screenDriver):
|
||||
# if (ink != 7) or (paper != 0):
|
||||
# print(ink,paper)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
'vcsaDriver auto_decode_vcsa: Error processing attributes: ' + str(e),
|
||||
debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"vcsaDriver auto_decode_vcsa: Error processing attributes: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
try:
|
||||
line_text += self.charmap[ch]
|
||||
except KeyError:
|
||||
line_text += '?'
|
||||
line_text += "?"
|
||||
|
||||
char_attrib = [
|
||||
self.fgColorValues[ink],
|
||||
@ -460,30 +719,47 @@ class driver(screenDriver):
|
||||
False, # strikethrough
|
||||
False, # reverse
|
||||
blink == 1, # blink
|
||||
'default', # fontsize
|
||||
'default'] # fontfamily
|
||||
"default", # fontsize
|
||||
"default",
|
||||
] # fontfamily
|
||||
line_attrib.append(char_attrib)
|
||||
all_text += line_text
|
||||
if y + 1 < rows:
|
||||
all_text += '\n'
|
||||
all_text += "\n"
|
||||
all_attrib.append(line_attrib)
|
||||
return str(all_text), all_attrib
|
||||
|
||||
def get_curr_application(self):
|
||||
"""Detect the currently running application on the active TTY.
|
||||
|
||||
Uses 'ps' command to identify which process is currently in the
|
||||
foreground on the active TTY, enabling application-specific features
|
||||
like bookmarks and settings.
|
||||
|
||||
Updates:
|
||||
env['screen']['new_application']: Name of current application
|
||||
|
||||
Note:
|
||||
Filters out common shell processes (grep, sh, ps) to find the
|
||||
actual user application.
|
||||
"""
|
||||
apps = []
|
||||
try:
|
||||
curr_screen = self.env['screen']['newTTY']
|
||||
apps = subprocess.Popen(
|
||||
'ps -t tty' +
|
||||
curr_screen +
|
||||
' -o comm,tty,stat',
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE).stdout.read().decode()[
|
||||
:-
|
||||
1].split('\n')
|
||||
curr_screen = self.env["screen"]["newTTY"]
|
||||
apps = (
|
||||
subprocess.Popen(
|
||||
"ps -t tty" + curr_screen + " -o comm,tty,stat",
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
.stdout.read()
|
||||
.decode()[:-1]
|
||||
.split("\n")
|
||||
)
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
return
|
||||
try:
|
||||
for i in apps:
|
||||
@ -491,15 +767,23 @@ class driver(screenDriver):
|
||||
i = i.split()
|
||||
i[0] = i[0]
|
||||
i[1] = i[1]
|
||||
if '+' in i[2]:
|
||||
if i[0] != '':
|
||||
if not "GREP" == i[0] and \
|
||||
not "SH" == i[0] and \
|
||||
not "PS" == i[0]:
|
||||
if "+" in i[2]:
|
||||
if i[0] != "":
|
||||
if (
|
||||
not "GREP" == i[0]
|
||||
and not "SH" == i[0]
|
||||
and not "PS" == i[0]
|
||||
):
|
||||
if "TTY" + curr_screen in i[1]:
|
||||
if self.env['screen']['new_application'] != i[0]:
|
||||
self.env['screen']['new_application'] = i[0]
|
||||
if (
|
||||
self.env["screen"]["new_application"]
|
||||
!= i[0]
|
||||
):
|
||||
self.env["screen"]["new_application"] = i[
|
||||
0
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
self.env['runtime']['DebugManager'].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
Reference in New Issue
Block a user