From 0473ebd8001d4e9c321cb833fbb1275f9c2e3abe Mon Sep 17 00:00:00 2001 From: Haris Skiadas Date: Sat, 26 Oct 2024 22:38:48 -0400 Subject: [PATCH 01/10] Add method to get our global configs path --- pretext/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pretext/utils.py b/pretext/utils.py index 43948e49..2ab9ad40 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -145,6 +145,11 @@ def project_xml_string(dirpath: Optional[Path] = None) -> str: return ET.tostring(project_xml(dirpath), encoding="unicode") +# Returns the pretext directory under home. +def home_path() -> Path: + return Path.home() / ".ptx" + + # TODO: is this ever called? def target_xml( alias: t.Optional[str] = None, dirpath: t.Optional[Path] = None From 95f304e8e686e55527933bdf8570416a158e0dca Mon Sep 17 00:00:00 2001 From: Haris Skiadas Date: Sat, 26 Oct 2024 22:43:17 -0400 Subject: [PATCH 02/10] Add utilities for managing the list of active servers --- pretext/server.py | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pretext/server.py diff --git a/pretext/server.py b/pretext/server.py new file mode 100644 index 00000000..e9fa6c5e --- /dev/null +++ b/pretext/server.py @@ -0,0 +1,128 @@ +from dataclasses import dataclass +import logging +import os +import typing as t +import psutil + +from pretext.utils import 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 + + def fromFileLine(line: str): + (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}" + + def isActiveServer(self) -> str: + """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 == (self.binding, 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): + """Attempt to terminate the process described by this info.""" + try: + log.info(f"Terminating {self.pid}") + psutil.Process(self.pid).terminate() + except Exception as e: + log.info(f"Terminate failed for {self.pid}.") + log.exception(e, exc_info=True) + + def url(self): + 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.Iterator[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: + 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: id, 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.add(newEntry) + if len(runningServers) >= PURGE_LIMIT: + log.info(f"There are {PURGE_LIMIT} or more servers on file. Cleaning up ...") + runningServers = stop_inactive_servers(runningServers) + save_running_servers(runningServers) + log.info(f"Added server entry {newEntry.toFileLine()}") + + +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_pathHash(pathHash: str) -> t.Optional[RunningServerInfo]: + return next( + (info for info in get_running_servers() if info.pathHash == pathHash), + default=None, + ) From 9786c483aef458b0ebf597d90f5d588866c1dadb Mon Sep 17 00:00:00 2001 From: Haris Skiadas Date: Tue, 29 Oct 2024 11:10:01 -0400 Subject: [PATCH 03/10] Add basic start-server functionality --- pretext/server.py | 79 ++++++++++++++++++++++++++++++++++++++++++++--- pretext/utils.py | 15 +++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/pretext/server.py b/pretext/server.py index e9fa6c5e..4d387f88 100644 --- a/pretext/server.py +++ b/pretext/server.py @@ -1,10 +1,13 @@ 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 home_path +from pretext.utils import hash_path, home_path # Get access to logger log = logging.getLogger("ptxlogger") @@ -102,7 +105,7 @@ def add_server_entry(pathHash: str, pid: id, port: int, binding: str) -> None: 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.add(newEntry) + runningServers.append(newEntry) if len(runningServers) >= PURGE_LIMIT: log.info(f"There are {PURGE_LIMIT} or more servers on file. Cleaning up ...") runningServers = stop_inactive_servers(runningServers) @@ -110,6 +113,13 @@ def add_server_entry(pathHash: str, pid: id, port: int, binding: str) -> None: log.info(f"Added server entry {newEntry.toFileLine()}") +def remove_server_entry(pathHash: str): + 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]: @@ -121,8 +131,69 @@ def stop_inactive_servers( server.terminate() -def active_server_for_pathHash(pathHash: str) -> t.Optional[RunningServerInfo]: +def active_server_for_path_hash(pathHash: str) -> t.Optional[RunningServerInfo]: return next( (info for info in get_running_servers() if info.pathHash == pathHash), - default=None, + 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, +) -> RunningServerInfo: + log.info("setting up ...") + pathHash = hash_path(base_dir) + pid = os.getpid() + port = 8128 + 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 callable is not None: + callable(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 diff --git a/pretext/utils.py b/pretext/utils.py index 2ab9ad40..1a5a0a15 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -1,3 +1,4 @@ +from hashlib import sha256 import os from collections.abc import Generator from contextlib import contextmanager @@ -23,7 +24,6 @@ pass from typing import Any, cast, List, Optional - from . import core, constants, resources # Get access to logger @@ -150,6 +150,10 @@ def home_path() -> Path: return Path.home() / ".ptx" +def hash_path(projectPath: Path) -> str: + return sha256(str(projectPath).encode("utf-8")).hexdigest()[:10] + + # TODO: is this ever called? def target_xml( alias: t.Optional[str] = None, dirpath: t.Optional[Path] = None @@ -257,6 +261,7 @@ def serve_forever( # 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: Any, **kwargs: Any): super().__init__(*args, directory=base_dir.as_posix(), **kwargs) @@ -759,7 +764,9 @@ def active_server_port() -> t.Optional[int]: """ # We look at all currently running processes and check if any are a pretext process that is a child of a pretext process. This would only happen if we have run a `pretext view` command to start the server, so we can assume that this is the server we are looking for. for proc in psutil.process_iter(): - if proc.name() == "pretext" and proc.parent().name() == "pretext": # type: ignore + if ( + proc.name() == "pretext" and proc.parent().name() == "pretext" + ): # type: ignore log.debug(f"Found pretext server running with pid {proc.pid}") # Sometimes the process stops but doesn't get removed from the process list. We check if the process is still running by checking its status. if proc.status() not in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]: @@ -797,7 +804,9 @@ def stop_server(port: t.Optional[int] = None) -> None: else: # As before, we look for a pretext process that is a child of a pretext process. This time we terminate that process. for proc in psutil.process_iter(): - if proc.name() == "pretext" and proc.parent().name() == "pretext": # type: ignore + if ( + proc.name() == "pretext" and proc.parent().name() == "pretext" + ): # type: ignore log.debug(f"Terminating process with PID {proc.pid}") proc.terminate() From 330446c2b1f4a92d256115fb82d2dbfb621094a0 Mon Sep 17 00:00:00 2001 From: Haris Skiadas Date: Tue, 29 Oct 2024 11:10:17 -0400 Subject: [PATCH 04/10] Start calling the new server functionality --- pretext/cli.py | 63 +++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/pretext/cli.py b/pretext/cli.py index 4b8750e6..06e256b8 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -27,6 +27,7 @@ core, constants, plastex, + server, VERSION, CORE_COMMIT, ) @@ -848,8 +849,15 @@ def view( webbrowser.open(url) return # Start server if there isn't one running already: - used_port = utils.active_server_port() - if restart_server or (port != used_port) or (used_port is None): + projectHash = utils.hash_path(project.abspath()) + current_server = server.active_server_for_path_hash(projectHash) + if restart_server and current_server is not None: + log.info( + f"Terminating existing server {current_server.pid} on port {current_server.port}" + ) + current_server.terminate() + current_server = None + if current_server is None: log.info( f"Now preparing local server to preview your project directory `{project.abspath()}`." ) @@ -861,43 +869,24 @@ def view( ) log.info(" personal computer.)") log.info("") - # First terminate any existing server using this port - if used_port == port: - try: - utils.stop_server(used_port) - except Exception as e: - log.warning("Failed to stop server.") - log.debug(e, exc_info=True) - pass - # Start the new server - server = project.server_process( - access=access, - port=port, - ) - server.start() - # Now get the updated port in case we had to pick a new one - actual_port = utils.active_server_port() or port - # set the url - url_base = utils.url_for_access(access=access, port=actual_port) - url = url_base + url_path - log.info(f"Server will soon be available at {url_base}") - if no_launch: - log.info(f"The {target_name} will be available at {url}") - else: - # SECONDS = 2 - log.info(f"Opening browser for {target_name} at {url}") - # time.sleep(SECONDS) - webbrowser.open(url) - try: - while server.is_alive(): - time.sleep(1) - except KeyboardInterrupt: - log.info("Stopping server.") - server.terminate() - return + def callback(actual_port: int): + url_base = utils.url_for_access(access=access, port=actual_port) + url = url_base + url_path + log.info(f"Server will soon be available at {url_base}") + if no_launch: + log.info(f"The {target_name} will be available at {url}") + else: + # SECONDS = 2 + log.info(f"Opening browser for {target_name} at {url}") + # time.sleep(SECONDS) + webbrowser.open(url) + + log.info("starting server ...") + server.start_server(project.abspath(), access, port, callback) + else: - url_base = utils.url_for_access(access=access, port=used_port) + url_base = utils.url_for_access(access=access, port=current_server.port) url = url_base + url_path log.info(f"Server is already available at {url_base}") if no_launch: From bab6bc5c76c0cbad7297f4f85f7da01090ec9a71 Mon Sep 17 00:00:00 2001 From: Haris Skiadas Date: Tue, 17 Dec 2024 15:41:46 -0500 Subject: [PATCH 05/10] Fix typing problems and some more cleanup --- pretext/cli.py | 2 +- pretext/server.py | 27 ++++++++++++++------------- pretext/utils.py | 17 +++++++++++------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/pretext/cli.py b/pretext/cli.py index 06e256b8..4e5a4749 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -870,7 +870,7 @@ def view( log.info(" personal computer.)") log.info("") - def callback(actual_port: int): + def callback(actual_port: int) -> None: url_base = utils.url_for_access(access=access, port=actual_port) url = url_base + url_path log.info(f"Server will soon be available at {url_base}") diff --git a/pretext/server.py b/pretext/server.py index 4d387f88..46c513a1 100644 --- a/pretext/server.py +++ b/pretext/server.py @@ -1,3 +1,4 @@ +from __future__ import annotations from dataclasses import dataclass from http.server import SimpleHTTPRequestHandler import logging @@ -22,7 +23,8 @@ class RunningServerInfo: port: int binding: str - def fromFileLine(line: str): + @staticmethod + def fromFileLine(line: str) -> RunningServerInfo: (pathHash, pid, port, binding) = line.split() return RunningServerInfo( pathHash=pathHash, pid=int(pid), port=int(port), binding=binding @@ -31,7 +33,7 @@ def fromFileLine(line: str): def toFileLine(self) -> str: return f"{self.pathHash} {self.pid} {self.port} {self.binding}" - def isActiveServer(self) -> str: + 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(): @@ -49,7 +51,7 @@ def isActiveServer(self) -> str: ) return False - def terminate(self): + def terminate(self) -> None: """Attempt to terminate the process described by this info.""" try: log.info(f"Terminating {self.pid}") @@ -58,7 +60,7 @@ def terminate(self): log.info(f"Terminate failed for {self.pid}.") log.exception(e, exc_info=True) - def url(self): + def url(self) -> str: return f"{self.binding}:{self.port}" @@ -81,7 +83,7 @@ def get_running_servers() -> t.List[RunningServerInfo]: return [] -def save_running_servers(runningServers: t.Iterator[RunningServerInfo]) -> None: +def save_running_servers(runningServers: t.List[RunningServerInfo]) -> None: """ Overwrites the ~/.ptx/running_servers file to store the new list of running servers. @@ -97,7 +99,7 @@ def save_running_servers(runningServers: t.Iterator[RunningServerInfo]) -> None: log.exception(e, exc_info=True) -def add_server_entry(pathHash: str, pid: id, port: int, binding: str) -> None: +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. @@ -108,12 +110,12 @@ def add_server_entry(pathHash: str, pid: id, port: int, binding: str) -> None: runningServers.append(newEntry) if len(runningServers) >= PURGE_LIMIT: log.info(f"There are {PURGE_LIMIT} or more servers on file. Cleaning up ...") - runningServers = stop_inactive_servers(runningServers) + runningServers = list(stop_inactive_servers(runningServers)) save_running_servers(runningServers) log.info(f"Added server entry {newEntry.toFileLine()}") -def remove_server_entry(pathHash: str): +def remove_server_entry(pathHash: str) -> None: remainingServers = [ info for info in get_running_servers() if info.pathHash != pathHash ] @@ -150,8 +152,8 @@ def start_server( base_dir: Path, access: t.Literal["public", "private"] = "private", port: int = 8128, - callback: t.Callable[[int], None] = None, -) -> RunningServerInfo: + callback: t.Callable[[int], None] | None = None, +) -> None: log.info("setting up ...") pathHash = hash_path(base_dir) pid = os.getpid() @@ -162,7 +164,6 @@ def start_server( # 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) @@ -186,8 +187,8 @@ class TCPServer(socketserver.TCPServer): log.info("adding server entry") add_server_entry(pathHash, pid, port, binding) log.info("Starting the server") - if callable is not None: - callable(port) + if callback is not None: + callback(port) httpd.serve_forever() except OSError: log.warning(f"Port {port} could not be used.") diff --git a/pretext/utils.py b/pretext/utils.py index 1a5a0a15..a0dc76e7 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -764,9 +764,9 @@ def active_server_port() -> t.Optional[int]: """ # We look at all currently running processes and check if any are a pretext process that is a child of a pretext process. This would only happen if we have run a `pretext view` command to start the server, so we can assume that this is the server we are looking for. for proc in psutil.process_iter(): - if ( - proc.name() == "pretext" and proc.parent().name() == "pretext" - ): # type: ignore + if proc is None: + continue + if isPretextProc(proc): # type: ignore log.debug(f"Found pretext server running with pid {proc.pid}") # Sometimes the process stops but doesn't get removed from the process list. We check if the process is still running by checking its status. if proc.status() not in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]: @@ -804,9 +804,7 @@ def stop_server(port: t.Optional[int] = None) -> None: else: # As before, we look for a pretext process that is a child of a pretext process. This time we terminate that process. for proc in psutil.process_iter(): - if ( - proc.name() == "pretext" and proc.parent().name() == "pretext" - ): # type: ignore + if isPretextProc(proc): log.debug(f"Terminating process with PID {proc.pid}") proc.terminate() @@ -847,3 +845,10 @@ def latest_version() -> t.Optional[str]: log.debug("Could not determine latest version of pretext.") log.debug(e, exc_info=True) return None + + +def isPretextProc(proc: psutil.Process) -> bool: + if proc.name() == "pretext": + return False + parent = proc.parent() + return parent is not None and parent.name() == "pretext" From 1d0860f4b4a452f92cc4e0963d684a858131fd81 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Tue, 17 Dec 2024 15:27:53 -0700 Subject: [PATCH 06/10] add newlines between servers --- pretext/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pretext/server.py b/pretext/server.py index 46c513a1..99591bb6 100644 --- a/pretext/server.py +++ b/pretext/server.py @@ -93,7 +93,8 @@ def save_running_servers(runningServers: t.List[RunningServerInfo]) -> None: try: runningServersFile = home_path() / "running_servers" with open(runningServersFile, "w") as f: - f.writelines([info.toFileLine() for info in runningServers]) + # Write each server info to a new line + f.writelines([f"{info.toFileLine()}\n" for info in runningServers]) except IOError as e: log.info("Unable to write running servers file.") log.exception(e, exc_info=True) From 740ab24c9e02790355242644d5f26fecad71d5f6 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Tue, 17 Dec 2024 20:59:48 -0700 Subject: [PATCH 07/10] Remove hardcoded port --- pretext/server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pretext/server.py b/pretext/server.py index 99591bb6..534faf95 100644 --- a/pretext/server.py +++ b/pretext/server.py @@ -31,7 +31,7 @@ def fromFileLine(line: str) -> RunningServerInfo: ) def toFileLine(self) -> str: - return f"{self.pathHash} {self.pid} {self.port} {self.binding}" + 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""" @@ -94,7 +94,7 @@ def save_running_servers(runningServers: t.List[RunningServerInfo]) -> None: runningServersFile = home_path() / "running_servers" with open(runningServersFile, "w") as f: # Write each server info to a new line - f.writelines([f"{info.toFileLine()}\n" for info in runningServers]) + 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) @@ -158,7 +158,6 @@ def start_server( log.info("setting up ...") pathHash = hash_path(base_dir) pid = os.getpid() - port = 8128 binding = binding_for_access(access) log.info("values set...") From 90990a4338748841693717f0563a9f39865e7fe6 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 18 Dec 2024 09:24:03 -0700 Subject: [PATCH 08/10] ensure servers start and stop correctly --- CHANGELOG.md | 3 +++ pretext/cli.py | 37 +++++++++++++++++++++---------------- pretext/server.py | 3 ++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6290e3..3c858ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Instructions: Add a subsection under `[Unreleased]` for additions, fixes, change ## [Unreleased] +### Changed + +- `pretext view` now reuses the current local server correctly, and starts different local servers for different projects. This allows you to correctly view multiple projects at the same time. ## [2.10.1] - 2024-12-10 diff --git a/pretext/cli.py b/pretext/cli.py index 4e5a4749..c9e036ba 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -784,18 +784,22 @@ def view( """ # pretext view -s should immediately stop the server and do nothing else. + if utils.cannot_find_project(task="view the output for"): + return + project = Project.parse() + if stop_server: try: + projectHash = utils.hash_path(project.abspath()) + current_server = server.active_server_for_path_hash(projectHash) log.info("\nStopping server.") - utils.stop_server() + current_server.terminate() except Exception as e: log.warning("Failed to stop server.") log.debug(e, exc_info=True) finally: return - if utils.cannot_find_project(task="view the output for"): - return - project = Project.parse() + try: target = project.get_target(name=target_name, log_info_for_none=not stage) except AssertionError as e: @@ -850,14 +854,25 @@ def view( return # Start server if there isn't one running already: projectHash = utils.hash_path(project.abspath()) - current_server = server.active_server_for_path_hash(projectHash) + current_server = server.active_server_for_path_hash(projectHash) if restart_server and current_server is not None: log.info( f"Terminating existing server {current_server.pid} on port {current_server.port}" ) current_server.terminate() current_server = None - if current_server is None: + # Double check that the current server really is active: + if current_server is not None and current_server.isActiveServer(): + url_base = utils.url_for_access(access=access, port=current_server.port) + url = url_base + url_path + log.info(f"Server is already available at {url_base}") + if no_launch: + log.info(f"The {target_name} is available at {url}") + else: + log.info(f"Now opening browser for {target_name} at {url}") + webbrowser.open(url) + # otherwise, start a server + else: log.info( f"Now preparing local server to preview your project directory `{project.abspath()}`." ) @@ -885,16 +900,6 @@ def callback(actual_port: int) -> None: log.info("starting server ...") server.start_server(project.abspath(), access, port, callback) - else: - url_base = utils.url_for_access(access=access, port=current_server.port) - url = url_base + url_path - log.info(f"Server is already available at {url_base}") - if no_launch: - log.info(f"The {target_name} is available at {url}") - else: - log.info(f"Now opening browser for {target_name} at {url}") - webbrowser.open(url) - # pretext deploy @main.command( diff --git a/pretext/server.py b/pretext/server.py index 534faf95..5e1fda0a 100644 --- a/pretext/server.py +++ b/pretext/server.py @@ -43,7 +43,7 @@ def isActiveServer(self) -> bool: log.info(f"Found zombie process {p.pid}") return False for _, _, _, laddr, _, _ in p.net_connections("all"): - if laddr == (self.binding, self.port): + if laddr.port == self.port: log.info(f"Found server at {self.url()}") return True log.info( @@ -56,6 +56,7 @@ def terminate(self) -> None: 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) From e0b77fe985d2f49b12e4bbfc825d8f98db644671 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 18 Dec 2024 10:00:56 -0700 Subject: [PATCH 09/10] format --- pretext/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pretext/cli.py b/pretext/cli.py index c9e036ba..ebd80d01 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -854,7 +854,7 @@ def view( return # Start server if there isn't one running already: projectHash = utils.hash_path(project.abspath()) - current_server = server.active_server_for_path_hash(projectHash) + current_server = server.active_server_for_path_hash(projectHash) if restart_server and current_server is not None: log.info( f"Terminating existing server {current_server.pid} on port {current_server.port}" From 61e80ec5b0beab9b4312f8f7f5504d74231c9d80 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 18 Dec 2024 10:04:46 -0700 Subject: [PATCH 10/10] fix types --- pretext/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pretext/cli.py b/pretext/cli.py index ebd80d01..24a64be8 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -793,7 +793,8 @@ def view( projectHash = utils.hash_path(project.abspath()) current_server = server.active_server_for_path_hash(projectHash) log.info("\nStopping server.") - current_server.terminate() + if current_server: + current_server.terminate() except Exception as e: log.warning("Failed to stop server.") log.debug(e, exc_info=True)