From 02a578ac8707fb6bdc59fef61728e89a6ade1e27 Mon Sep 17 00:00:00 2001 From: Daren Thomas Date: Mon, 11 Nov 2019 14:51:15 +0100 Subject: [PATCH 1/5] whitespace pep8 --- cea/interfaces/dashboard/server/jobs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cea/interfaces/dashboard/server/jobs.py b/cea/interfaces/dashboard/server/jobs.py index c3c1ef0c43..9f63bff87b 100644 --- a/cea/interfaces/dashboard/server/jobs.py +++ b/cea/interfaces/dashboard/server/jobs.py @@ -49,6 +49,7 @@ def next_id(): # this is the first job... return 1 + # FIXME: replace with database or similar solution class JobInfo(object): """Store all the information required to run a job""" From 4115193a697548f3841e039c85b04fc43b2e218b Mon Sep 17 00:00:00 2001 From: Daren Thomas Date: Wed, 13 Nov 2019 10:39:19 +0100 Subject: [PATCH 2/5] allow cancelling and killing of jobs --- cea/interfaces/dashboard/server/jobs.py | 18 +++++++++++ cea/interfaces/dashboard/tools/routes.py | 39 ++++++++++++++++-------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/cea/interfaces/dashboard/server/jobs.py b/cea/interfaces/dashboard/server/jobs.py index 9f63bff87b..689951824a 100644 --- a/cea/interfaces/dashboard/server/jobs.py +++ b/cea/interfaces/dashboard/server/jobs.py @@ -6,6 +6,7 @@ from flask_restplus import Namespace, Resource, fields, reqparse from flask import request, current_app +from cea.interfaces.dashboard.tools.routes import kill_job, worker_processes __author__ = "Daren Thomas" @@ -24,6 +25,7 @@ JOB_STATE_STARTED = 1 JOB_STATE_SUCCESS = 2 JOB_STATE_ERROR = 3 +JOB_STATE_CANCELED = 4 job_info_model = api.model('JobInfo', { 'id': fields.Integer, @@ -114,6 +116,8 @@ def post(self, jobid): job = jobs[jobid] job.state = JOB_STATE_SUCCESS job.error = None + if job.id in worker_processes: + del worker_processes[job.id] current_app.socketio.emit("cea-worker-success", api.marshal(job, job_info_model)) return job @@ -125,5 +129,19 @@ def post(self, jobid): job = jobs[jobid] job.state = JOB_STATE_ERROR job.error = request.data + if job.id in worker_processes: + del worker_processes[job.id] current_app.socketio.emit("cea-worker-error", api.marshal(job, job_info_model)) return job + + +@api.route("/cancel/") +class JobCanceled(Resource): + @api.marshal_with(job_info_model) + def post(self, jobid): + job = jobs[jobid] + job.state = JOB_STATE_CANCELED + job.error = "Canceled by user" + kill_job(jobid) + current_app.socketio.emit("cea-worker-canceled", api.marshal(job, job_info_model)) + return job diff --git a/cea/interfaces/dashboard/tools/routes.py b/cea/interfaces/dashboard/tools/routes.py index 72fc6b041e..fa4b0bdd58 100644 --- a/cea/interfaces/dashboard/tools/routes.py +++ b/cea/interfaces/dashboard/tools/routes.py @@ -16,21 +16,34 @@ ) # maintain a list of all subprocess.Popen objects created -worker_processes = [] +worker_processes = {} # jobid -> subprocess.Popen def shutdown_worker_processes(): """When shutting down the flask server, make sure any subprocesses are also terminated. See issue #2408.""" - for popen in worker_processes: - # using code from here: https://stackoverflow.com/a/4229404/2260 - # to terminate child processes too - print("SHUTDOWN: killing child processes of {pid}".format(pid=popen.pid)) + for jobid in worker_processes.keys(): + kill_job(jobid) + + +def kill_job(jobid): + """Kill the processes associated with a jobid""" + if not jobid in worker_processes: + return + + popen = worker_processes[jobid] + # using code from here: https://stackoverflow.com/a/4229404/2260 + # to terminate child processes too + print("killing child processes of {jobid} ({pid})".format(jobid=jobid, pid=popen.pid)) + try: process = psutil.Process(popen.pid) - children = process.children(recursive=True) - for child in children: - print("-- killing child {pid}".format(pid=child.pid)) - child.kill() - process.kill() + except psutil.NoSuchProcess: + return + children = process.children(recursive=True) + for child in children: + print("-- killing child {pid}".format(pid=child.pid)) + child.kill() + process.kill() + del worker_processes[jobid] @blueprint.route("/") @@ -42,17 +55,17 @@ def route_index(): def route_workers(): """Return a list of worker processes""" processes = [] - for worker in worker_processes: + for worker in worker_processes.values(): processes.append(worker.pid) processes.extend(child.pid for child in psutil.Process(worker.pid).children(recursive=True)) return jsonify(sorted(processes)) -@blueprint.route('/start/', methods=['POST']) +@blueprint.route('/start/', methods=['POST']) def route_start(jobid): """Start a ``cea-worker`` subprocess for the script. (FUTURE: add support for cloud-based workers""" print("tools/route_start: {jobid}".format(**locals())) - worker_processes.append(subprocess.Popen(["python", "-m", "cea.worker", jobid])) + worker_processes[jobid] = subprocess.Popen(["python", "-m", "cea.worker", jobid]) return jsonify(jobid) From 226ec6f6f244129bf19168a8e0b87b5167526375 Mon Sep 17 00:00:00 2001 From: Daren Thomas Date: Wed, 13 Nov 2019 10:46:16 +0100 Subject: [PATCH 3/5] arguments to popen need to be strings --- cea/interfaces/dashboard/tools/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cea/interfaces/dashboard/tools/routes.py b/cea/interfaces/dashboard/tools/routes.py index fa4b0bdd58..82ff5971b1 100644 --- a/cea/interfaces/dashboard/tools/routes.py +++ b/cea/interfaces/dashboard/tools/routes.py @@ -65,7 +65,7 @@ def route_workers(): def route_start(jobid): """Start a ``cea-worker`` subprocess for the script. (FUTURE: add support for cloud-based workers""" print("tools/route_start: {jobid}".format(**locals())) - worker_processes[jobid] = subprocess.Popen(["python", "-m", "cea.worker", jobid]) + worker_processes[jobid] = subprocess.Popen(["python", "-m", "cea.worker", "{jobid}".format(jobid=jobid)]) return jsonify(jobid) From e0d31b9523581b29b5936fca8f361664787feea6 Mon Sep 17 00:00:00 2001 From: Daren Thomas Date: Wed, 13 Nov 2019 11:43:42 +0100 Subject: [PATCH 4/5] bumping version to 2.25.2 --- cea/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cea/__init__.py b/cea/__init__.py index 6e51c7c5dc..b836477667 100644 --- a/cea/__init__.py +++ b/cea/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.25.1" +__version__ = "2.25.2" class ConfigError(Exception): From caad852616a31840f167684f9863432d548afeb4 Mon Sep 17 00:00:00 2001 From: Daren Thomas Date: Wed, 13 Nov 2019 13:48:39 +0100 Subject: [PATCH 5/5] abort if pip install cityenergyanalyst fails... --- setup/cityenergyanalyst.nsi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/setup/cityenergyanalyst.nsi b/setup/cityenergyanalyst.nsi index f394caaa40..b94e8425ea 100644 --- a/setup/cityenergyanalyst.nsi +++ b/setup/cityenergyanalyst.nsi @@ -1,5 +1,7 @@ # NSIS script for creating the City Energy Analyst installer +; include logic library +!include 'LogicLib.nsh' ; include the modern UI stuff !include "MUI2.nsh" @@ -175,6 +177,15 @@ Section "Base Installation" Base_Installation_Section nsExec::ExecToLog '"$INSTDIR\Dependencies\Python\python.exe" -m pip install -U --force-reinstall pip' DetailPrint "Pip installing CityEnergyAnalyst==${VER}" nsExec::ExecToLog '"$INSTDIR\Dependencies\Python\python.exe" -m pip install -U cityenergyanalyst==${VER}' + + # make sure cea was installed + Pop $0 + DetailPrint 'pip install cityenergyanalyst==${VER} returned $0' + ${If} "$0" != "0" + Abort "Could not install CityEnergyAnalyst ${VER} - see Details" + ${EndIf} + + DetailPrint "Pip installing Jupyter" nsExec::ExecToLog '"$INSTDIR\Dependencies\Python\python.exe" -m pip install --force-reinstall jupyter ipython'