-
Notifications
You must be signed in to change notification settings - Fork 28
Reworking view #878
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
Reworking view #878
Changes from all commits
0473ebd
95f304e
9786c48
330446c
bab6bc5
1d0860f
740ab24
90990a4
e0b77fe
61e80ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| from __future__ import annotations | ||
| from dataclasses import dataclass | ||
| from http.server import SimpleHTTPRequestHandler | ||
| import logging | ||
| import os | ||
| from pathlib import Path | ||
| import socketserver | ||
| import typing as t | ||
| import psutil | ||
|
|
||
| from pretext.utils import hash_path, home_path | ||
|
|
||
| # Get access to logger | ||
| log = logging.getLogger("ptxlogger") | ||
|
|
||
|
|
||
| @dataclass | ||
| class RunningServerInfo: | ||
| """A simple dataclass to hold the information in the running servers file.""" | ||
|
|
||
| pathHash: str | ||
| pid: int | ||
| port: int | ||
| binding: str | ||
|
|
||
| @staticmethod | ||
| def fromFileLine(line: str) -> RunningServerInfo: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that methods in Python should be
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's funny that the logging module doesn't follow that convention. I've definitely been bad at following the convention, as my mind really doesn't like it at all.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Java mixes And yeah, really this should be caught by our linter. |
||
| (pathHash, pid, port, binding) = line.split() | ||
| return RunningServerInfo( | ||
| pathHash=pathHash, pid=int(pid), port=int(port), binding=binding | ||
| ) | ||
|
|
||
| def toFileLine(self) -> str: | ||
| return f"{self.pathHash} {self.pid} {self.port} {self.binding}\n" | ||
|
|
||
| def isActiveServer(self) -> bool: | ||
| """Returns whether the server represented by this object is active on the provided port""" | ||
| p = psutil.Process(self.pid) | ||
| if not p.is_running(): | ||
| log.info(f"Found entry no longer running {p.pid}") | ||
| return False | ||
| if p.status == psutil.STATUS_ZOMBIE: | ||
| log.info(f"Found zombie process {p.pid}") | ||
| return False | ||
| for _, _, _, laddr, _, _ in p.net_connections("all"): | ||
| if laddr.port == self.port: | ||
| log.info(f"Found server at {self.url()}") | ||
| return True | ||
| log.info( | ||
| f"Found process {self.pid} no longer listening on specified port {self.port}" | ||
| ) | ||
| return False | ||
|
|
||
| def terminate(self) -> None: | ||
| """Attempt to terminate the process described by this info.""" | ||
| try: | ||
| log.info(f"Terminating {self.pid}") | ||
| psutil.Process(self.pid).terminate() | ||
| remove_server_entry(self.pathHash) | ||
| except Exception as e: | ||
| log.info(f"Terminate failed for {self.pid}.") | ||
| log.exception(e, exc_info=True) | ||
|
|
||
| def url(self) -> str: | ||
| return f"{self.binding}:{self.port}" | ||
|
|
||
|
|
||
| def get_running_servers() -> t.List[RunningServerInfo]: | ||
| """ | ||
| Processes the ~/.ptx/running_servers file to retrieve a list | ||
| of any possibly current running servers. | ||
| """ | ||
| if not home_path().exists(): | ||
| return [] | ||
| try: | ||
| runningServersFile = home_path() / "running_servers" | ||
| if not runningServersFile.is_file(): | ||
| return [] | ||
| with open(runningServersFile, "r") as f: | ||
| return [RunningServerInfo.fromFileLine(line) for line in f.readlines()] | ||
| except IOError as e: | ||
| log.info("Unable to open running servers file.") | ||
| log.exception(e, exc_info=True) | ||
| return [] | ||
|
|
||
|
|
||
| def save_running_servers(runningServers: t.List[RunningServerInfo]) -> None: | ||
| """ | ||
| Overwrites the ~/.ptx/running_servers file to store | ||
| the new list of running servers. | ||
| """ | ||
| # Ensure home path exists | ||
| os.makedirs(home_path(), exist_ok=True) | ||
| try: | ||
| runningServersFile = home_path() / "running_servers" | ||
| with open(runningServersFile, "w") as f: | ||
| # Write each server info to a new line | ||
| f.writelines([info.toFileLine() for info in runningServers]) | ||
| except IOError as e: | ||
| log.info("Unable to write running servers file.") | ||
| log.exception(e, exc_info=True) | ||
|
|
||
|
|
||
| def add_server_entry(pathHash: str, pid: int, port: int, binding: str) -> None: | ||
| """Add a new server entry to ~/.ptx/running_servers. | ||
|
|
||
| This function does not attempt to ensure that an active server doesn't already exist. | ||
| """ | ||
| PURGE_LIMIT = 10 # If more servers active, try to clean up | ||
| runningServers = get_running_servers() | ||
| newEntry = RunningServerInfo(pathHash=pathHash, pid=pid, port=port, binding=binding) | ||
| runningServers.append(newEntry) | ||
| if len(runningServers) >= PURGE_LIMIT: | ||
| log.info(f"There are {PURGE_LIMIT} or more servers on file. Cleaning up ...") | ||
| runningServers = list(stop_inactive_servers(runningServers)) | ||
| save_running_servers(runningServers) | ||
| log.info(f"Added server entry {newEntry.toFileLine()}") | ||
|
|
||
|
|
||
| def remove_server_entry(pathHash: str) -> None: | ||
| remainingServers = [ | ||
| info for info in get_running_servers() if info.pathHash != pathHash | ||
| ] | ||
| save_running_servers(remainingServers) | ||
|
|
||
|
|
||
| def stop_inactive_servers( | ||
| servers: t.List[RunningServerInfo], | ||
| ) -> t.Iterator[RunningServerInfo]: | ||
| """Stops any inactive servers and yields the active ones.""" | ||
| for server in servers: | ||
| if server.isActiveServer(): | ||
| yield server | ||
| else: | ||
| server.terminate() | ||
|
|
||
|
|
||
| def active_server_for_path_hash(pathHash: str) -> t.Optional[RunningServerInfo]: | ||
| return next( | ||
| (info for info in get_running_servers() if info.pathHash == pathHash), | ||
| None, | ||
| ) | ||
|
|
||
|
|
||
| # boilerplate to prevent overzealous caching by preview server, and | ||
| # avoid port issues | ||
| def binding_for_access(access: t.Literal["public", "private"] = "private") -> str: | ||
| if access == "private": | ||
| return "localhost" | ||
| return "0.0.0.0" | ||
|
|
||
|
|
||
| def start_server( | ||
| base_dir: Path, | ||
| access: t.Literal["public", "private"] = "private", | ||
| port: int = 8128, | ||
| callback: t.Callable[[int], None] | None = None, | ||
| ) -> None: | ||
| log.info("setting up ...") | ||
| pathHash = hash_path(base_dir) | ||
| pid = os.getpid() | ||
| binding = binding_for_access(access) | ||
| log.info("values set...") | ||
|
|
||
| # Previously we defined a custom handler to prevent caching, but we don't need to do that anymore. It was causing issues with the _static js/css files inside codespaces for an unknown reason. Might bring this back in the future. | ||
| # 2024-04-05: try using this again to let Firefox work | ||
| class RequestHandler(SimpleHTTPRequestHandler): | ||
| def __init__(self, *args: t.Any, **kwargs: t.Any): | ||
| super().__init__(*args, directory=base_dir.as_posix(), **kwargs) | ||
|
|
||
| """HTTP request handler with no caching""" | ||
|
|
||
| def end_headers(self) -> None: | ||
| self.send_my_headers() | ||
| SimpleHTTPRequestHandler.end_headers(self) | ||
|
|
||
| def send_my_headers(self) -> None: | ||
| self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") | ||
| self.send_header("Pragma", "no-cache") | ||
| self.send_header("Expires", "0") | ||
|
|
||
| class TCPServer(socketserver.TCPServer): | ||
| allow_reuse_address = True | ||
|
|
||
| while True: | ||
| try: | ||
| with TCPServer((binding, port), RequestHandler) as httpd: | ||
| log.info("adding server entry") | ||
| add_server_entry(pathHash, pid, port, binding) | ||
| log.info("Starting the server") | ||
| if callback is not None: | ||
| callback(port) | ||
| httpd.serve_forever() | ||
| except OSError: | ||
| log.warning(f"Port {port} could not be used.") | ||
| port += 1 | ||
| log.warning(f"Trying port {port} instead.\n") | ||
| except KeyboardInterrupt: | ||
| log.info("Stopping server.") | ||
| remove_server_entry(pathHash) | ||
| return | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting that this didn't appear to be a problem when I was testing locally. I could have sworn I had tested with more than one entry.
Would this be better done by simply adding the newline in the toFileLine function instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I like that better actually. I'll make that change.