Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communicate between Porcupine instances #1215

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions porcupine/__main__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

from porcupine import __version__ as porcupine_version
from porcupine import (
_logs,
_state,
dirs,
get_main_window,
get_tab_manager,
menubar,
pluginloader,
settings,
tabs,
)
from porcupine import _logs, _state, dirs, get_main_window, menubar, pluginloader, settings

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -133,6 +123,7 @@ def main() -> None:
)

args = parser.parse_args()

_state.init(args)

# Prevent showing up a not-ready-yet root window to user
Expand All @@ -142,25 +133,14 @@ def main() -> None:
menubar._init()
pluginloader.run_setup_functions(args.shuffle_plugins)

tabmanager = get_tab_manager()
for path_string in args.files:
if path_string == "-":
# don't close stdin so it's possible to do this:
#
# $ porcu - -
# bla bla bla
# ^D
# bla bla
# ^D
tabmanager.add_tab(tabs.FileTab(tabmanager, content=sys.stdin.read()))
else:
tabmanager.open_file(Path(path_string))
_state.open_files(args.files)

get_main_window().deiconify()
try:
get_main_window().mainloop()
finally:
settings.save()

log.info("exiting Porcupine successfully")


Expand Down
67 changes: 67 additions & 0 deletions porcupine/_ipc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations

import queue
import threading
from multiprocessing import connection
from pathlib import Path
from typing import Any

from porcupine import dirs

_ADDRESS_FILE = Path(dirs.user_cache_dir) / "ipc_address.txt"


# the addresses contain random junk so they are very unlikely to
# conflict with each other
# example addresses: r'\\.\pipe\pyc-1412-1-7hyryfd_',
# '/tmp/pymp-_lk54sed/listener-4o8n1xrc',
def send(objects: list[Any]) -> None:
"""Send objects from an iterable to a process running session().

Raise ConnectionRefusedError if session() is not running.
"""
# reading the address file, connecting to a windows named pipe and
# connecting to an AF_UNIX socket all raise FileNotFoundError :D
try:
with _ADDRESS_FILE.open("r") as file:
address = file.read().strip()
client = connection.Client(address)
except FileNotFoundError:
raise ConnectionRefusedError("session() is not running") from None

with client:
for message in objects:
client.send(message)


def _listener2queue(listener: connection.Listener, object_queue: queue.Queue[Any]) -> None:
"""Accept connections. Receive and queue objects."""
while True:
try:
client = listener.accept()
except OSError:
# it's closed
break

with client:
while True:
try:
object_queue.put(client.recv())
except EOFError:
break


def start_session() -> tuple[connection.Listener, queue.Queue[Any]]:
"""Start the listener session. Return the listener object, and the message queue.
The listener has to be closed manually.
"""
message_queue: queue.Queue[Any] = queue.Queue()
listener = connection.Listener()

with _ADDRESS_FILE.open("w") as file:
print(listener.address, file=file)

thread = threading.Thread(target=_listener2queue, args=[listener, message_queue], daemon=True)
thread.start()

return listener, message_queue
57 changes: 55 additions & 2 deletions porcupine/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import dataclasses
import logging
import os
import queue
import sys
import tkinter
import types
from typing import Any, Callable, Type
from multiprocessing import connection
from pathlib import Path
from typing import Any, Callable, Iterable, Type

from porcupine import images, tabs, utils
from porcupine import _ipc, images, tabs, utils
from porcupine.tabs import FileTab

# Windows resolution
if sys.platform == "win32":
Expand All @@ -32,18 +36,52 @@ class _State:
tab_manager: tabs.TabManager
quit_callbacks: list[Callable[[], bool]]
parsed_args: Any # not None
ipc_session: connection.Listener


# global state makes some things a lot easier (I'm sorry)
_global_state: _State | None = None

Quit = object()


def _log_tkinter_error(
exc: Type[BaseException], val: BaseException, tb: types.TracebackType | None
) -> Any:
log.error("Error in tkinter callback", exc_info=(exc, val, tb))


def open_files(files: Iterable[str]) -> None:
tabmanager = get_tab_manager()
for path_string in files:
if path_string == "-":
# don't close stdin so it's possible to do this:
#
# $ porcu - -
# bla bla bla
# ^D
# bla bla
# ^D
tabmanager.add_tab(FileTab(tabmanager, content=sys.stdin.read()))
else:
tabmanager.open_file(Path(path_string))


def listen_for_files(message_queue: queue.Queue[Any]) -> None:
try:
message = message_queue.get_nowait()
except queue.Empty:
message = None
else:
try:
open_files([message])
except Exception as e:
log.error(e)

if message is not Quit:
_get_state().root.after(500, listen_for_files, message_queue)


# undocumented on purpose, don't use in plugins
def init(args: Any) -> None:
assert args is not None
Expand All @@ -53,6 +91,14 @@ def init(args: Any) -> None:

log.debug("init() starts")

try:
_ipc.send(args.files)
except ConnectionRefusedError:
ipc_session, message_queue = _ipc.start_session()
else:
log.info("another instance of Porcupine is already running, files were sent to it")
sys.exit()

root = tkinter.Tk(className="Porcupine") # class name shows up in my alt+tab list
log.debug("root window created")
log.debug("Tcl/Tk version: " + root.tk.eval("info patchlevel"))
Expand Down Expand Up @@ -81,7 +127,11 @@ def init(args: Any) -> None:
tab_manager=tab_manager,
quit_callbacks=[],
parsed_args=args,
ipc_session=ipc_session,
)

listen_for_files(message_queue)

log.debug("init() done")


Expand Down Expand Up @@ -140,6 +190,9 @@ def quit() -> None:
if not callback():
return

_ipc.send([Quit])
_get_state().ipc_session.close()

for tab in get_tab_manager().tabs():
get_tab_manager().close_tab(tab)
get_main_window().destroy()