From 51c912f3a896272941490a0664d334f493c2bd15 Mon Sep 17 00:00:00 2001
From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr>
Date: Fri, 27 Oct 2023 09:33:32 +0200
Subject: [PATCH] Solver: Code refactoring.

---
 src/Solver/ASolver.py       | 249 +--------------------------
 src/Solver/CommandLine.py   | 327 ++++++++++++++++++++++++++++++++++++
 src/Solver/GenericSolver.py |   7 +-
 src/Solver/Mage.py          |   4 +-
 4 files changed, 337 insertions(+), 250 deletions(-)
 create mode 100644 src/Solver/CommandLine.py

diff --git a/src/Solver/ASolver.py b/src/Solver/ASolver.py
index b461d602..c4fa286a 100644
--- a/src/Solver/ASolver.py
+++ b/src/Solver/ASolver.py
@@ -59,17 +59,6 @@ class AbstractSolver(object):
         self._name = name
         self._description = ""
 
-        self._path_input = ""
-        self._path_solver = ""
-        self._path_output = ""
-
-        self._cmd_input = ""
-        self._cmd_solver = ""
-        self._cmd_output = ""
-
-        self._process = None
-        self._output = None
-
         # Last study running
         self._study = None
 
@@ -92,7 +81,6 @@ class AbstractSolver(object):
             ("all_init_time", "000:00:00:00"),
             ("all_final_time", "999:99:00:00"),
             ("all_timestep", "300.0"),
-            ("all_command_line_arguments", ""),
         ]
 
         return lst
@@ -141,18 +129,6 @@ class AbstractSolver(object):
     def description(self, description):
         self._description = description
 
-    def set_input(self, path, cmd):
-        self._path_input = path
-        self._cmd_input = cmd
-
-    def set_solver(self, path, cmd):
-        self._path_solver = path
-        self._cmd_solver = cmd
-
-    def set_output(self, path, cmd):
-        self._path_output = path
-        self._cmd_output = cmd
-
     ##########
     # Export #
     ##########
@@ -160,41 +136,6 @@ class AbstractSolver(object):
     def export(self, study, repertory, qlog=None):
         raise NotImplementedMethodeError(self, self.export)
 
-    def cmd_args(self, study):
-        """Return solver command line arguments list
-
-        Returns:
-            Command line arguments list
-        """
-        params = study.river.get_params(self.type)
-        args = params.get_by_key("all_command_line_arguments")
-
-        return args.split(" ")
-
-    def input_param(self):
-        """Return input command line parameter(s)
-
-        Returns:
-            Returns input parameter(s) string
-        """
-        raise NotImplementedMethodeError(self, self.input_param)
-
-    def output_param(self):
-        """Return output command line parameter(s)
-
-        Returns:
-            Returns output parameter(s) string
-        """
-        raise NotImplementedMethodeError(self, self.output_param)
-
-    def log_file(self):
-        """Return log file name
-
-        Returns:
-            Returns log file name as string
-        """
-        raise NotImplementedMethodeError(self, self.log_file)
-
     ###########
     # RESULTS #
     ###########
@@ -208,195 +149,17 @@ class AbstractSolver(object):
     # Run #
     #######
 
-    def _install_dir(self):
-        return os.path.abspath(
-            os.path.join(
-                os.path.dirname(__file__),
-                "..", ".."
-            )
-        )
-
-    def _format_command(self, study, cmd, path=""):
-        """Format command line
-
-        Args:
-            cmd: The command line
-            path: Optional path string (replace @path in cmd)
-
-        Returns:
-            The executable and list of arguments
-        """
-        # HACK: Works in most case... Trust me i'm an engineer
-        cmd = cmd.replace("@install_dir", self._install_dir())
-        cmd = cmd.replace("@path", path.replace(" ", "%20"))
-        cmd = cmd.replace("@input", self.input_param())
-        cmd = cmd.replace("@output", self.output_param())
-        cmd = cmd.replace("@dir", self._process.workingDirectory())
-        cmd = cmd.replace("@args", " ".join(self.cmd_args(study)))
-
-        logger.debug(f"! {cmd}")
-
-        if cmd[0] == "\"":
-            # Command line executable path is between " char
-            cmd = cmd.split("\"")
-            exe = cmd[1].replace("%20", " ")
-            args = list(
-                filter(
-                    lambda s: s != "",
-                    "\"".join(cmd[2:]).split(" ")[1:]
-                )
-            )
-        else:
-            # We suppose the command line executable path as no space char
-            cmd = cmd.replace("\\ ", "&_&").split(" ")
-            exe = cmd[0].replace("&_&", " ")
-            args = list(
-                filter(
-                    lambda s: s != "",
-                    map(lambda s: s.replace("&_&", " "), cmd[1:])
-                )
-            )
-
-        logger.info(f"! {exe} {args}")
-        return exe, args
-
-    def run_input_data_fomater(self, study):
-        if self._cmd_input == "":
-            self._run_next(study)
-            return True
-
-        cmd = self._cmd_input
-        exe, args = self._format_command(study, cmd, self._path_input)
-
-        if not os.path.exists(exe):
-            error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
-            return error
-
-        self._process.start(
-            exe, args,
-        )
-
-        return True
-
-    def run_solver(self, study):
-        if self._cmd_solver == "":
-            self._run_next(study)
-            return True
-
-        cmd = self._cmd_solver
-        exe, args = self._format_command(study, cmd, self._path_solver)
-
-        if not os.path.exists(exe):
-            error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
-            return error
-
-        self._process.start(
-            exe, args,
-        )
-        self._process.waitForStarted()
-
-        self._status = STATUS.RUNNING
-        return True
-
-    def run_output_data_fomater(self, study):
-        if self._cmd_output == "":
-            self._run_next(study)
-            return True
-
-        cmd = self._cmd_output
-        exe, args = self._format_command(study, cmd, self._path_output)
-
-        if not os.path.exists(exe):
-            error = f"[ERROR] Path {exe} do not exists"
-            logger.info(error)
-            return error
-
-        self._process.start(
-            exe, args,
-        )
-
-        return True
-
-    def _data_ready(self):
-        s = self._process.readAll().data().decode()
-        if self._output is not None:
-            for x in s.split('\n'):
-                self._output.put(x)
-
-    def _run_next(self, study):
-        self._step += 1
-        if self._step < len(self._runs):
-            res = self._runs[self._step](study)
-            if res is not True:
-                self._output.put(res)
-        else:
-            self._status = STATUS.STOPED
-
-    def _finished(self, study, exit_code, exit_status):
-        if self._output is not None:
-            self._output.put(exit_code)
-
-        self._run_next(study)
-
-    def run(self, study, process=None, output_queue=None):
-        self._study = study
-
-        if process is not None:
-            self._process = process
-        if output_queue is not None:
-            self._output = output_queue
-
-        self._process.readyRead.connect(self._data_ready)
-        self._process.finished.connect(
-            lambda c, s: self._finished(study, c, s))
-
-        self._runs = [
-            self.run_input_data_fomater,
-            self.run_solver,
-            self.run_output_data_fomater,
-        ]
-        self._step = 0
-        # Run first step
-        res = self._runs[0](study)
-        if res is not True:
-            self._output.put(res)
+    def run(self, study):
+        raise NotImplementedMethodeError(self, self.run)
 
     def kill(self):
-        if self._process is None:
-            return True
-
-        self._process.kill()
-        self._status = STATUS.STOPED
-        return True
+        raise NotImplementedMethodeError(self, self.kill)
 
     def start(self, study, process=None):
-        if _signal:
-            # Solver is PAUSED, so continue execution
-            if self._status == STATUS.PAUSED:
-                os.kill(self._process.pid(), SIGCONT)
-                self._status = STATUS.RUNNING
-                return True
-
-        self.run(study, process)
-        return True
+        raise NotImplementedMethodeError(self, self.start)
 
     def pause(self):
-        if _signal:
-            if self._process is None:
-                return False
-
-            # Send SIGSTOP to PAUSED solver
-            os.kill(self._process.pid(), SIGSTOP)
-            self._status = STATUS.PAUSED
-            return True
-        return False
+        raise NotImplementedMethodeError(self, self.pause)
 
     def stop(self):
-        if self._process is None:
-            return False
-
-        self._process.terminate()
-        self._status = STATUS.STOPED
-        return True
+        raise NotImplementedMethodeError(self, self.stop)
diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py
new file mode 100644
index 00000000..c922b86a
--- /dev/null
+++ b/src/Solver/CommandLine.py
@@ -0,0 +1,327 @@
+# CommandLine.py -- Pamhyr
+# Copyright (C) 2023  INRAE
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+
+from tools import timer
+
+try:
+    # Installation allow Unix-like signal
+    from signal import SIGTERM, SIGSTOP, SIGCONT
+    _signal = True
+except Exception:
+    _signal = False
+
+from enum import Enum
+
+from Model.Except import NotImplementedMethodeError
+
+from Model.Results.Results import Results
+from Model.Results.River.River import River, Reach, Profile
+
+from Solver.ASolver import AbstractSolver, STATUS
+
+logger = logging.getLogger()
+
+
+class CommandLineSolver(AbstractSolver):
+    _type = ""
+
+    def __init__(self, name):
+        super(CommandLineSolver, self).__init__(name)
+
+        self._current_process = None
+        self._status = STATUS.NOT_LAUNCHED
+
+        self._path_input = ""
+        self._path_solver = ""
+        self._path_output = ""
+
+        self._cmd_input = ""
+        self._cmd_solver = ""
+        self._cmd_output = ""
+
+        self._process = None
+        self._output = None
+
+        # Last study running
+        self._study = None
+
+    @classmethod
+    def default_parameters(cls):
+        lst = super(CommandLineSolver, cls).default_parameters()
+
+        lst += [
+            ("all_command_line_arguments", ""),
+        ]
+
+        return lst
+
+    def set_input(self, path, cmd):
+        self._path_input = path
+        self._cmd_input = cmd
+
+    def set_solver(self, path, cmd):
+        self._path_solver = path
+        self._cmd_solver = cmd
+
+    def set_output(self, path, cmd):
+        self._path_output = path
+        self._cmd_output = cmd
+
+    ##########
+    # Export #
+    ##########
+
+    def cmd_args(self, study):
+        """Return solver command line arguments list
+
+        Returns:
+            Command line arguments list
+        """
+        params = study.river.get_params(self.type)
+        args = params.get_by_key("all_command_line_arguments")
+
+        return args.split(" ")
+
+    def input_param(self):
+        """Return input command line parameter(s)
+
+        Returns:
+            Returns input parameter(s) string
+        """
+        raise NotImplementedMethodeError(self, self.input_param)
+
+    def output_param(self):
+        """Return output command line parameter(s)
+
+        Returns:
+            Returns output parameter(s) string
+        """
+        raise NotImplementedMethodeError(self, self.output_param)
+
+    def log_file(self):
+        """Return log file name
+
+        Returns:
+            Returns log file name as string
+        """
+        raise NotImplementedMethodeError(self, self.log_file)
+
+    #######
+    # Run #
+    #######
+
+    def _install_dir(self):
+        return os.path.abspath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "..", ".."
+            )
+        )
+
+    def _format_command(self, study, cmd, path=""):
+        """Format command line
+
+        Args:
+            cmd: The command line
+            path: Optional path string (replace @path in cmd)
+
+        Returns:
+            The executable and list of arguments
+        """
+        # HACK: Works in most case... Trust me i'm an engineer
+        cmd = cmd.replace("@install_dir", self._install_dir())
+        cmd = cmd.replace("@path", path.replace(" ", "%20"))
+        cmd = cmd.replace("@input", self.input_param())
+        cmd = cmd.replace("@output", self.output_param())
+        cmd = cmd.replace("@dir", self._process.workingDirectory())
+        cmd = cmd.replace("@args", " ".join(self.cmd_args(study)))
+
+        logger.debug(f"! {cmd}")
+
+        if cmd[0] == "\"":
+            # Command line executable path is between " char
+            cmd = cmd.split("\"")
+            exe = cmd[1].replace("%20", " ")
+            args = list(
+                filter(
+                    lambda s: s != "",
+                    "\"".join(cmd[2:]).split(" ")[1:]
+                )
+            )
+        else:
+            # We suppose the command line executable path as no space char
+            cmd = cmd.replace("\\ ", "&_&").split(" ")
+            exe = cmd[0].replace("&_&", " ")
+            args = list(
+                filter(
+                    lambda s: s != "",
+                    map(lambda s: s.replace("&_&", " "), cmd[1:])
+                )
+            )
+
+        logger.info(f"! {exe} {args}")
+        return exe, args
+
+    def run_input_data_fomater(self, study):
+        if self._cmd_input == "":
+            self._run_next(study)
+            return True
+
+        cmd = self._cmd_input
+        exe, args = self._format_command(study, cmd, self._path_input)
+
+        if not os.path.exists(exe):
+            error = f"[ERROR] Path {exe} do not exists"
+            logger.info(error)
+            return error
+
+        self._process.start(
+            exe, args,
+        )
+
+        return True
+
+    def run_solver(self, study):
+        if self._cmd_solver == "":
+            self._run_next(study)
+            return True
+
+        cmd = self._cmd_solver
+        exe, args = self._format_command(study, cmd, self._path_solver)
+
+        if not os.path.exists(exe):
+            error = f"[ERROR] Path {exe} do not exists"
+            logger.info(error)
+            return error
+
+        self._process.start(
+            exe, args,
+        )
+        self._process.waitForStarted()
+
+        self._status = STATUS.RUNNING
+        return True
+
+    def run_output_data_fomater(self, study):
+        if self._cmd_output == "":
+            self._run_next(study)
+            return True
+
+        cmd = self._cmd_output
+        exe, args = self._format_command(study, cmd, self._path_output)
+
+        if not os.path.exists(exe):
+            error = f"[ERROR] Path {exe} do not exists"
+            logger.info(error)
+            return error
+
+        self._process.start(
+            exe, args,
+        )
+
+        return True
+
+    def _data_ready(self):
+        # Read process output and put lines in queue
+        s = self._process.readAll().data().decode()
+        if self._output is not None:
+            for x in s.split('\n'):
+                self._output.put(x)
+
+    def _run_next(self, study):
+        self._step += 1
+        if self._step < len(self._runs):
+            res = self._runs[self._step](study)
+            if res is not True:
+                self._output.put(res)
+        else:
+            self._status = STATUS.STOPED
+
+    def _finished(self, study, exit_code, exit_status):
+        if self._output is not None:
+            self._output.put(exit_code)
+
+        self._run_next(study)
+
+    def run(self, study, process=None, output_queue=None):
+        self._study = study
+
+        # Replace old values if needed
+        if process is not None:
+            self._process = process
+        if output_queue is not None:
+            self._output = output_queue
+
+        # Connect / reconnect signal
+        self._process.readyRead.connect(self._data_ready)
+        self._process.finished.connect(
+            lambda c, s: self._finished(study, c, s))
+
+        # Prepare running step
+        self._runs = [
+            self.run_input_data_fomater,
+            self.run_solver,
+            self.run_output_data_fomater,
+        ]
+        self._step = 0
+
+        # Run first step
+        res = self._runs[0](study)
+        if res is not True:
+            self._output.put(res)
+
+    def kill(self):
+        if self._process is None:
+            return True
+
+        self._process.kill()
+        self._status = STATUS.STOPED
+        return True
+
+    def start(self, study, process=None):
+        if _signal:
+            # Solver is PAUSED, so continue execution
+            if self._status == STATUS.PAUSED:
+                os.kill(self._process.pid(), SIGCONT)
+                self._status = STATUS.RUNNING
+                return True
+
+        self.run(study, process)
+        return True
+
+    def pause(self):
+        if _signal:
+            if self._process is None:
+                return False
+
+            # Send SIGSTOP to PAUSED solver
+            os.kill(self._process.pid(), SIGSTOP)
+            self._status = STATUS.PAUSED
+            return True
+        return False
+
+    def stop(self):
+        if self._process is None:
+            return False
+
+        self._process.terminate()
+        self._status = STATUS.STOPED
+        return True
diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py
index bc5a9f6b..9fb272e4 100644
--- a/src/Solver/GenericSolver.py
+++ b/src/Solver/GenericSolver.py
@@ -16,12 +16,9 @@
 
 # -*- coding: utf-8 -*-
 
-from Solver.ASolver import (
-    AbstractSolver, STATUS
-)
+from Solver.CommandLine import CommandLineSolver
 
-
-class GenericSolver(AbstractSolver):
+class GenericSolver(CommandLineSolver):
     _type = "generic"
 
     def __init__(self, name):
diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py
index baf23a9b..52efc071 100644
--- a/src/Solver/Mage.py
+++ b/src/Solver/Mage.py
@@ -22,7 +22,7 @@ import numpy as np
 
 from tools import timer
 
-from Solver.ASolver import AbstractSolver
+from Solver.CommandLine import CommandLineSolver
 from Checker.Mage import MageNetworkGraphChecker
 
 from Model.Results.Results import Results
@@ -41,7 +41,7 @@ def mage_file_open(filepath, mode):
     return f
 
 
-class Mage(AbstractSolver):
+class Mage(CommandLineSolver):
     _type = "mage"
 
     def __init__(self, name):
-- 
GitLab