From 1094310328c913890a0fe2f030f5a3c227d68d4e Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Tue, 16 Feb 2021 13:31:37 +1100 Subject: [PATCH 01/13] Rename config options * PLANTUML_ARGS -> PLANTUML_FILE_OPTIONS * PLANTUML_MARKDOWN_ARGS -> PLANTUML_MARKDOWN_OPTIONS --- tests/test_plantuml.py | 4 ++-- tests/test_plantuml_markdown.py | 2 +- v8/plantuml/README.md | 6 ++--- v8/plantuml/conf.py.sample | 8 +++---- v8/plantuml/plantuml.py | 28 +++++++++++------------ v8/plantuml_markdown/conf.py.sample | 8 +++---- v8/plantuml_markdown/plantuml_markdown.py | 6 ++--- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/test_plantuml.py b/tests/test_plantuml.py index 43f1555e..286e25dc 100644 --- a/tests/test_plantuml.py +++ b/tests/test_plantuml.py @@ -21,7 +21,7 @@ def test_render_file_success(tmp_site_path): (tmp_site_path / 'pages' / 'includes' / 'include2.iuml').write_text('participant "included-2"') plugin = create_plugin({ - 'PLANTUML_ARGS': [ + 'PLANTUML_FILE_OPTIONS': [ '-chide footbox', '-Ipages/includes/include1.iuml', ], @@ -104,7 +104,7 @@ def test_gen_tasks(tmp_site_path): def test_task_depends_on_included_files(tmp_site_path): plugin = create_plugin({ - 'PLANTUML_ARGS': [ + 'PLANTUML_FILE_OPTIONS': [ '-Iincludes/include1.iuml', '-Iincludes/include2.iuml', '-Iincludes/bar*.iuml', diff --git a/tests/test_plantuml_markdown.py b/tests/test_plantuml_markdown.py index 387c980e..7742de65 100644 --- a/tests/test_plantuml_markdown.py +++ b/tests/test_plantuml_markdown.py @@ -155,7 +155,7 @@ def f(data: str, plantuml_continue_after_failure=False) -> CompileResult: 'PLANTUML_DEBUG': True, 'PLANTUML_CONTINUE_AFTER_FAILURE': plantuml_continue_after_failure, 'PLANTUML_EXEC': os.environ.get('PLANTUML_EXEC', 'plantuml').split(), - 'PLANTUML_MARKDOWN_ARGS': [ + 'PLANTUML_MARKDOWN_OPTIONS': [ '-chide footbox', '-nometadata', '-Sshadowing=false', diff --git a/v8/plantuml/README.md b/v8/plantuml/README.md index 6aebd6d5..59a5a730 100644 --- a/v8/plantuml/README.md +++ b/v8/plantuml/README.md @@ -14,10 +14,10 @@ The plugin expects PlantUML files to be encoded with UTF-8. I have some ideas to speed this up, and they may be available in future plugin versions. - Changes to files included via `!include ...` or via a pattern (e.g. `-Ipath/to/*.iuml`) will NOT trigger a rebuild. - Instead, if you include them explicitly in `PLANTUML_ARGS` (e.g. `-Ipath/to/foo.iuml`) then they will trigger a - rebuild. + Instead, if you include them explicitly in `PLANTUML_FILE_OPTIONS` (e.g. `-Ipath/to/foo.iuml`) then they will trigger + a rebuild. -- `nikola auto` does not watch dirs in `PLANTUML_FILES` or files included via `PLANTUML_ARGS` / `!include`. +- `nikola auto` does not watch dirs in `PLANTUML_FILES` or files included via `PLANTUML_FILE_OPTIONS` / `!include`. As a workaround you could put PlantUML files under any dir listed in `POSTS` or `PAGES` because those dirs are watched. (Use `.iuml` suffix for include files to prevent them matching the `*.puml` wildcard in `PLANTUML_FILES`) diff --git a/v8/plantuml/conf.py.sample b/v8/plantuml/conf.py.sample index 141581b8..f43efa8a 100644 --- a/v8/plantuml/conf.py.sample +++ b/v8/plantuml/conf.py.sample @@ -16,7 +16,7 @@ PLANTUML_EXEC = ['plantuml'] # -# PLANTUML_ARGS (list of strings) - CLI arguments that are sent to PlantUML when rendering PlantUML files, +# PLANTUML_FILE_OPTIONS (list of strings) - options used when rendering PlantUML files, # see https://plantuml.com/command-line # # Examples @@ -27,10 +27,10 @@ PLANTUML_EXEC = ['plantuml'] # Specify the style in conf.py # [ '-chide footbox', '-SShadowing=false' ] # -PLANTUML_ARGS = [] +PLANTUML_FILE_OPTIONS = [] # -# PLANTUML_FILES contains (wildcard, destination, extension, args) tuples. +# PLANTUML_FILES contains (wildcard, destination, extension, options) tuples. # # is used to generate a list of source files in the same way as POSTS and PAGES. # @@ -39,7 +39,7 @@ PLANTUML_ARGS = [] # # As with POSTS and PAGES you can create any directory structure you want and it will be reflected in the output. # -# is a list of cli arguments that are appended to PLANTUML_ARGS +# is a list of strings that is appended to PLANTUML_FILE_OPTIONS # PLANTUML_FILES = ( ('plantuml/*.puml', 'plantuml', '.svg', ['-tsvg']), diff --git a/v8/plantuml/plantuml.py b/v8/plantuml/plantuml.py index 8cd89955..efd112c9 100644 --- a/v8/plantuml/plantuml.py +++ b/v8/plantuml/plantuml.py @@ -9,7 +9,7 @@ from nikola.log import get_logger from nikola.plugin_categories import Task -DEFAULT_PLANTUML_ARGS = [] +DEFAULT_PLANTUML_FILE_OPTIONS = [] DEFAULT_PLANTUML_DEBUG = False @@ -31,12 +31,12 @@ class PlantUmlTask(Task): name = 'plantuml' - _common_args = ... # type: List[str] + _file_options = ... # type: List[str] plantuml_manager = ... # Optional[PlantUmlManager] def set_site(self, site): super().set_site(site) - self._common_args = list(site.config.get('PLANTUML_ARGS', DEFAULT_PLANTUML_ARGS)) + self._file_options = list(site.config.get('PLANTUML_FILE_OPTIONS', DEFAULT_PLANTUML_FILE_OPTIONS)) self.plantuml_manager = PlantUmlManager(site) def gen_tasks(self): @@ -48,17 +48,17 @@ def gen_tasks(self): output_path = Path(output_folder) # Logic derived from nikola.plugins.misc.scan_posts.ScanPosts.scan() - for pattern, destination, extension, args in plantuml_files: - combined_args = self._common_args + args + for pattern, destination, extension, options in plantuml_files: + combined_options = self._file_options + options kw = { - 'combined_args': combined_args, + 'combined_options': combined_options, 'filters': filters, 'output_folder': output_folder, } # TODO figure out exactly what the PlantUML include patterns do and expand them similarly here - includes = list(set(a[2:] for a in combined_args if a.startswith('-I') and '*' not in a and '?' not in a)) + includes = list(set(a[2:] for a in combined_options if a.startswith('-I') and '*' not in a and '?' not in a)) pattern = Path(pattern) root = pattern.parent @@ -71,14 +71,14 @@ def gen_tasks(self): 'name': dst_str, 'file_dep': includes + [str(src)], 'targets': [dst_str], - 'actions': [(self.render_file, [src, dst, combined_args + ['-filename', src.name]])], + 'actions': [(self.render_file, [src, dst, combined_options + ['-filename', src.name]])], 'uptodate': [utils.config_changed(kw, 'plantuml:' + dst_str)], 'clean': True, } yield utils.apply_filters(task, filters) - def render_file(self, src: Path, dst: Path, args: Sequence[str]) -> bool: - output, error = self.plantuml_manager.render(src.read_bytes(), args) + def render_file(self, src: Path, dst: Path, options: Sequence[str]) -> bool: + output, error = self.plantuml_manager.render(src.read_bytes(), options) dst.parent.mkdir(parents=True, exist_ok=True) dst.write_bytes(output) @@ -103,15 +103,15 @@ def __init__(self, site) -> None: if site.config.get('PLANTUML_DEBUG', DEFAULT_PLANTUML_DEBUG): self.logger.level = DEBUG - def render(self, source: bytes, args: Sequence[str]) -> Tuple[bytes, Optional[str]]: + def render(self, source: bytes, options: Sequence[str]) -> Tuple[bytes, Optional[str]]: """Returns (output, error)""" - def process_arg(arg): - return arg \ + def process_option(opt): + return opt \ .replace('%site_path%', os.getcwd()) \ .encode('utf8') - command = list(map(process_arg, chain(self.exec, args, ['-pipe', '-stdrpt']))) + command = list(map(process_option, chain(self.exec, options, ['-pipe', '-stdrpt']))) self.logger.debug('render() exec: %s\n%s', command, source) diff --git a/v8/plantuml_markdown/conf.py.sample b/v8/plantuml_markdown/conf.py.sample index 884e6fb2..a520d271 100644 --- a/v8/plantuml_markdown/conf.py.sample +++ b/v8/plantuml_markdown/conf.py.sample @@ -8,11 +8,11 @@ # # -# PLANTUML_MARKDOWN_ARGS (list of strings) - CLI arguments that are sent to PlantUML when rendering for markdown files, +# PLANTUML_MARKDOWN_OPTIONS (list of strings) - options used when rendering for markdown files, # see https://plantuml.com/command-line # -# Note this is independent of PLANTUML_ARGS in the "plantuml" plugin. -# If you want them to be the same then do "PLANTUML_ARGS = PLANTUML_MARKDOWN_ARGS = [ ... ]" +# Note this is independent of PLANTUML_FILE_OPTIONS in the "plantuml" plugin. +# If you want them to be the same then do "PLANTUML_FILE_OPTIONS = PLANTUML_MARKDOWN_OPTIONS = [ ... ]" # # Examples # -------- @@ -22,4 +22,4 @@ # Specify the style in conf.py # [ '-chide footbox', '-SShadowing=false' ] # -PLANTUML_MARKDOWN_ARGS = [] +PLANTUML_MARKDOWN_OPTIONS = [] diff --git a/v8/plantuml_markdown/plantuml_markdown.py b/v8/plantuml_markdown/plantuml_markdown.py index 3a27d69a..91f83f39 100644 --- a/v8/plantuml_markdown/plantuml_markdown.py +++ b/v8/plantuml_markdown/plantuml_markdown.py @@ -9,7 +9,7 @@ from nikola.plugin_categories import MarkdownExtension from nikola.utils import LocaleBorg, req_missing, slugify -DEFAULT_PLANTUML_MARKDOWN_ARGS = [] +DEFAULT_PLANTUML_MARKDOWN_OPTIONS = [] class PlantUmlMarkdownProcessor(FencedBlockPreprocessor): @@ -17,7 +17,7 @@ def __init__(self, md, config, site, logger): super().__init__(md, config) self._logger = logger self._plantuml_manager = None # Lazily retrieved because it might not exist right now - self._plantuml_markdown_args = list(site.config.get('PLANTUML_MARKDOWN_ARGS', DEFAULT_PLANTUML_MARKDOWN_ARGS)) + self._markdown_options = list(site.config.get('PLANTUML_MARKDOWN_OPTIONS', DEFAULT_PLANTUML_MARKDOWN_OPTIONS)) self._prefix: List[str] = [] self._site: Nikola = site @@ -65,7 +65,7 @@ def listing(): def svg(): rendered_bytes, error = self.plantuml_manager.render( match.group('code').encode('utf8'), - self._plantuml_markdown_args + self._prefix + ['-tsvg'] + self._markdown_options + self._prefix + ['-tsvg'] ) if error: # Note we never "continue" when rendered_bytes is empty because that likely means PlantUML failed to start From facb5f6e088c54c3c3fef99469f16bbca6df7dc7 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Tue, 16 Feb 2021 13:41:31 +1100 Subject: [PATCH 02/13] Bump version to 1.0.0 Add CHANGES.md --- v8/plantuml/CHANGES.md | 11 +++++++++++ v8/plantuml/plantuml.plugin | 2 +- v8/plantuml_markdown/CHANGES.md | 5 +++++ v8/plantuml_markdown/plantuml_markdown.plugin | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 v8/plantuml/CHANGES.md create mode 100644 v8/plantuml_markdown/CHANGES.md diff --git a/v8/plantuml/CHANGES.md b/v8/plantuml/CHANGES.md new file mode 100644 index 00000000..7928f1a8 --- /dev/null +++ b/v8/plantuml/CHANGES.md @@ -0,0 +1,11 @@ +# 1.0.0 +* Renamed `PLANTUML_ARGS` config option to `PLANTUML_FILE_OPTIONS`. + +# 0.2.0 +* Add `PlantUmlTask.plantuml_manager` so the `plantuml_markdown` plugin can use it. + +# 0.1.1 +* Update PLANTUML_FILES in conf.py.sample to match the default behaviour. + +# 0.1 +* First release diff --git a/v8/plantuml/plantuml.plugin b/v8/plantuml/plantuml.plugin index 21a4c7ef..ad03afbb 100644 --- a/v8/plantuml/plantuml.plugin +++ b/v8/plantuml/plantuml.plugin @@ -9,6 +9,6 @@ PluginCategory = Task [Documentation] Author = Matthew Leather -Version = 0.2.0 +Version = 1.0.0 Website = https://plugins.getnikola.com/#plantuml Description = Renders PlantUML files diff --git a/v8/plantuml_markdown/CHANGES.md b/v8/plantuml_markdown/CHANGES.md new file mode 100644 index 00000000..1beeba5c --- /dev/null +++ b/v8/plantuml_markdown/CHANGES.md @@ -0,0 +1,5 @@ +# 1.0.0 +* Renamed `PLANTUML_MARKDOWN_ARGS` config option to `PLANTUML_MARKDOWN_OPTIONS` + +# 0.1.0 +* First release diff --git a/v8/plantuml_markdown/plantuml_markdown.plugin b/v8/plantuml_markdown/plantuml_markdown.plugin index 145f8255..3e38c6f8 100644 --- a/v8/plantuml_markdown/plantuml_markdown.plugin +++ b/v8/plantuml_markdown/plantuml_markdown.plugin @@ -10,6 +10,6 @@ Compiler = markdown [Documentation] Author = Matthew Leather -Version = 0.1.0 +Version = 1.0.0 Website = https://plugins.getnikola.com/#plantuml_markdown Description = Markdown extension for PlantUML From df34b33919abd52328f11ae3627197501f6ca9d1 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Tue, 16 Feb 2021 14:02:42 +1100 Subject: [PATCH 03/13] Fail if PLANTUML_ARGS or PLANTUML_MARKDOWN_ARGS is still being used. --- v8/plantuml/plantuml.py | 2 ++ v8/plantuml_markdown/plantuml_markdown.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/v8/plantuml/plantuml.py b/v8/plantuml/plantuml.py index efd112c9..87b3f84e 100644 --- a/v8/plantuml/plantuml.py +++ b/v8/plantuml/plantuml.py @@ -36,6 +36,8 @@ class PlantUmlTask(Task): def set_site(self, site): super().set_site(site) + if 'PLANTUML_ARGS' in site.config: + raise Exception('PLANTUML_ARGS is no longer supported, please use PLANTUML_FILE_OPTIONS instead') self._file_options = list(site.config.get('PLANTUML_FILE_OPTIONS', DEFAULT_PLANTUML_FILE_OPTIONS)) self.plantuml_manager = PlantUmlManager(site) diff --git a/v8/plantuml_markdown/plantuml_markdown.py b/v8/plantuml_markdown/plantuml_markdown.py index 91f83f39..7aec7d0e 100644 --- a/v8/plantuml_markdown/plantuml_markdown.py +++ b/v8/plantuml_markdown/plantuml_markdown.py @@ -15,6 +15,8 @@ class PlantUmlMarkdownProcessor(FencedBlockPreprocessor): def __init__(self, md, config, site, logger): super().__init__(md, config) + if 'PLANTUML_MARKDOWN_ARGS' in site.config: + raise Exception('PLANTUML_MARKDOWN_ARGS is no longer supported, please use PLANTUML_MARKDOWN_OPTIONS instead') self._logger = logger self._plantuml_manager = None # Lazily retrieved because it might not exist right now self._markdown_options = list(site.config.get('PLANTUML_MARKDOWN_OPTIONS', DEFAULT_PLANTUML_MARKDOWN_OPTIONS)) From 53a529806dc813a5c36f82541acdc14b80d5599e Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Tue, 16 Feb 2021 14:22:07 +1100 Subject: [PATCH 04/13] Move Python 3.6 requirement to bottom of https://plugins.getnikola.com/v8/plantuml_markdown/ along with the other requirements. --- v8/plantuml_markdown/README.md | 4 +--- v8/plantuml_markdown/requirements-nonpy.txt | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 v8/plantuml_markdown/requirements-nonpy.txt diff --git a/v8/plantuml_markdown/README.md b/v8/plantuml_markdown/README.md index 79869d08..739a263e 100644 --- a/v8/plantuml_markdown/README.md +++ b/v8/plantuml_markdown/README.md @@ -1,8 +1,6 @@ This plugin renders [PlantUML](https://plantuml.com/) in Markdown files. -# Requirements - -* Python >= 3.6 (we use `markdown>=3.3.0` which requires it) +Requires Python >= 3.6 because we use `markdown>=3.3.0` which requires it. # Usage diff --git a/v8/plantuml_markdown/requirements-nonpy.txt b/v8/plantuml_markdown/requirements-nonpy.txt new file mode 100644 index 00000000..2f0e54e1 --- /dev/null +++ b/v8/plantuml_markdown/requirements-nonpy.txt @@ -0,0 +1 @@ +Python>=3.6::https://www.python.org/ From 29d3ff1342e8d0499fdf573e14acdc7b30ceb1e0 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 18 Feb 2021 13:02:37 +1100 Subject: [PATCH 05/13] Add support for PlantUML PicoWeb server. --- .github/workflows/ci.yml | 6 + tests/__init__.py | 7 +- tests/conftest.py | 19 ++- tests/test_plantuml.py | 12 +- tests/test_plantuml_markdown.py | 8 +- v8/plantuml/CHANGES.md | 5 +- v8/plantuml/README.md | 5 - v8/plantuml/conf.py.sample | 47 +++++++- v8/plantuml/plantuml.py | 184 +++++++++++++++++++++++++++-- v8/plantuml/requirements-nonpy.txt | 2 +- v8/plantuml_markdown/CHANGES.md | 5 +- 11 files changed, 265 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58d63783..53aca528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,12 @@ jobs: PLANTUML_EXEC: java -Djava.awt.headless=true -jar ${{github.workspace}}/plantuml.jar run: | py.test --color=yes tests/ + - name: Run PlantUML PicoWeb tests + env: + PLANTUML_SYSTEM: picoweb + PLANTUML_PICOWEB_START_COMMAND: java -Djava.awt.headless=true -jar ${{github.workspace}}/plantuml.jar -picoweb:0:localhost + run: | + py.test --color=yes tests/plantuml* flake8: name: Linting (flake8) strategy: diff --git a/tests/__init__.py b/tests/__init__.py index 68a35aea..5c8f5499 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -import re +import os from functools import lru_cache from pathlib import Path from textwrap import dedent @@ -11,6 +11,7 @@ __all__ = [ 'cached_property', 'execute_plugin_tasks', + 'getenv_split', 'simple_html_page', 'TEST_DATA_PATH', 'V7_PLUGIN_PATH', @@ -36,6 +37,10 @@ def execute_plugin_tasks(plugin: Task, verbosity: int = 0): raise Exception("Task error for '{}'\n{}".format(t.name, catched.get_msg())) +def getenv_split(key: str, default=None): + return os.environ[key].split() if key in os.environ else default + + def simple_html_page(body: str) -> str: return dedent(''' diff --git a/tests/conftest.py b/tests/conftest.py index 6108b21a..7fee984c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from nikola import Nikola from nikola.post import Post from nikola.utils import LocaleBorg -from tests import cached_property, simple_html_page +from tests import cached_property, getenv_split, simple_html_page @fixture @@ -115,6 +115,23 @@ def localeborg_setup(default_locale): LocaleBorg.reset() +@fixture +def maybe_plantuml_picoweb_server(tmp_site_path): + if os.getenv('PLANTUML_SYSTEM') == 'picoweb': + from v8.plantuml.plantuml import DEFAULT_PLANTUML_PICOWEB_START_COMMAND, DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS, \ + DEFAULT_PLANTUML_PICOWEB_URL, PicoWebSupervisor + supervisor = PicoWebSupervisor( + command=getenv_split('PLANTUML_PICOWEB_START_COMMAND', DEFAULT_PLANTUML_PICOWEB_START_COMMAND), + start_timeout=os.getenv('PLANTUML_PICOWEB_START_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS), + url_template=os.getenv('PLANTUML_PICOWEB_URL', DEFAULT_PLANTUML_PICOWEB_URL), + stop_after_main_thread=False, + ) + yield + supervisor.stop() + else: + yield + + @fixture def tmp_site_path(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) diff --git a/tests/test_plantuml.py b/tests/test_plantuml.py index 286e25dc..3677ba9c 100644 --- a/tests/test_plantuml.py +++ b/tests/test_plantuml.py @@ -2,12 +2,12 @@ from textwrap import dedent from typing import Dict -from tests import execute_plugin_tasks -from v8.plantuml.plantuml import PlantUmlTask +from tests import execute_plugin_tasks, getenv_split +from v8.plantuml.plantuml import DEFAULT_PLANTUML_EXEC, DEFAULT_PLANTUML_SYSTEM, PlantUmlTask # Note this test is also sufficient to prove that rendering binary image files will work -def test_render_file_success(tmp_site_path): +def test_render_file_success(maybe_plantuml_picoweb_server, tmp_site_path): (tmp_site_path / 'pages' / 'test.puml').write_text(dedent('''\ @startuml title filename="%filename()" @@ -47,7 +47,7 @@ def test_render_file_success(tmp_site_path): ] -def test_render_file_error(tmp_site_path): +def test_render_file_error(maybe_plantuml_picoweb_server, tmp_site_path): (tmp_site_path / 'pages' / 'test.puml').write_text(dedent('''\ @startuml A -> B @@ -142,10 +142,10 @@ def __init__(self, config: Dict): 'FILTERS': {}, 'OUTPUT_FOLDER': 'output', 'PLANTUML_DEBUG': True, + 'PLANTUML_EXEC': getenv_split('PLANTUML_EXEC', DEFAULT_PLANTUML_EXEC), + 'PLANTUML_SYSTEM': os.getenv('PLANTUML_SYSTEM', DEFAULT_PLANTUML_SYSTEM), } self.config.update(config) - if 'PLANTUML_EXEC' in os.environ: - self.config['PLANTUML_EXEC'] = os.environ['PLANTUML_EXEC'].split() def plugin_tasks(plugin): diff --git a/tests/test_plantuml_markdown.py b/tests/test_plantuml_markdown.py index 7742de65..66574251 100644 --- a/tests/test_plantuml_markdown.py +++ b/tests/test_plantuml_markdown.py @@ -7,8 +7,9 @@ if sys.version_info < (3, 6): raise pytest.skip("plantuml_markdown plugin requires Python >= 3.6", allow_module_level=True) -from tests import V8_PLUGIN_PATH +from tests import V8_PLUGIN_PATH, getenv_split from tests.conftest import CompileResult +from v8.plantuml.plantuml import DEFAULT_PLANTUML_EXEC, DEFAULT_PLANTUML_SYSTEM from v8.plantuml_markdown.plantuml_markdown import PlantUmlMarkdownProcessor, first_line_for_listing_block @@ -146,7 +147,7 @@ def test_first_line_for_listing_block(line, expected): @fixture -def do_compile_test(basic_compile_test): +def do_compile_test(basic_compile_test, maybe_plantuml_picoweb_server): def f(data: str, plantuml_continue_after_failure=False) -> CompileResult: return basic_compile_test( '.md', @@ -154,12 +155,13 @@ def f(data: str, plantuml_continue_after_failure=False) -> CompileResult: extra_config={ 'PLANTUML_DEBUG': True, 'PLANTUML_CONTINUE_AFTER_FAILURE': plantuml_continue_after_failure, - 'PLANTUML_EXEC': os.environ.get('PLANTUML_EXEC', 'plantuml').split(), + 'PLANTUML_EXEC': getenv_split('PLANTUML_EXEC', DEFAULT_PLANTUML_EXEC), 'PLANTUML_MARKDOWN_OPTIONS': [ '-chide footbox', '-nometadata', '-Sshadowing=false', ], + 'PLANTUML_SYSTEM': os.getenv('PLANTUML_SYSTEM', DEFAULT_PLANTUML_SYSTEM), }, extra_plugins_dirs=[ V8_PLUGIN_PATH / 'plantuml', diff --git a/v8/plantuml/CHANGES.md b/v8/plantuml/CHANGES.md index 7928f1a8..ba12e180 100644 --- a/v8/plantuml/CHANGES.md +++ b/v8/plantuml/CHANGES.md @@ -1,5 +1,6 @@ # 1.0.0 -* Renamed `PLANTUML_ARGS` config option to `PLANTUML_FILE_OPTIONS`. +* Add support for PlantUML PicoWeb server. +* Rename `PLANTUML_ARGS` config option to `PLANTUML_FILE_OPTIONS`. # 0.2.0 * Add `PlantUmlTask.plantuml_manager` so the `plantuml_markdown` plugin can use it. @@ -8,4 +9,4 @@ * Update PLANTUML_FILES in conf.py.sample to match the default behaviour. # 0.1 -* First release +* First release. diff --git a/v8/plantuml/README.md b/v8/plantuml/README.md index 59a5a730..d2a71f06 100644 --- a/v8/plantuml/README.md +++ b/v8/plantuml/README.md @@ -2,17 +2,12 @@ This plugin converts [PlantUML](https://plantuml.com/) files. The default configuration will output all `*.puml` files found under the `plantuml` dir as SVG files. -Developed against PlantUML version 1.2020.24. Probably works with some earlier versions. - # Unicode The plugin expects PlantUML files to be encoded with UTF-8. # Known Issues -- It's slow! Every PlantUML rendering launches a new Java process, on my laptop it takes 4-8 seconds per file. - I have some ideas to speed this up, and they may be available in future plugin versions. - - Changes to files included via `!include ...` or via a pattern (e.g. `-Ipath/to/*.iuml`) will NOT trigger a rebuild. Instead, if you include them explicitly in `PLANTUML_FILE_OPTIONS` (e.g. `-Ipath/to/foo.iuml`) then they will trigger a rebuild. diff --git a/v8/plantuml/conf.py.sample b/v8/plantuml/conf.py.sample index f43efa8a..3b995ec2 100644 --- a/v8/plantuml/conf.py.sample +++ b/v8/plantuml/conf.py.sample @@ -1,7 +1,24 @@ +# +# PLANTUML_SYSTEM ('exec' or 'picoweb') +# +# exec The PLANTUML_EXEC command is run separately for each rendering, +# this starts a new Java process each time so will be slow. +# +# picoweb An HTTP request is sent to PLANTUML_PICOWEB_URL for each rendering, this is much faster than 'exec'. +# +# If PLANTUML_PICOWEB_START_COMMAND is an empty list then Nikola assumes PicoWeb is already running, +# otherwise Nikola will run PLANTUML_PICOWEB_START_COMMAND the first time PicoWeb is needed. +# +# However if the 'PLANTUML_PICOWEB_URL' environment variable is set then that URL is used and +# PLANTUML_PICOWEB_START_COMMAND / PLANTUML_PICOWEB_URL config options are ignored. +# This is kind of a kludge so 'nikola auto' can start the server and share it with child builds processes. +# +PLANTUML_SYSTEM = 'exec' + # # PLANTUML_EXEC (list of strings) - The command to run PlantUML # -# '%site_path%' anywhere in PLANTUML_EXEC will be replaced with the full path to the site dir. +# %site_path% anywhere in PLANTUML_EXEC will be replaced with the full path to the site dir. # PlantUML is run in the site dir so often this is not needed. # # Examples @@ -48,7 +65,7 @@ PLANTUML_FILES = ( # # PLANTUML_CONTINUE_AFTER_FAILURE (boolean) - If True then Nikola will continue executing after any PlantUML failures. # -# PlantUML puts its error messages in the rendered output so you might find this option helpful when running `nikola auto`. +# PlantUML puts its error messages in the rendered output so you might find this option helpful when running 'nikola auto'. # PLANTUML_CONTINUE_AFTER_FAILURE = False @@ -56,3 +73,29 @@ PLANTUML_CONTINUE_AFTER_FAILURE = False # PLANTUML_DEBUG (boolean) - Control plugin verbosity # PLANTUML_DEBUG = False + +# +# PLANTUML_PICOWEB_START_COMMAND (list of strings) - The command to start a PlantUML PicoWeb Server +# +# %site_path% anywhere in PLANTUML_PICOWEB_START_COMMAND will be replaced with the full path to the site dir. +# PlantUML is run in the site dir so often this is not needed. +# +PLANTUML_PICOWEB_START_COMMAND = ['plantuml', '-picoweb:0:localhost'] + +# +# PLANTUML_PICOWEB_URL (string) - URL of the PicoWeb Server +# +# If Nikola starts a PicoWeb Server then %port% anywhere in PLANTUML_PICOWEB_URL will be replaced by the actual +# port number of the server. +# +PLANTUML_PICOWEB_URL = 'http://localhost:%port%' + +# +# PLANTUML_PICOWEB_START_TIMEOUT_SECONDS (int) - Maximum time to wait for the PicoWeb server to start. +# +PLANTUML_PICOWEB_START_TIMEOUT_SECONDS = 30 + +# +# PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS (int) - Maximum time to wait for a single rendering. +# +PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS = 30 diff --git a/v8/plantuml/plantuml.py b/v8/plantuml/plantuml.py index 87b3f84e..71da9bc3 100644 --- a/v8/plantuml/plantuml.py +++ b/v8/plantuml/plantuml.py @@ -1,9 +1,17 @@ +import json import os import subprocess from itertools import chain from logging import DEBUG from pathlib import Path -from typing import List, Optional, Sequence, Tuple +from queue import Empty, Queue +from subprocess import Popen +from threading import Lock, Thread, main_thread +from typing import Iterable, List, Optional, Sequence, Tuple + +import blinker +import requests +from requests import HTTPError from nikola import utils from nikola.log import get_logger @@ -21,6 +29,18 @@ DEFAULT_PLANTUML_CONTINUE_AFTER_FAILURE = False +DEFAULT_PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS = 30 + +DEFAULT_PLANTUML_PICOWEB_START_COMMAND = ['plantuml', '-picoweb:0:localhost'] + +DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS = 30 + +DEFAULT_PLANTUML_PICOWEB_URL = 'http://localhost:%port%' + +DEFAULT_PLANTUML_SYSTEM = 'exec' + +PICOWEB_URL_ENV_VAR = 'PLANTUML_PICOWEB_URL' + # TODO when 3.5 support is dropped # - Use capture_output arg in subprocess.run() @@ -32,14 +52,14 @@ class PlantUmlTask(Task): name = 'plantuml' _file_options = ... # type: List[str] - plantuml_manager = ... # Optional[PlantUmlManager] + plantuml_manager = ... # type: Optional[PlantUmlManager] def set_site(self, site): super().set_site(site) if 'PLANTUML_ARGS' in site.config: raise Exception('PLANTUML_ARGS is no longer supported, please use PLANTUML_FILE_OPTIONS instead') self._file_options = list(site.config.get('PLANTUML_FILE_OPTIONS', DEFAULT_PLANTUML_FILE_OPTIONS)) - self.plantuml_manager = PlantUmlManager(site) + self.plantuml_manager = PlantUmlManager.create(site) def gen_tasks(self): yield self.group_task() @@ -98,24 +118,40 @@ def render_file(self, src: Path, dst: Path, options: Sequence[str]) -> bool: class PlantUmlManager: """PlantUmlManager is used by 'plantuml' and 'plantuml_markdown' plugins""" + @classmethod + def create(cls, site): + system = site.config.get('PLANTUML_SYSTEM', DEFAULT_PLANTUML_SYSTEM) + if system == 'exec': + return ExecPlantUmlManager(site) + elif system == 'picoweb': + return PicowebPlantUmlManager(site) + else: + raise ValueError('Unknown PLANTUML_SYSTEM "{}"'.format(system)) + def __init__(self, site) -> None: self.continue_after_failure = site.config.get('PLANTUML_CONTINUE_AFTER_FAILURE', DEFAULT_PLANTUML_CONTINUE_AFTER_FAILURE) - self.exec = site.config.get('PLANTUML_EXEC', DEFAULT_PLANTUML_EXEC) - self.logger = get_logger('plantuml_manager') + self._logger = get_logger(self.__class__.__name__) if site.config.get('PLANTUML_DEBUG', DEFAULT_PLANTUML_DEBUG): - self.logger.level = DEBUG + self._logger.level = DEBUG def render(self, source: bytes, options: Sequence[str]) -> Tuple[bytes, Optional[str]]: """Returns (output, error)""" + raise NotImplementedError + + @staticmethod + def _process_options(options: Iterable[str]) -> Sequence[str]: + return [o.replace('%site_path%', os.getcwd()) for o in options] + - def process_option(opt): - return opt \ - .replace('%site_path%', os.getcwd()) \ - .encode('utf8') +class ExecPlantUmlManager(PlantUmlManager): + def __init__(self, site) -> None: + super().__init__(site) + self._exec = site.config.get('PLANTUML_EXEC', DEFAULT_PLANTUML_EXEC) - command = list(map(process_option, chain(self.exec, options, ['-pipe', '-stdrpt']))) + def render(self, source: bytes, options: Sequence[str]) -> Tuple[bytes, Optional[str]]: + command = [o.encode('utf8') for o in self._process_options(chain(self._exec, options, ['-pipe', '-stdrpt']))] - self.logger.debug('render() exec: %s\n%s', command, source) + self._logger.debug('render() exec: %s\n%s', command, source) result = subprocess.run(command, input=source, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -128,3 +164,127 @@ def process_option(opt): details = str(result.stderr) return result.stdout, "PlantUML error (return code {}): {}".format(result.returncode, details) + + +class PicowebPlantUmlManager(PlantUmlManager): + def __init__(self, site) -> None: + super().__init__(site) + self._site = site + self._lock = Lock() + self._render_timeout = site.config.get('PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS) + self._start_command = site.config.get('PLANTUML_PICOWEB_START_COMMAND', DEFAULT_PLANTUML_PICOWEB_START_COMMAND) + self._start_timeout = site.config.get('PLANTUML_PICOWEB_START_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS) + + if PICOWEB_URL_ENV_VAR in os.environ: + self._server_available = True + self._url = os.getenv(PICOWEB_URL_ENV_VAR) + else: + # If there is no start command then we assume a server is already running + self._server_available = self._start_command == [] + self._url = site.config.get('PLANTUML_PICOWEB_URL', DEFAULT_PLANTUML_PICOWEB_URL) + + if self._server_available: + self._logger.debug('Will use an already running PlantUML PicoWeb server at "%s"', self._url) + + blinker.signal('auto_command_starting').connect(self._on_auto_command_starting) + + def render(self, source: bytes, options: Sequence[str]) -> Tuple[bytes, Optional[str]]: + self._maybe_start_picoweb() + + data = json.dumps({ + 'options': self._process_options(options), + 'source': str(source, 'utf8') + }) + + self._logger.debug('render() %s', data) + response = requests.post(self._url + '/render', data=data, timeout=self._render_timeout, allow_redirects=False) + + if response.status_code == 200: + return response.content, self._error_message_from_picoweb_response(response) + + if response.status_code == 302 \ + and response.headers['location'] == '/plantuml/png/oqbDJyrBuGh8ISmh2VNrKGZ8JCuFJqqAJYqgIotY0aefG5G00000': + raise Exception('This version of PlantUML does not support "POST /render", you need a version >= 1.2021.2') + + try: + response.raise_for_status() + except HTTPError as e: + text = response.text + if text: + raise HTTPError("{} - {}".format(e, text), response=e.response) + else: + raise + + raise Exception('Unexpected {} response from PlantUML PicoWeb server'.format(response.status_code)) + + @staticmethod + def _error_message_from_picoweb_response(response): + if 'X-PlantUML-Diagram-Error' in response.headers: + return "PlantUML Error (line={}): {}".format( + response.headers['X-PlantUML-Diagram-Error-Line'], + response.headers['X-PlantUML-Diagram-Error'] + ) + else: + return None + + def _on_auto_command_starting(self, site): # noqa + self._maybe_start_picoweb() + + def _maybe_start_picoweb(self): + with self._lock: + if self._server_available: + return + PicoWebSupervisor( + command=self._process_options(self._start_command), + start_timeout=self._start_timeout, + url_template=self._url, + stop_after_main_thread=True, + ) + self._url = os.environ[PICOWEB_URL_ENV_VAR] + self._server_available = True + + +class PicoWebSupervisor: + def __init__(self, command: Sequence[str], start_timeout, url_template: str, stop_after_main_thread: bool): + logger = get_logger('plantuml_picoweb') + + command_bytes = [c.encode('utf8') for c in command] + logger.info('Starting PlantUML Picoweb server, command=%s', command_bytes) + + if stop_after_main_thread: + # Ensure the logging thread finishes and we dont leave behind an orphan PicoWeb process + def _stop(): + main_thread().join() + self.stop() + + Thread(target=_stop, name='plantuml-picoweb-stop').start() + + self._process = Popen(command_bytes, stderr=subprocess.PIPE) + + queue = Queue() + + def process_logging(): + looking_for_port = True + for line in self._process.stderr: + if looking_for_port and line.startswith(b'webPort='): + queue.put(int(line[8:])) + looking_for_port = False + else: + logger.error(str(line, 'utf8').rstrip()) + + # Not a daemon thread because those can be stopped abruptly and the final logging lines might be lost + Thread(target=process_logging, name='plantuml-picoweb-logging').start() + + try: + port = queue.get(timeout=start_timeout) + except Empty: + raise Exception('Timeout waiting for PlantUML PicoWeb server to start') + + url = os.environ[PICOWEB_URL_ENV_VAR] = url_template.replace('%port%', str(port)) + logger.info('PlantUML PicoWeb server is listening at "%s"', url) + + def stop(self): + if self._process: + self._process.terminate() + if PICOWEB_URL_ENV_VAR in os.environ: + del os.environ[PICOWEB_URL_ENV_VAR] diff --git a/v8/plantuml/requirements-nonpy.txt b/v8/plantuml/requirements-nonpy.txt index d8f007ad..2b20143c 100644 --- a/v8/plantuml/requirements-nonpy.txt +++ b/v8/plantuml/requirements-nonpy.txt @@ -1 +1 @@ -PlantUML::https://plantuml.com/ +PlantUML>=1.2021.2::https://plantuml.com/ diff --git a/v8/plantuml_markdown/CHANGES.md b/v8/plantuml_markdown/CHANGES.md index 1beeba5c..92f69834 100644 --- a/v8/plantuml_markdown/CHANGES.md +++ b/v8/plantuml_markdown/CHANGES.md @@ -1,5 +1,6 @@ # 1.0.0 -* Renamed `PLANTUML_MARKDOWN_ARGS` config option to `PLANTUML_MARKDOWN_OPTIONS` +* Rename `PLANTUML_MARKDOWN_ARGS` config option to `PLANTUML_MARKDOWN_OPTIONS`. +* Update tests to suit `plantuml` plugin v1.0.0. # 0.1.0 -* First release +* First release. From 1eb79d138533c8caa0a60c7eab32e0f9486746e1 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 18 Feb 2021 13:17:40 +1100 Subject: [PATCH 06/13] Doc tweaks --- v8/plantuml/CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v8/plantuml/CHANGES.md b/v8/plantuml/CHANGES.md index ba12e180..55ac9176 100644 --- a/v8/plantuml/CHANGES.md +++ b/v8/plantuml/CHANGES.md @@ -6,7 +6,7 @@ * Add `PlantUmlTask.plantuml_manager` so the `plantuml_markdown` plugin can use it. # 0.1.1 -* Update PLANTUML_FILES in conf.py.sample to match the default behaviour. +* Update `PLANTUML_FILES` in `conf.py.sample` to match the default behaviour. # 0.1 * First release. From b1d6e569370e4c9b142f737afb7ca7686ee181e9 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 18 Feb 2021 13:33:28 +1100 Subject: [PATCH 07/13] Maybe 'apt-get update' will fix the build? --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53aca528..356c338b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,8 @@ jobs: env: PLANTUML_VERSION: 1.2020.24 run: | + sudo apt-get update + # Not using "apt-get install plantuml" because it's an old version sudo apt-get install default-jre-headless From 250cfe089f147f2e951d15dfcb048a072fe2e658 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 18 Feb 2021 13:54:20 +1100 Subject: [PATCH 08/13] Oops --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 356c338b..eb677e4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: PLANTUML_SYSTEM: picoweb PLANTUML_PICOWEB_START_COMMAND: java -Djava.awt.headless=true -jar ${{github.workspace}}/plantuml.jar -picoweb:0:localhost run: | - py.test --color=yes tests/plantuml* + py.test --color=yes tests/test_plantuml* flake8: name: Linting (flake8) strategy: From 28b23d0f5ee705d7ef1bc6d0ddd649719a5c47c0 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Fri, 19 Feb 2021 12:43:26 +1100 Subject: [PATCH 09/13] Better handing of PicoWeb termination --- tests/conftest.py | 10 +++-- v8/plantuml/plantuml.py | 86 ++++++++++++++++++++++++----------------- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7fee984c..a06d4a39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ from nikola.post import Post from nikola.utils import LocaleBorg from tests import cached_property, getenv_split, simple_html_page +from v8.plantuml.plantuml import PICOWEB_URL_ENV_VAR @fixture @@ -116,16 +117,17 @@ def localeborg_setup(default_locale): @fixture -def maybe_plantuml_picoweb_server(tmp_site_path): +def maybe_plantuml_picoweb_server(monkeypatch, tmp_site_path): if os.getenv('PLANTUML_SYSTEM') == 'picoweb': from v8.plantuml.plantuml import DEFAULT_PLANTUML_PICOWEB_START_COMMAND, DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS, \ DEFAULT_PLANTUML_PICOWEB_URL, PicoWebSupervisor - supervisor = PicoWebSupervisor( + supervisor = PicoWebSupervisor() + supervisor.start( command=getenv_split('PLANTUML_PICOWEB_START_COMMAND', DEFAULT_PLANTUML_PICOWEB_START_COMMAND), - start_timeout=os.getenv('PLANTUML_PICOWEB_START_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS), + timeout=os.getenv('PLANTUML_PICOWEB_START_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_START_TIMEOUT_SECONDS), url_template=os.getenv('PLANTUML_PICOWEB_URL', DEFAULT_PLANTUML_PICOWEB_URL), - stop_after_main_thread=False, ) + monkeypatch.setenv(PICOWEB_URL_ENV_VAR, supervisor.url) yield supervisor.stop() else: diff --git a/v8/plantuml/plantuml.py b/v8/plantuml/plantuml.py index 71da9bc3..0cf3fe64 100644 --- a/v8/plantuml/plantuml.py +++ b/v8/plantuml/plantuml.py @@ -2,7 +2,7 @@ import os import subprocess from itertools import chain -from logging import DEBUG +from logging import DEBUG, INFO, WARNING from pathlib import Path from queue import Empty, Queue from subprocess import Popen @@ -169,7 +169,6 @@ def render(self, source: bytes, options: Sequence[str]) -> Tuple[bytes, Optional class PicowebPlantUmlManager(PlantUmlManager): def __init__(self, site) -> None: super().__init__(site) - self._site = site self._lock = Lock() self._render_timeout = site.config.get('PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS', DEFAULT_PLANTUML_PICOWEB_RENDER_TIMEOUT_SECONDS) self._start_command = site.config.get('PLANTUML_PICOWEB_START_COMMAND', DEFAULT_PLANTUML_PICOWEB_START_COMMAND) @@ -234,57 +233,74 @@ def _maybe_start_picoweb(self): with self._lock: if self._server_available: return - PicoWebSupervisor( + supervisor = PicoWebSupervisor() + supervisor.stop_after_main_thread() + supervisor.start( command=self._process_options(self._start_command), - start_timeout=self._start_timeout, + timeout=self._start_timeout, url_template=self._url, - stop_after_main_thread=True, ) - self._url = os.environ[PICOWEB_URL_ENV_VAR] + self._url = os.environ[PICOWEB_URL_ENV_VAR] = supervisor.url self._server_available = True class PicoWebSupervisor: - def __init__(self, command: Sequence[str], start_timeout, url_template: str, stop_after_main_thread: bool): - logger = get_logger('plantuml_picoweb') - + def __init__(self): + self._logger = get_logger('plantuml_picoweb') + self._logging_thread = None + self._process = None + self._queue = Queue() + self.url = None + + def start(self, command: Sequence[str], url_template: str, timeout: int): command_bytes = [c.encode('utf8') for c in command] - logger.info('Starting PlantUML Picoweb server, command=%s', command_bytes) - - if stop_after_main_thread: - # Ensure the logging thread finishes and we dont leave behind an orphan PicoWeb process - def _stop(): - main_thread().join() - self.stop() - - Thread(target=_stop, name='plantuml-picoweb-stop').start() - + self._logger.info('Starting PlantUML Picoweb server, command=%s', command_bytes) self._process = Popen(command_bytes, stderr=subprocess.PIPE) - queue = Queue() + # Not using a daemon thread for the logging because those can be stopped abruptly and the final log lines might be lost + self._logging_thread = Thread(target=self._process_logging, name='plantuml-picoweb-logging') + self._logging_thread.start() - def process_logging(): - looking_for_port = True - for line in self._process.stderr: - if looking_for_port and line.startswith(b'webPort='): - queue.put(int(line[8:])) - looking_for_port = False - else: - logger.error(str(line, 'utf8').rstrip()) + port = self._wait_for_port(timeout) + self.url = url_template.replace('%port%', str(port)) + self._logger.info('PlantUML PicoWeb server is listening at "%s"', self.url) - # Not a daemon thread because those can be stopped abruptly and the final logging lines might be lost - Thread(target=process_logging, name='plantuml-picoweb-logging').start() + def _process_logging(self): + looking_for_port = True + for line in self._process.stderr: + if looking_for_port and line.startswith(b'webPort='): + self._queue.put(int(line[8:])) + looking_for_port = False + else: + self._logger.error(str(line, 'utf8').rstrip()) + exit_code = self._process.wait() + self._logger.log( + INFO if exit_code in [0, 130, 143] else WARNING, + 'PlantUML PicoWeb server finished (exit code %d)', exit_code + ) + self._queue.put('finished') + + def _wait_for_port(self, timeout: int) -> int: try: - port = queue.get(timeout=start_timeout) + item = self._queue.get(timeout=timeout) + if item == 'finished': + raise Exception('PlantUML PicoWeb server died unexpectedly') + return item except Empty: raise Exception('Timeout waiting for PlantUML PicoWeb server to start') - url = os.environ[PICOWEB_URL_ENV_VAR] = url_template.replace('%port%', str(port)) - logger.info('PlantUML PicoWeb server is listening at "%s"', url) + def stop_after_main_thread(self): + """Ensure the logging thread finishes and we dont leave behind an orphan PicoWeb process""" + + def _stop(): + main_thread().join() + self.stop() + + Thread(target=_stop, name='plantuml-picoweb-stop').start() def stop(self): if self._process: self._process.terminate() - if PICOWEB_URL_ENV_VAR in os.environ: - del os.environ[PICOWEB_URL_ENV_VAR] + if self._logging_thread: + self._logging_thread.join() From 13d8fa7ac02ac9b5eef8f0f2e6aceb605f298936 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 22 Apr 2021 13:31:02 +1000 Subject: [PATCH 10/13] Bump PlantUML version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb677e4d..3bbd2d87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: pip install pybtex - name: Install PlantUML env: - PLANTUML_VERSION: 1.2020.24 + PLANTUML_VERSION: v1.2021.4 run: | sudo apt-get update From a709518c918c1b3529444f81bb0177d9f2ed17a8 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 22 Apr 2021 13:38:53 +1000 Subject: [PATCH 11/13] Use GitHubs setup-java action --- .github/workflows/ci.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bbd2d87..a2791351 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,19 +32,14 @@ jobs: - name: Install test dependencies run: | pip install pybtex + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 8 - name: Install PlantUML env: PLANTUML_VERSION: v1.2021.4 - run: | - sudo apt-get update - - # Not using "apt-get install plantuml" because it's an old version - sudo apt-get install default-jre-headless - - # Avoid Java logging "Created user preferences directory" - mkdir -p ~/.java/.userPrefs - - wget -q -O "${{github.workspace}}/plantuml.jar" "https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION}.jar" + run: wget -q -O "${{github.workspace}}/plantuml.jar" "https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION}.jar" - name: Run tests env: PLANTUML_EXEC: java -Djava.awt.headless=true -jar ${{github.workspace}}/plantuml.jar From e5b0abf57eb8a45b0e8d4340e15d92f900a68c54 Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 22 Apr 2021 13:44:43 +1000 Subject: [PATCH 12/13] Drop PlantUML version because latest is not on repo1.maven.org --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2791351..067ecd0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: java-version: 8 - name: Install PlantUML env: - PLANTUML_VERSION: v1.2021.4 + PLANTUML_VERSION: v1.2021.3 run: wget -q -O "${{github.workspace}}/plantuml.jar" "https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION}.jar" - name: Run tests env: From cb120aa455f693a2b4b24afd4a5e96768f33e93f Mon Sep 17 00:00:00 2001 From: matthew16550 Date: Thu, 22 Apr 2021 13:59:36 +1000 Subject: [PATCH 13/13] oops --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 067ecd0a..6cc75645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: java-version: 8 - name: Install PlantUML env: - PLANTUML_VERSION: v1.2021.3 + PLANTUML_VERSION: 1.2021.4 run: wget -q -O "${{github.workspace}}/plantuml.jar" "https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION}.jar" - name: Run tests env: