diff --git a/CHANGELOG.md b/CHANGELOG.md index 1509dca8..5e1dbcff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added * RELAP-5 Plugin +* MCNP Plugin +* Serpent Plugin ### Changes @@ -17,7 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. passed on to the `Plugin.run` method * The `Database` class now acts like a sequence * Database directory names use random strings to avoid clashes when multiple - instances of WATTS are running simulataneously + instances of WATTS are running simultaneously * File template-based plugins now accept an `extra_template_inputs` argument indicating extra template files that should be rendered * The `PluginOpenMC` class now takes an optional `function` argument that @@ -25,6 +27,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * All plugins consistently use an attribute `executable` for specifying the path to an executable +### Fixed + +* Use non-blocking pipe when capturing output to avoid some plugins stalling. +* Avoid use of Unix-specific features in the Python standard library when + running on Windows + ## [0.2.0] ### Added diff --git a/src/watts/fileutils.py b/src/watts/fileutils.py index 97b4392d..a4da0eea 100644 --- a/src/watts/fileutils.py +++ b/src/watts/fileutils.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT from contextlib import contextmanager +import errno import os import platform import select @@ -10,6 +11,9 @@ import tempfile from typing import Union +if sys.platform != 'win32': + import fcntl + # Type for arguments that accept file paths PathLike = Union[str, bytes, os.PathLike] @@ -91,18 +95,39 @@ def run(args): Based on https://stackoverflow.com/a/12272262 and https://stackoverflow.com/a/7730201 """ - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) + # Windows doesn't support select.select and fcntl module so just default to + # using subprocess.run. In this case, show_stdout/show_stderr won't work. + if sys.platform == 'win32': + subprocess.run(args) + return + + # Helper function to add the O_NONBLOCK flag to a file descriptor + def make_async(fd): + fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK) + + # Helper function to read some data from a file descriptor, ignoring EAGAIN errors + def read_async(fd): + try: + return fd.read() + except IOError as e: + if e.errno != errno.EAGAIN: + raise e + else: + return '' + + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + make_async(p.stdout) + make_async(p.stderr) while True: - select.select([p.stdout, p.stderr], [], []) + select.select([p.stdout, p.stderr], [], [], 0) - stdout_data = p.stdout.read() - stderr_data = p.stderr.read() + stdout_data = read_async(p.stdout) + stderr_data = read_async(p.stderr) if stdout_data: - sys.stdout.write(stdout_data) + sys.stdout.write(stdout_data.decode()) if stderr_data: - sys.stderr.write(stderr_data) + sys.stderr.write(stderr_data.decode()) if p.poll() is not None: break