Skip to content
New issue

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

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

Already on GitHub? # to your account

Collegamento ipc #85

Merged
merged 4 commits into from
Aug 1, 2024
Merged
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
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pygments
pyeditorconfig
beartype
token_tools
collegamento

# Testing
pytest
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pygments
pyeditorconfig
beartype
token_tools
collegamento
3 changes: 2 additions & 1 deletion salve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

beartype_this_package()

from collegamento import Response # noqa: F401, E402

from .ipc import IPC # noqa: F401, E402
from .misc import ( # noqa: F401, E402
AUTOCOMPLETE,
Expand All @@ -11,6 +13,5 @@
HIGHLIGHT,
LINKS_AND_CHARS,
REPLACEMENTS,
Response,
)
from .server_functions import is_unicode_letter # noqa: F401, E402
227 changes: 42 additions & 185 deletions salve/ipc.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,51 @@
from logging import Logger, getLogger
from multiprocessing import Process, Queue, freeze_support
from pathlib import Path
from random import randint

from collegamento import FileClient

from .misc import (
AUTOCOMPLETE,
COMMAND,
COMMANDS,
DEFINITION,
EDITORCONFIG,
Notification,
Request,
RequestQueueType,
Response,
ResponseQueueType,
HIGHLIGHT,
LINKS_AND_CHARS,
REPLACEMENTS,
)
from .wrappers import (
editorconfig_request_wrapper,
find_autocompletions_request_wrapper,
get_definition_request_wrapper,
get_highlights_request_wrapper,
get_replacements_request_wrapper,
get_special_tokens_request_wrapper,
)
from .server import Server


class IPC:
class IPC(FileClient):
"""The IPC class is used to talk to the server and run commands. The public API includes the following methods:
- IPC.request()
- IPC.cancel_request()
- IPC.update_file()
- IPC.remove_file()
- IPC.kill_IPC()
"""

def __init__(self, id_max: int = 15_000) -> None:
self.all_ids: list[int] = []
self.id_max = id_max
self.current_ids: dict[str, int] = {}
self.newest_responses: dict[str, Response | None] = {}
for command in COMMANDS:
self.current_ids[command] = 0
self.newest_responses[command] = None

self.files: dict[str, str] = {}

self.logger: Logger = getLogger("IPC")
self.logger.info("Creating server")
self.response_queue: ResponseQueueType = Queue()
self.requests_queue: RequestQueueType = Queue()
self.main_server: Process
self.create_server()
self.logger.info("Initialization is complete")

def create_server(self) -> None:
"""Creates the main_server through a subprocess - internal API"""
freeze_support()
server_logger = getLogger("Server")
self.main_server = Process(
target=Server,
args=(self.response_queue, self.requests_queue, server_logger),
daemon=True,
def __init__(self, id_max: int = 15000) -> None:
super().__init__(
id_max=id_max,
commands={
AUTOCOMPLETE: find_autocompletions_request_wrapper,
REPLACEMENTS: get_replacements_request_wrapper,
HIGHLIGHT: get_highlights_request_wrapper,
EDITORCONFIG: editorconfig_request_wrapper,
DEFINITION: get_definition_request_wrapper,
LINKS_AND_CHARS: get_special_tokens_request_wrapper,
},
)
self.main_server.start()
self.logger.info("Server created")

self.logger.info("Copying files to server")
files_copy = self.files.copy()
self.files = {}
for file, data in files_copy.items():
self.update_file(file, data)
self.logger.debug("Finished copying files to server")

def create_message(self, type: str, **kwargs) -> None:
"""Creates a Message based on the args and kwawrgs provided. Highly flexible. - internal API"""
self.logger.info("Creating message for server")
id = randint(1, self.id_max) # 0 is reserved for the empty case
while id in self.all_ids:
id = randint(1, self.id_max)
self.all_ids.append(id)

self.logger.debug("ID for message created")
if not self.main_server.is_alive():
self.logger.critical(
"Server was killed at some point, creating server"
)
self.create_server()

match type:
case "request":
self.logger.info("Creating request for server")
command = kwargs.get("command", "")
self.current_ids[command] = id
request: Request = {
"id": id,
"type": type,
"command": command,
"file": "",
}
request.update(**kwargs)
self.logger.debug(f"Request created: {request}")
self.requests_queue.put(request)
self.logger.info("Message sent")
case "notification":
self.logger.info("Creating notification for server")
notification: Notification = {
"id": id,
"type": type,
"remove": False,
"file": "",
"contents": "",
}
notification.update(**kwargs)
self.logger.debug(f"Notification created: {notification}")
self.requests_queue.put(notification)
self.logger.info("Message sent")

def request(
# Pyright likes to complain and say this won't work but it actually does
# TODO: Use plum or custom multiple dispatch (make it a new project for salve organization)
def request( # type: ignore
self,
command: COMMAND,
file: str = "",
Expand All @@ -133,96 +71,15 @@ def request(
raise Exception(f"File {file} does not exist in system!")

self.logger.debug("Sending info to create_message()")
self.create_message(
type="request",
command=command,
file=file,
expected_keywords=expected_keywords,
current_word=current_word,
language=language,
text_range=text_range,
file_path=file_path,
definition_starters=definition_starters,
)

def cancel_request(self, command: str) -> None:
"""Cancels a request of type command - external API"""
if command not in COMMANDS:
self.logger.exception(
f"Cannot cancel command {command}, valid commands are {COMMANDS}"
)
raise Exception(
f"Cannot cancel command {command}, valid commands are {COMMANDS}"
)

self.logger.info(f"Cancelled command: {command}")
self.current_ids[command] = 0

def parse_response(self, res: Response) -> None:
"""Parses main_server output line and discards useless responses - internal API"""
self.logger.debug("Parsing server response")
id = res["id"]
self.all_ids.remove(id)

if "command" not in res:
self.logger.info("Response was notification response")
return

command = res["command"]
if id != self.current_ids[command]:
self.logger.info("Response is from old request")
return

self.logger.info(f"Response is useful for command type: {command}")
self.current_ids[command] = 0
self.newest_responses[command] = res

def check_responses(self) -> None:
"""Checks all main_server output by calling IPC.parse_line() on each response - internal API"""
self.logger.debug("Checking responses")
while not self.response_queue.empty():
self.parse_response(self.response_queue.get())

def get_response(self, command: str) -> Response | None:
"""Checks responses and returns the current response of type command if it has been returned - external API"""
self.logger.info(f"Getting response for type: {command}")
if command not in COMMANDS:
self.logger.exception(
f"Cannot get response of command {command}, valid commands are {COMMANDS}"
)
raise Exception(
f"Cannot get response of command {command}, valid commands are {COMMANDS}"
)

self.check_responses()
response: Response | None = self.newest_responses[command]
self.newest_responses[command] = None
self.logger.info("Response retrieved")
return response

def update_file(self, file: str, current_state: str) -> None:
"""Updates files in the system - external API"""

self.logger.info(f"Updating file: {file}")
self.files[file] = current_state

self.logger.debug("Notifying server of file update")
self.create_message("notification", file=file, contents=current_state)

def remove_file(self, file: str) -> None:
"""Removes a file from the main_server - external API"""
if file not in list(self.files.keys()):
self.logger.exception(
f"Cannot remove file {file} as file is not in file database!"
)
raise Exception(
f"Cannot remove file {file} as file is not in file database!"
)

self.logger.info("Notifying server of file deletion")
self.create_message("notification", remove=True, file=file)

def kill_IPC(self) -> None:
"""Kills the main_server when salve_ipc's services are no longer required - external API"""
self.logger.info("Killing server")
self.main_server.kill()
request: dict = {
"command": command,
"expected_keywords": expected_keywords,
"current_word": current_word,
"language": language,
"text_range": text_range,
"file_path": file_path,
"definition_starters": definition_starters,
}
if file:
request.update({"file": file})
super().request(request)
54 changes: 0 additions & 54 deletions salve/misc.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from multiprocessing.queues import Queue as GenericQueueClass
from pathlib import Path
from typing import TYPE_CHECKING, NotRequired, TypedDict

from token_tools import Token

COMMANDS: list[str] = [
"autocomplete",
"replacements",
Expand All @@ -20,51 +14,3 @@
EDITORCONFIG: COMMAND = COMMANDS[3]
DEFINITION: COMMAND = COMMANDS[4]
LINKS_AND_CHARS: COMMAND = COMMANDS[5]


class Message(TypedDict):
"""Base class for messages in and out of the server"""

id: int
type: str # Can be "request", "response", "notification"


class Request(Message):
"""Request results/output from the server with command specific input"""

command: str # Can only be commands in COMMANDS
file: str
expected_keywords: NotRequired[list[str]] # autocomplete, replacements
current_word: NotRequired[str] # autocomplete, replacements, definition
language: NotRequired[str] # highlight
text_range: NotRequired[tuple[int, int]] # highlight, links_and_chars
file_path: NotRequired[Path | str] # editorconfig
definition_starters: NotRequired[
list[tuple[str, str]]
] # definition (list of regexes)


class Notification(Message):
"""Notifies the server to add/update/remove a file for usage in fulfilling commands"""

file: str
remove: bool
contents: NotRequired[str]


class Response(Message):
"""Server responses to requests and notifications"""

cancelled: bool
command: NotRequired[str]
result: NotRequired[list[str | Token] | dict[str, str] | Token]


if TYPE_CHECKING:
ResponseQueueType = GenericQueueClass[Response]
RequestQueueType = GenericQueueClass[Request | Notification]
# Else, this is CPython < 3.12. We are now in the No Man's Land
# of Typing. In this case, avoid subscripting "GenericQueue". Ugh.
else:
ResponseQueueType = GenericQueueClass
RequestQueueType = GenericQueueClass
Loading
Loading