Skip to content
Closed
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
21 changes: 20 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions pretext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from typing import Any, Callable, List, Literal, Optional
from functools import update_wrapper


from . import (
utils,
resources,
Expand All @@ -32,7 +31,6 @@
CORE_COMMIT,
)


from .project import Project

log = logging.getLogger("ptxlogger")
Expand All @@ -58,6 +56,7 @@

# Add a decorator to provide nice exception handling for validation errors for all commands. It avoids printing a confusing traceback, and also nicely formats validation errors.
def nice_errors(f: Callable[..., None]) -> Any:

@click.pass_context
def try_except(ctx: click.Context, *args: Any, **kwargs: Any) -> Any:
try:
Expand Down Expand Up @@ -790,8 +789,8 @@ def view(

if stop_server:
try:
projectHash = utils.hash_path(project.abspath())
current_server = server.active_server_for_path_hash(projectHash)
project_hash = utils.hash_path(project.abspath())
current_server = server.active_server_for_path_hash(project_hash)
log.info("\nStopping server.")
if current_server:
current_server.terminate()
Expand Down Expand Up @@ -854,16 +853,16 @@ def view(
webbrowser.open(url)
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)
project_hash = utils.hash_path(project.abspath())
current_server = server.active_server_for_path_hash(project_hash)
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
# Double check that the current server really is active:
if current_server is not None and current_server.isActiveServer():
if current_server is not None and current_server.is_active_server():
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}")
Expand Down
77 changes: 42 additions & 35 deletions pretext/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@
# Get access to logger
log = logging.getLogger("ptxlogger")

# Limit for how many entries to allow in the server file
# before attempting to clean up non-running entries.
# Note: This is not a limit to the number of concurrent servers.
PURGE_LIMIT = 10

Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be unrelated to the pep8 stuff, so don't forget to open a new PR to implement this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a constant that was defined inside the one function that uses it, but pep8-naming complained about the all-caps use there, so I moved it to the top. If I leave it back without renaming we would be getting pep8-naming errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nm, I see this was moved out to be a constant.


@dataclass
class RunningServerInfo:
"""A simple dataclass to hold the information in the running servers file."""

pathHash: str
path_hash: str
pid: int
port: int
binding: str

@staticmethod
def fromFileLine(line: str) -> RunningServerInfo:
(pathHash, pid, port, binding) = line.split()
def from_file_line(line: str) -> RunningServerInfo:
(path_hash, pid, port, binding) = line.split()
return RunningServerInfo(
pathHash=pathHash, pid=int(pid), port=int(port), binding=binding
path_hash=path_hash, pid=int(pid), port=int(port), binding=binding
)

def toFileLine(self) -> str:
return f"{self.pathHash} {self.pid} {self.port} {self.binding}\n"
def to_file_line(self) -> str:
return f"{self.path_hash} {self.pid} {self.port} {self.binding}\n"

def isActiveServer(self) -> bool:
def is_active_server(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():
Expand All @@ -56,7 +61,7 @@ def terminate(self) -> None:
try:
log.info(f"Terminating {self.pid}")
psutil.Process(self.pid).terminate()
remove_server_entry(self.pathHash)
remove_server_entry(self.path_hash)
except Exception as e:
log.info(f"Terminate failed for {self.pid}.")
log.exception(e, exc_info=True)
Expand All @@ -73,71 +78,72 @@ def get_running_servers() -> t.List[RunningServerInfo]:
if not home_path().exists():
return []
try:
runningServersFile = home_path() / "running_servers"
if not runningServersFile.is_file():
running_servers_file = home_path() / "running_servers"
if not running_servers_file.is_file():
return []
with open(runningServersFile, "r") as f:
return [RunningServerInfo.fromFileLine(line) for line in f.readlines()]
with open(running_servers_file, "r") as f:
return [RunningServerInfo.from_file_line(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:
def save_running_servers(servers: 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:
running_servers_file = home_path() / "running_servers"
with open(running_servers_file, "w") as f:
# Write each server info to a new line
f.writelines([info.toFileLine() for info in runningServers])
f.writelines([info.to_file_line() for info in servers])
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:
def add_server_entry(path_hash: 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:
running_servers = get_running_servers()
new_entry = RunningServerInfo(
path_hash=path_hash, pid=pid, port=port, binding=binding
)
running_servers.append(new_entry)
if len(running_servers) >= 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()}")
running_servers = list(stop_inactive_servers(running_servers))
save_running_servers(running_servers)
log.info(f"Added server entry {new_entry.to_file_line()}")


def remove_server_entry(pathHash: str) -> None:
remainingServers = [
info for info in get_running_servers() if info.pathHash != pathHash
def remove_server_entry(path_hash: str) -> None:
remaining_servers = [
info for info in get_running_servers() if info.path_hash != path_hash
]
save_running_servers(remainingServers)
save_running_servers(remaining_servers)


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():
if server.is_active_server():
yield server
else:
server.terminate()


def active_server_for_path_hash(pathHash: str) -> t.Optional[RunningServerInfo]:
def active_server_for_path_hash(path_hash: str) -> t.Optional[RunningServerInfo]:
return next(
(info for info in get_running_servers() if info.pathHash == pathHash),
(info for info in get_running_servers() if info.path_hash == path_hash),
None,
)

Expand All @@ -157,14 +163,15 @@ def start_server(
callback: t.Callable[[int], None] | None = None,
) -> None:
log.info("setting up ...")
pathHash = hash_path(base_dir)
path_hash = 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)

Expand All @@ -186,7 +193,7 @@ class TCPServer(socketserver.TCPServer):
try:
with TCPServer((binding, port), RequestHandler) as httpd:
log.info("adding server entry")
add_server_entry(pathHash, pid, port, binding)
add_server_entry(path_hash, pid, port, binding)
log.info("Starting the server")
if callback is not None:
callback(port)
Expand All @@ -197,5 +204,5 @@ class TCPServer(socketserver.TCPServer):
log.warning(f"Trying port {port} instead.\n")
except KeyboardInterrupt:
log.info("Stopping server.")
remove_server_entry(pathHash)
remove_server_entry(path_hash)
return
10 changes: 5 additions & 5 deletions pretext/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ def home_path() -> Path:
return Path.home() / ".ptx"


def hash_path(projectPath: Path) -> str:
return sha256(str(projectPath).encode("utf-8")).hexdigest()[:10]
def hash_path(project_path: Path) -> str:
return sha256(str(project_path).encode("utf-8")).hexdigest()[:10]


# TODO: is this ever called?
Expand Down Expand Up @@ -766,7 +766,7 @@ def active_server_port() -> t.Optional[int]:
for proc in psutil.process_iter():
if proc is None:
continue
if isPretextProc(proc): # type: ignore
if is_pretext_proc(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]:
Expand Down Expand Up @@ -804,7 +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 isPretextProc(proc):
if is_pretext_proc(proc):
log.debug(f"Terminating process with PID {proc.pid}")
proc.terminate()

Expand Down Expand Up @@ -847,7 +847,7 @@ def latest_version() -> t.Optional[str]:
return None


def isPretextProc(proc: psutil.Process) -> bool:
def is_pretext_proc(proc: psutil.Process) -> bool:
if proc.name() == "pretext":
return False
parent = proc.parent()
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ prefig = { extras = ["pycairo"], version = "^0.2.10", optional = true }
black = [{version = "^23", python = "3.8"}, {version = "^24.10", python = ">=3.9"}]
codechat-server = "^0"
flake8 = "^6"
pep8-naming = "^0.14.1"
lxml-stubs = "^0"
mypy = "^1"
pytest = "^7"
Expand Down