Skip to content

Instantly share code, notes, and snippets.

@azazar
Created January 8, 2025 11:22
Show Gist options
  • Save azazar/ccccdedd997599a391316a2e984bad10 to your computer and use it in GitHub Desktop.
Save azazar/ccccdedd997599a391316a2e984bad10 to your computer and use it in GitHub Desktop.
A script that provides remote access to a single window using VNC over LAN.
#!/usr/bin/env python3
"""
vnc_single_window_wx_xlib_dbus.py
A Python script that:
- Uses python3-xlib (Xlib) to list available windows (plus a "root" option).
- Displays a wxPython GUI (ListBox) of windows.
- On selection, we run x11vnc on that window (passwordless + accept popup).
- Publishes the chosen window name via Avahi (Zeroconf) using python3-dbus instead of avahi-publish-service.
Dependencies (Debian/Ubuntu):
sudo apt-get install python3-wxgtk4.0 python3-xlib python3-dbus x11vnc
Optional:
python3-avahi, dbus-x11, etc.
NOTE:
- This script won't see windows on pure Wayland unless they're running under XWayland.
- We still rely on x11vnc for actual VNC service.
"""
import sys
import time
import signal
import subprocess
import wx
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from Xlib import display, X, Xutil
# Default VNC port:
VNC_PORT = 5900
##############################################################################
# WINDOW ENUMERATION (Xlib) #
##############################################################################
def list_windows_xlib():
"""
Use python3-xlib to list all normal windows (ID + title) plus 'root'.
Filters out unmapped/icon/no-name windows (similar to xwininfo approach).
Return: list of (wid_str, title)
wid_str is like 'root' or '0x1234abcd' (hex)
"""
results = []
# Always offer 'root' first
results.append(("root", "Entire_Desktop"))
try:
d = display.Display()
except Exception as e:
print(f"Warning: Unable to open X display: {e}", file=sys.stderr)
# We only have "root"
return results
root = d.screen().root
# We'll do a recursive walk to find all children
def recurse(window):
# Query children
try:
children = window.query_tree().children
except:
# If query_tree fails for any reason, skip
return
for w in children:
# Try to read the window's attributes
try:
attrs = w.get_attributes()
except:
continue
# We only want windows that are viewable or mapped
# Checking MapState might be used, or we can skip by other heuristics
if attrs.map_state != X.IsViewable:
# It's unmapped or icon, skip
continue
# Try to get the window's name
title = get_window_name(d, w)
if title is not None and title.strip():
# Build a hex string for the window ID
wid_hex = f"0x{w.id:x}"
results.append((wid_hex, title))
# Recurse children
recurse(w)
# Start recursion from root
recurse(root)
d.close()
return results
def get_window_name(d, w):
"""
Retrieve a window's name/title using a few properties:
- _NET_WM_NAME (UTF-8)
- WM_NAME (fallback)
Return a string or None if not found.
"""
# Xlib constants for atoms
NET_WM_NAME = d.intern_atom('_NET_WM_NAME')
UTF8_STRING = d.intern_atom('UTF8_STRING')
WM_NAME = d.intern_atom('WM_NAME')
# Try _NET_WM_NAME
try:
prop = w.get_full_property(NET_WM_NAME, UTF8_STRING)
if (prop is not None) and (prop.value):
return prop.value.decode('utf-8', errors='replace')
except:
pass
# Fallback to regular WM_NAME
try:
prop = w.get_full_property(WM_NAME, X.AnyPropertyType)
if (prop is not None) and (prop.value):
# Might be bytes or string
return prop.value.decode('utf-8', errors='replace')
except:
pass
# No name
return None
##############################################################################
# AVAHI PUBLISH (DBus) #
##############################################################################
class AvahiPublisher:
"""
Uses DBus to talk to Avahi, publishing an RFB (VNC) service.
Replaces the 'avahi-publish-service' command-line approach.
You can call .publish(service_name, port) to publish,
and .unpublish() to remove the service.
"""
def __init__(self):
# Use a main loop for DBus
DBusGMainLoop(set_as_default=True)
# Connect to Avahi on the system bus
self.bus = dbus.SystemBus()
self.server = dbus.Interface(
self.bus.get_object('org.freedesktop.Avahi', '/'),
'org.freedesktop.Avahi.Server'
)
# We'll create an EntryGroup for our service
path = self.server.EntryGroupNew()
self.entry_group = dbus.Interface(
self.bus.get_object('org.freedesktop.Avahi', path),
'org.freedesktop.Avahi.EntryGroup'
)
self.published = False
def publish(self, service_name, port):
"""
Publish the RFB (VNC) service at the given port, named 'service_name'.
"""
if self.published:
return
interface = -1 # AVAHI_IF_UNSPEC
protocol = -1 # AVAHI_PROTO_UNSPEC
flags = 0 # No special flags
# type is '_rfb._tcp' for VNC
# domain and host default to empty => use Avahi defaults
# txt records => empty array for now
self.entry_group.AddService(
interface, protocol, flags,
service_name, '_rfb._tcp', '', '', port,
dbus.Array([], signature='ay')
)
self.entry_group.Commit()
self.published = True
print(f"Avahi: Published service '{service_name}' on port {port}.")
def unpublish(self):
"""
Unpublish the service (if published).
"""
if self.published:
self.entry_group.Reset()
self.published = False
print("Avahi: Service unpublished.")
##############################################################################
# WX GUI FOR WINDOW SELECTION #
##############################################################################
class MainFrame(wx.Frame):
"""
wx Frame to display the list of windows in a ListBox, plus 'root' for entire desktop.
"""
def __init__(self, parent, windows, title="Select a Window for x11vnc"):
super(MainFrame, self).__init__(parent, title=title, size=(600, 400))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
instr_label = wx.StaticText(panel, label="Select a window (double-click or select + OK):")
sizer.Add(instr_label, 0, wx.ALL, 5)
self.windows = windows # list of (wid_str, wtitle)
self.listbox = wx.ListBox(panel, style=wx.LB_SINGLE)
for wid, wtitle in self.windows:
label = f"[{wid}] {wtitle}"
self.listbox.Append(label)
sizer.Add(self.listbox, 1, wx.EXPAND | wx.ALL, 5)
# Buttons
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
ok_button = wx.Button(panel, label="OK")
cancel_button = wx.Button(panel, label="Cancel")
btn_sizer.Add(ok_button, 0, wx.ALL, 5)
btn_sizer.Add(cancel_button, 0, wx.ALL, 5)
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
# Bind events
ok_button.Bind(wx.EVT_BUTTON, self.on_ok)
cancel_button.Bind(wx.EVT_BUTTON, self.on_cancel)
self.listbox.Bind(wx.EVT_LISTBOX_DCLICK, self.on_ok)
panel.SetSizer(sizer)
self.Centre()
self.selected = None
def on_ok(self, event):
sel = self.listbox.GetSelection()
if sel == wx.NOT_FOUND:
wx.MessageBox("No window selected. Please pick one.", "Warning", wx.OK | wx.ICON_WARNING)
return
self.selected = self.windows[sel]
self.Close()
def on_cancel(self, event):
self.selected = None
self.Close()
def get_selection(self):
return self.selected
class App(wx.App):
def __init__(self, windows):
self.windows = windows
super().__init__()
def OnInit(self):
self.frame = MainFrame(None, self.windows)
self.frame.Show()
return True
def get_selection(self):
return self.frame.get_selection()
##############################################################################
# RUN x11vnc #
##############################################################################
def run_x11vnc(window_id, port):
"""
Launch x11vnc on `window_id` (string like 'root' or '0x1234abcd'),
in passwordless mode, requiring manual acceptance for each connection.
Blocks until x11vnc exits.
"""
print(f"Starting x11vnc on window '{window_id}', port {port}, passwordless + accept popup.")
if window_id == "root":
cmd = [
"x11vnc",
"-id", "root",
"-rfbport", str(port),
"-shared",
"-forever",
"-nopw",
"-accept", "popup"
]
else:
cmd = [
"x11vnc",
"-id", window_id,
"-rfbport", str(port),
"-shared",
"-forever",
"-nopw",
"-accept", "popup"
]
subprocess.run(cmd)
##############################################################################
# MAIN #
##############################################################################
def main():
# 1) Gather windows via python-xlib
windows = list_windows_xlib()
if not windows:
print("No windows found. Are you in an X session? Exiting.")
sys.exit(1)
# 2) If user passed an argument, skip GUI
if len(sys.argv) > 1:
arg = sys.argv[1]
# Attempt partial match or direct ID
chosen = None
for (wid, wtitle) in windows:
if wid.lower() == arg.lower():
chosen = (wid, wtitle)
break
if arg.lower() in wtitle.lower():
chosen = (wid, wtitle)
break
if not chosen:
print(f"No window matches '{arg}'. Exiting.")
sys.exit(1)
window_id, window_title = chosen
else:
# 3) Otherwise, show the wxPython GUI
app = App(windows)
app.MainLoop()
selected = app.get_selection()
if not selected:
print("No window selected (canceled). Exiting.")
sys.exit(1)
window_id, window_title = selected
# Avahi-friendly name
if window_id == "root":
service_name = "Desktop (root)"
else:
service_name = window_title
service_name = service_name[:60] # truncate
# 4) Publish via Avahi using DBus (no avahi-publish-service)
try:
avahi = AvahiPublisher()
avahi.publish(service_name, VNC_PORT)
except Exception as e:
print(f"Note: Could not publish Avahi service: {e}")
# 5) Run x11vnc
try:
run_x11vnc(window_id, VNC_PORT)
finally:
# 6) Unpublish Avahi
if 'avahi' in locals():
avahi.unpublish()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment