Skip to content

Commit

Permalink
New runner (#12)
Browse files Browse the repository at this point in the history
* New runner

* added help and shell + logging

* resolve symlink
  • Loading branch information
JessicaTegner authored Feb 23, 2025
1 parent c6ce880 commit 86eae0f
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 35 deletions.
139 changes: 104 additions & 35 deletions pytinytex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import asyncio
from pathlib import Path
import sys
import os
import subprocess
import platform
import logging

from .tinytex_download import download_tinytex, DEFAULT_TARGET_FOLDER # noqa

logger = logging.getLogger("pytinytex")
formatter = logging.Formatter('%(message)s')

# create a console handler and set the level to debug
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)



# Global cache
__tinytex_path = None

def update(package="-all"):
path = get_tinytex_path()
try:
code, stdout,stderr = _run_tlmgr_command(["update", package], path, False)
return True
except RuntimeError:
raise
return False
return _run_tlmgr_command(["update", package], path, False)

def help():
path = get_tinytex_path()
return _run_tlmgr_command(["help"], path, False)

def shell():
path = get_tinytex_path()
return _run_tlmgr_command(["shell"], path, False, True)

def get_tinytex_path(base=None):
if __tinytex_path:
Expand Down Expand Up @@ -74,41 +88,96 @@ def _get_file(dir, prefix):
except FileNotFoundError:
raise RuntimeError("Unable to find {}.".format(prefix))

def _run_tlmgr_command(args, path, machine_readable=True):
def _run_tlmgr_command(args, path, machine_readable=True, interactive=False):
if machine_readable:
if "--machine-readable" not in args:
args.insert(0, "--machine-readable")
tlmgr_executable = _get_file(path, "tlmgr")
# resolve any symlinks
tlmgr_executable = str(Path(tlmgr_executable).resolve(True))
args.insert(0, tlmgr_executable)
new_env = os.environ.copy()
creation_flag = 0x08000000 if sys.platform == "win32" else 0 # set creation flag to not open TinyTeX in new console on windows
p = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=new_env,
creationflags=creation_flag)
# something else than 'None' indicates that the process already terminated
if p.returncode is not None:
raise RuntimeError(
'TLMGR died with exitcode "%s" before receiving input: %s' % (p.returncode,
p.stderr.read())
)

stdout, stderr = p.communicate()


try:
stdout = stdout.decode("utf-8")
except UnicodeDecodeError:
raise RuntimeError("Unable to decode stdout from TinyTeX")

logger.debug(f"Running command: {args}")
return asyncio.run(_run_command(*args, stdin=interactive, env=new_env, creationflags=creation_flag))
except Exception:
raise

async def read_stdout(process, output_buffer):
"""Read lines from process.stdout and print them."""
logger.debug(f"Reading stdout from process {process.pid}")
try:
while True:
line = await process.stdout.readline()
if not line: # EOF reached
break
line = line.decode('utf-8').rstrip()
output_buffer.append(line)
logger.info(line)
except Exception as e:
logger.error(f"Error in read_stdout: {e}")
finally:
process._transport.close()
return await process.wait()

async def send_stdin(process):
"""Read user input from sys.stdin and send it to process.stdin."""
logger.debug(f"Sending stdin to process {process.pid}")
loop = asyncio.get_running_loop()
try:
stderr = stderr.decode("utf-8")
except UnicodeDecodeError:
raise RuntimeError("Unable to decode stderr from TinyTeX")
while True:
# Offload the blocking sys.stdin.readline() call to the executor.
user_input = await loop.run_in_executor(None, sys.stdin.readline)
if not user_input: # EOF (e.g. Ctrl-D)
break
process.stdin.write(user_input.encode('utf-8'))
await process.stdin.drain()
except Exception as e:
logger.error(f"Error in send_stdin: {e}")
finally:
if process.stdin:
process._transport.close()


async def _run_command(*args, stdin=False, **kwargs):
# Create the subprocess with pipes for stdout and stdin.
process = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
stdin=asyncio.subprocess.PIPE if stdin else asyncio.subprocess.DEVNULL,
**kwargs
)

output_buffer = []
# Create tasks to read stdout and send stdin concurrently.
stdout_task = asyncio.create_task(read_stdout(process, output_buffer))
stdin_task = None
if stdin:
stdin_task = asyncio.create_task(send_stdin(process))

if stderr == "" and p.returncode == 0:
return p.returncode, stdout, stderr
else:
raise RuntimeError("TLMGR died with the following error:\n{0}".format(stderr.strip()))
return p.returncode, stdout, stderr
try:
if stdin:
# Wait for both tasks to complete.
logger.debug("Waiting for stdout and stdin tasks to complete")
await asyncio.gather(stdout_task, stdin_task)
else:
# Wait for the stdout task to complete.
logger.debug("Waiting for stdout task to complete")
await stdout_task
# Return the process return code.
exit_code = await process.wait()
except KeyboardInterrupt:
process.terminate() # Gracefully terminate the subprocess.
exit_code = await process.wait()
finally:
# Cancel tasks that are still running.
stdout_task.cancel()
if stdin_task:
stdin_task.cancel()
captured_output = "\n".join(output_buffer)
if exit_code != 0:
raise RuntimeError(f"Error running command: {captured_output}")
return exit_code, captured_output
8 changes: 8 additions & 0 deletions tests/test_tinytex_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import pytinytex
from .utils import download_tinytex, TINYTEX_DISTRIBUTION # noqa

def test_run_help(download_tinytex): # noqa
exit_code, output = pytinytex.help()
assert exit_code == 0
assert "TeX Live" in output

0 comments on commit 86eae0f

Please # to comment.