Skip to content

Commit

Permalink
feat(cli): replace TCP server with Named Pipe server for command hand…
Browse files Browse the repository at this point in the history
…ling

This change introduces a Named Pipe server to handle application control commands instead of the previous TCP server implementation. The new CLI pipe server improves communication efficiency and reliability, particularly in Windows environments. The relevant classes and methods have been updated to reflect this change, ensuring seamless operation of the application.
  • Loading branch information
amnweb committed Jan 26, 2025
1 parent 1b8b3ba commit c699762
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 61 deletions.
51 changes: 26 additions & 25 deletions src/core/tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from settings import GITHUB_URL, SCRIPT_PATH, APP_NAME, APP_NAME_FULL, DEFAULT_CONFIG_DIRECTORY, GITHUB_THEME_URL, BUILD_VERSION
from core.config import get_config
from core.console import WindowShellDialog
from core.utils.cli_client import TCPHandler
from core.utils.cli_client import CliPipeHandler
import threading

OS_STARTUP_FOLDER = os.path.join(os.environ['APPDATA'], r'Microsoft\Windows\Start Menu\Programs\Startup')
Expand All @@ -37,10 +37,10 @@ def __init__(self, bar_manager: BarManager):
self.setToolTip(APP_NAME)
self._load_config()
self._bar_manager.remove_tray_icon_signal.connect(self.remove_tray_icon)
# Start the TCP server if the executable exists, if running from source, the server will not start
# Start the CLI pipe server if the executable exists, if running from source, the server will not start
if os.path.exists(EXE_PATH):
self.tcp_handler = TCPHandler(self.stop_or_reload_application)
self.start_tcp_server()
self.cli_pipi_handler = CliPipeHandler(self.stop_or_reload_application)
self.start_cli_server()

def _load_config(self):
try:
Expand All @@ -53,16 +53,16 @@ def _load_config(self):
self.komorebi_stop = config['komorebi']["stop_command"]
self.komorebi_reload = config['komorebi']["reload_command"]

def start_tcp_server(self):
def start_cli_server(self):
"""
Start the TCP server in a separate daemon thread.
Start the CLI pipe server in a separate thread.
"""
server_thread = threading.Thread(target=self.tcp_handler.start_socket_server, daemon=True)
server_thread = threading.Thread(target=self.cli_pipi_handler.start_cli_pipe_server, daemon=True)
server_thread.start()

def stop_or_reload_application(self, reload=False):
"""
Stop or reload the application from the TCP server.
Stop or reload the application from the CLI.
"""
if reload:
self._reload_application()
Expand All @@ -76,8 +76,9 @@ def _load_favicon(self):
self.setIcon(self._icon)

def _load_context_menu(self):
menu = QMenu()
menu.setWindowModality(Qt.WindowModality.WindowModal)
self.menu = QMenu()
self.menu.setWindowModality(Qt.WindowModality.WindowModal)

style_sheet = """
QMenu {
background-color: #26292b;
Expand Down Expand Up @@ -109,27 +110,27 @@ def _load_context_menu(self):
padding-right:24px;
}
"""
menu.setStyleSheet(style_sheet)
self.menu.setStyleSheet(style_sheet)

github_action = menu.addAction("Visit GitHub")
github_action = self.menu.addAction("Visit GitHub")
github_action.triggered.connect(self._open_docs_in_browser)

open_config_action = menu.addAction("Open Config")
open_config_action = self.menu.addAction("Open Config")
open_config_action.triggered.connect(self._open_config)
if os.path.exists(THEME_EXE_PATH):
yasb_themes_action = menu.addAction("Get Themes")
yasb_themes_action = self.menu.addAction("Get Themes")
yasb_themes_action.triggered.connect(lambda: os.startfile(THEME_EXE_PATH))

reload_action = menu.addAction("Reload YASB")
reload_action = self.menu.addAction("Reload YASB")
reload_action.triggered.connect(self._reload_application)

menu.addSeparator()
debug_menu = menu.addMenu("Debug")
self.menu.addSeparator()
debug_menu = self.menu.addMenu("Debug")
info_action = debug_menu.addAction("Information")
info_action.triggered.connect(self._show_info)
menu.addSeparator()
self.menu.addSeparator()
if self.is_komorebi_installed():
komorebi_menu = menu.addMenu("Komorebi")
komorebi_menu = self.menu.addMenu("Komorebi")
start_komorebi = komorebi_menu.addAction("Start Komorebi")
start_komorebi.triggered.connect(self._start_komorebi)

Expand All @@ -139,25 +140,25 @@ def _load_context_menu(self):
reload_komorebi = komorebi_menu.addAction("Reload Komorebi")
reload_komorebi.triggered.connect(self._reload_komorebi)

menu.addSeparator()
self.menu.addSeparator()

if self.is_autostart_enabled():
disable_startup_action = menu.addAction("Disable Autostart")
disable_startup_action = self.menu.addAction("Disable Autostart")
disable_startup_action.triggered.connect(self._disable_startup)
else:
enable_startup_action = menu.addAction("Enable Autostart")
enable_startup_action = self.menu.addAction("Enable Autostart")
enable_startup_action.triggered.connect(self._enable_startup)

logs_action = debug_menu.addAction("Logs")
logs_action.triggered.connect(self._open_logs)

about_action = menu.addAction("About")
about_action = self.menu.addAction("About")
about_action.triggered.connect(self._show_about_dialog)

exit_action = menu.addAction("Exit")
exit_action = self.menu.addAction("Exit")
exit_action.triggered.connect(self._exit_application)

self.setContextMenu(menu)
self.setContextMenu(self.menu)

@pyqtSlot()
def remove_tray_icon(self):
Expand Down
36 changes: 24 additions & 12 deletions src/core/utils/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import socket
import pywintypes
import win32file
import subprocess
import sys
import logging
Expand Down Expand Up @@ -80,18 +81,29 @@ def error(self, message):
class CLIHandler:

def stop_or_reload_application(reload=False):
pipe_name = r'\\.\pipe\yasb_pipe'
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect(('localhost', 65432))
if reload:
sock.sendall(b'reload')
else:
sock.sendall(b'stop')
response = sock.recv(1024).decode('utf-8')
if response != 'ACK':
print(f"Received unexpected response: {response}")
except ConnectionRefusedError:
print("Failed to connect to yasb.exe. It may not be running.")
pipe_handle = win32file.CreateFile(
pipe_name,
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
0,
None,
win32file.OPEN_EXISTING,
0,
None
)
cmd = b'reload' if reload else b'stop'
win32file.WriteFile(pipe_handle, cmd)
_, response = win32file.ReadFile(pipe_handle, 64 * 1024)
if response.decode('utf-8').strip() != 'ACK':
print(f"Received unexpected response: {response.decode('utf-8').strip()}")
win32file.CloseHandle(pipe_handle)
except pywintypes.error as e:
# ERROR_FILE_NOT_FOUND can indicate the pipe doesn't exist
if e.args[0] == 2:
print("Failed to connect to YASB. Pipe not found. It may not be running.")
else:
print(f"Pipe error: {e}")
except Exception as e:
print(f"Error: {e}")

Expand Down
82 changes: 58 additions & 24 deletions src/core/utils/cli_client.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,73 @@
import socket
import logging
import threading
import win32pipe, win32file, pywintypes
from settings import DEBUG

class TCPHandler:
class CliPipeHandler:
def __init__(self, stop_or_reload_callback):
self.stop_or_reload_callback = stop_or_reload_callback
self.pipe_name = r'\\.\pipe\yasb_pipe'
self.server_thread = None
self.stop_event = threading.Event()

def handle_client_connection(self, conn, addr):
with conn:
try:
command = conn.recv(1024).decode('utf-8').strip()
logging.info(f"YASB received command: {command}")
def handle_client_connection(self, pipe):
try:
while True:
data = win32file.ReadFile(pipe, 64*1024)
command = data[1].decode('utf-8').strip()
if DEBUG:
logging.info(f"YASB received command {command}")
if command.lower() == 'stop':
conn.sendall(b'ACK')
win32file.WriteFile(pipe, b'ACK')
self.stop_or_reload_callback()
self.stop_cli_pipe_server()
elif command.lower() == 'reload':
conn.sendall(b'ACK')
win32file.WriteFile(pipe, b'ACK')
self.stop_or_reload_callback(reload=True)
self.stop_cli_pipe_server()
else:
conn.sendall(b'YASB Unknown Command')
except Exception as e:
logging.error(f"Error handling client {addr}: {e}")
win32file.WriteFile(pipe, b'YASB Unknown Command')
except pywintypes.error as e:
if e.args[0] == 109: # ERROR_BROKEN_PIPE
if DEBUG:
logging.info("Pipe closed by client")
else:
logging.error(f"YASB CLI error handling client: {e}")

def start_socket_server(self, host='localhost', port=65432):
def start_cli_pipe_server(self):
"""
Start the TCP server to listen for incoming commands.
Start the Named Pipe server to listen for incoming commands.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
try:
server.bind((host, port))
server.listen()
logging.info(f"YASB socket server started on {host}:{port}")
while True:
conn, addr = server.accept()
client_thread = threading.Thread(target=self.handle_client_connection, args=(conn, addr))
def run_server():
if DEBUG:
logging.info(f"YASB CLI Pipe server started")
while not self.stop_event.is_set():
pipe = win32pipe.CreateNamedPipe(
self.pipe_name,
win32pipe.PIPE_ACCESS_DUPLEX,
win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
1, 65536, 65536,
0,
None
)
try:
win32pipe.ConnectNamedPipe(pipe, None)
client_thread = threading.Thread(target=self.handle_client_connection, args=(pipe,))
client_thread.start()
except Exception as e:
logging.error(f"Socket server encountered an error: {e}")
except Exception as e:
logging.error(f"Pipe server encountered an error: {e}")
win32pipe.DisconnectNamedPipe(pipe)

self.stop_event.clear()
self.server_thread = threading.Thread(target=run_server)
self.server_thread.start()

def stop_cli_pipe_server(self):
"""
Stop the Named Pipe server.
"""
self.stop_event.set()
if self.server_thread:
self.server_thread.join()
if DEBUG:
logging.info("YASB Named Pipe server stopped")

0 comments on commit c699762

Please # to comment.