From afcba443a86c1b47f8b9fe8c9feea829027078f8 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Mar 2022 17:36:49 +0000 Subject: [PATCH 1/4] Added fortls autoupdate during initialisation The server will ping PyPi and check if a version is available greater than the currently installed version. If an old version is detected fortls will download the new version to the pip localtion of the current Python distribution. This should allow for fortls to be correctly installed most times. Obviously if running from a directory structure that does not adhere to how pip/conda install packages this might not work. --- fortls/interface.py | 8 +++++ fortls/langserver.py | 58 ++++++++++++++++++++++++++++++++ test/test_interface.py | 20 ++++++++++- test/test_source/f90_config.json | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/fortls/interface.py b/fortls/interface.py index 3fdfff6a..cc3ba31f 100644 --- a/fortls/interface.py +++ b/fortls/interface.py @@ -76,6 +76,14 @@ def commandline_args(name: str = "fortls") -> argparse.ArgumentParser: " as is)" ), ) + parser.add_argument( + "--disable_autoupdate", + action="store_true", + help=( + "fortls automatically checks PyPi for newer version and installs them." + "Use this option to disable the autoupdate feature." + ), + ) # XXX: Deprecated, argument not attached to anything. Remove parser.add_argument( "--preserve_keyword_order", diff --git a/fortls/langserver.py b/fortls/langserver.py index 02661d0d..0f25ce56 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -5,10 +5,16 @@ import logging import os import re +import subprocess +import sys import traceback +import urllib.request from multiprocessing import Pool from pathlib import Path from typing import Pattern +from urllib.error import URLError + +from packaging import version # Local modules from fortls.constants import ( @@ -208,6 +214,8 @@ def serve_initialize(self, request): self._config_logger(request) self._load_intrinsics() self._add_source_dirs() + if self._update_version_pypi(): + log.log("Please restart the server for new version to activate") # Initialize workspace self.workspace_init() @@ -1486,6 +1494,9 @@ def _load_config_file_general(self, config_dict: dict) -> None: ) self.sync_type: int = 2 if self.incremental_sync else 1 self.sort_keywords = config_dict.get("sort_keywords", self.sort_keywords) + self.disable_autoupdate = config_dict.get( + "disable_autoupdate", self.disable_autoupdate + ) # Autocomplete options ------------------------------------------------- self.autocomplete_no_prefix = config_dict.get( @@ -1634,6 +1645,53 @@ def _create_ref_link(self, obj): }, } + def _update_version_pypi(self, test: bool = False): + """Fetch updates from PyPi for fortls + + Parameters + ---------- + test : bool, optional + flag used to override exit checks, only for unittesting, by default False + """ + if self.disable_autoupdate: + return False + v = version.parse(__version__) + # Do not run for prerelease and dev release + if v.is_prerelease and not test: + return False + try: + with urllib.request.urlopen("https://pypi.org/pypi/fortls/json") as resp: + info = json.loads(resp.read().decode("utf-8")) + # This is the only reliable way to compare version semantics + if version.parse(info["info"]["version"]) > v or test: + self.post_message( + f"Using fortls {__version__}. A newer version of is" + " available through PyPi. An attempt will be made to update" + " the server", + 3, + ) + # Run pip + result = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "fortls", + "--upgrade", + ], + capture_output=True, + ) + if result.stdout: + log.info(result.stdout) + if result.stderr: + log.error(result.stderr) + return True + # No internet connection exceptions + except (URLError, KeyError): + log.warning("Failed to update the fortls Language Server") + return False + class JSONRPC2Error(Exception): def __init__(self, code, message, data=None): diff --git a/test/test_interface.py b/test/test_interface.py index 34c8ddb8..501888a6 100644 --- a/test/test_interface.py +++ b/test/test_interface.py @@ -12,13 +12,14 @@ def test_command_line_general_options(): args = parser.parse_args( "-c config_file.json -n 2 --notify_init --incremental_sync --sort_keywords" - " --debug_log".split() + " --disable_autoupdate --debug_log".split() ) assert args.config == "config_file.json" assert args.nthreads == 2 assert args.notify_init assert args.incremental_sync assert args.sort_keywords + assert args.disable_autoupdate assert args.debug_log @@ -103,6 +104,7 @@ def test_config_file_general_options(): assert server.notify_init assert server.incremental_sync assert server.sort_keywords + assert server.disable_autoupdate def test_config_file_dir_parsing_options(): @@ -164,3 +166,19 @@ def test_config_file_codeactions_options(): server, root = unittest_server_init() # Code Actions options assert server.enable_code_actions + + +def test_version_update_pypi(): + from fortls.langserver import LangServer + from fortls.jsonrpc import JSONRPC2Connection, ReadWriter + + parser = commandline_args("fortls") + args = parser.parse_args("-c f90_config.json".split()) + args = vars(args) + args["disable_autoupdate"] = False + + stdin, stdout = sys.stdin.buffer, sys.stdout.buffer + s = LangServer(conn=JSONRPC2Connection(ReadWriter(stdin, stdout)), settings=args) + s.root_path = (Path(__file__).parent / "test_source").resolve() + did_update = s._update_version_pypi(test=True) + assert did_update diff --git a/test/test_source/f90_config.json b/test/test_source/f90_config.json index 59a1a551..2d4779b7 100644 --- a/test/test_source/f90_config.json +++ b/test/test_source/f90_config.json @@ -3,6 +3,7 @@ "notify_init": true, "incremental_sync": true, "sort_keywords": true, + "disable_autoupdate": true, "source_dirs": ["subdir", "pp/**"], "incl_suffixes": [".FF", ".fpc", ".h", "f20"], From 849022b0e2cf6e6e90402522009615eed8a77e3a Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Mar 2022 17:43:09 +0000 Subject: [PATCH 2/4] Updated imports with isort --- fortls/objects.py | 2 +- fortls/parse_fortran.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fortls/objects.py b/fortls/objects.py index ac96cbba..bb89e46b 100644 --- a/fortls/objects.py +++ b/fortls/objects.py @@ -25,8 +25,8 @@ VAR_TYPE_ID, WHERE_TYPE_ID, FRegex, - USE_info, INCLUDE_info, + USE_info, ) from fortls.helper_functions import get_keywords, get_paren_substring, get_var_stack from fortls.jsonrpc import path_to_uri diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 7d0eccbe..2a6d434b 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -18,12 +18,12 @@ PY3K, SELECT_TYPE_ID, SUBMODULE_TYPE_ID, + CLASS_info, FRegex, FUN_sig, - RESULT_sig, - CLASS_info, GEN_info, INT_info, + RESULT_sig, SELECT_info, SMOD_info, SUB_info, From d6a4f89b853ff46c89c4950fe22ff01a4a0acf9d Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Mar 2022 17:45:35 +0000 Subject: [PATCH 3/4] Updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf57f07..4c7aafdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELONG +## 2.2.6 + +### Added + +- Added the capability for `fortls` to auto-update use `--disable_autoupdate` to disable + ([#76](https://github.com/gnikit/fortls/issues/76)) + ## 2.2.5 ### Changed From 40fa5496cb7d0352583c0c1cac5103b373a87706 Mon Sep 17 00:00:00 2001 From: gnikit Date: Tue, 8 Mar 2022 17:50:19 +0000 Subject: [PATCH 4/4] Secirity fix --- fortls/langserver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fortls/langserver.py b/fortls/langserver.py index 0f25ce56..bb744f1a 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -1660,7 +1660,9 @@ def _update_version_pypi(self, test: bool = False): if v.is_prerelease and not test: return False try: - with urllib.request.urlopen("https://pypi.org/pypi/fortls/json") as resp: + # For security reasons register as Request before opening + request = urllib.request.Request("https://pypi.org/pypi/fortls/json") + with urllib.request.urlopen(request) as resp: info = json.loads(resp.read().decode("utf-8")) # This is the only reliable way to compare version semantics if version.parse(info["info"]["version"]) > v or test: