Compare commits
8 Commits
nilmrun-1.
...
nilmrun-1.
Author | SHA1 | Date | |
---|---|---|---|
9309fd9b57 | |||
21bd1bd050 | |||
cafdfce4f0 | |||
477c27a4e6 | |||
bed26e099b | |||
9224566f9b | |||
a8ecad9329 | |||
5b878378f3 |
@@ -7,7 +7,7 @@ Prerequisites:
|
|||||||
sudo apt-get install python2.7 python-setuptools
|
sudo apt-get install python2.7 python-setuptools
|
||||||
|
|
||||||
# Plus nilmdb and its dependencies
|
# Plus nilmdb and its dependencies
|
||||||
nilmdb (1.8.2+)
|
nilmdb (1.9.5+)
|
||||||
|
|
||||||
Install:
|
Install:
|
||||||
|
|
||||||
|
@@ -92,6 +92,12 @@ class Process(object):
|
|||||||
except OSError: # pragma: no cover
|
except OSError: # pragma: no cover
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def kill(pid, sig):
|
||||||
|
try:
|
||||||
|
return os.kill(pid, sig)
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
return
|
||||||
|
|
||||||
# Find all children
|
# Find all children
|
||||||
group = getpgid(self._process.pid)
|
group = getpgid(self._process.pid)
|
||||||
main = psutil.Process(self._process.pid)
|
main = psutil.Process(self._process.pid)
|
||||||
@@ -100,7 +106,7 @@ class Process(object):
|
|||||||
# Kill with SIGTERM, if they're still in this process group
|
# Kill with SIGTERM, if they're still in this process group
|
||||||
for proc in allproc:
|
for proc in allproc:
|
||||||
if getpgid(proc.pid) == group:
|
if getpgid(proc.pid) == group:
|
||||||
os.kill(proc.pid, signal.SIGTERM)
|
kill(proc.pid, signal.SIGTERM)
|
||||||
|
|
||||||
# Wait for it to die again
|
# Wait for it to die again
|
||||||
if self._join(timeout):
|
if self._join(timeout):
|
||||||
@@ -109,7 +115,7 @@ class Process(object):
|
|||||||
# One more try with SIGKILL
|
# One more try with SIGKILL
|
||||||
for proc in allproc:
|
for proc in allproc:
|
||||||
if getpgid(proc.pid) == group:
|
if getpgid(proc.pid) == group:
|
||||||
os.kill(proc.pid, signal.SIGKILL)
|
kill(proc.pid, signal.SIGKILL)
|
||||||
|
|
||||||
# See if it worked
|
# See if it worked
|
||||||
return self._join(timeout)
|
return self._join(timeout)
|
||||||
@@ -178,9 +184,27 @@ class ProcessManager(object):
|
|||||||
"""Track and manage a collection of Process objects"""
|
"""Track and manage a collection of Process objects"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.processes = {}
|
self.processes = {}
|
||||||
self.tmpfiles = {}
|
self.tmpdirs = {}
|
||||||
self.tmpdir = tempfile.mkdtemp(prefix = "nilmrun-usercode-")
|
atexit.register(self._atexit)
|
||||||
atexit.register(shutil.rmtree, self.tmpdir)
|
|
||||||
|
def _cleanup_tmpdir(self, pid):
|
||||||
|
if pid in self.tmpdirs:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.tmpdirs[pid])
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
del self.tmpdirs[pid]
|
||||||
|
|
||||||
|
def _atexit(self):
|
||||||
|
# Kill remaining processes, remove their dirs
|
||||||
|
for pid in self.processes.keys():
|
||||||
|
try:
|
||||||
|
self.processes[pid].terminate()
|
||||||
|
del self.processes[pid]
|
||||||
|
shutil.rmtree(self.tmpdirs[pid])
|
||||||
|
del self.tmpdirs[pid]
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.processes.keys())
|
return iter(self.processes.keys())
|
||||||
@@ -193,15 +217,33 @@ class ProcessManager(object):
|
|||||||
executed. The arguments, which must be strings, will be
|
executed. The arguments, which must be strings, will be
|
||||||
accessible in the code as sys.argv[1:]."""
|
accessible in the code as sys.argv[1:]."""
|
||||||
# The easiest way to do this, by far, is to just write the
|
# The easiest way to do this, by far, is to just write the
|
||||||
# code to a file.
|
# code to a file. Make a directory to put it in.
|
||||||
(fd, path) = tempfile.mkstemp(prefix = "nilmrun-usercode-",
|
tmpdir = tempfile.mkdtemp(prefix = "nilmrun-usercode-")
|
||||||
suffix = ".py", dir=self.tmpdir)
|
try:
|
||||||
with os.fdopen(fd, 'w') as f:
|
# Write the code
|
||||||
f.write(code)
|
codepath = os.path.join(tmpdir, "usercode.py")
|
||||||
argv = [ sys.executable, "-B", "-s", "-u", path ] + args
|
with open(codepath, "w") as f:
|
||||||
pid = self.run_command(argv)
|
f.write(code)
|
||||||
self.tmpfiles[pid] = path
|
# Save the args too, for debugging purposes
|
||||||
return pid
|
with open(os.path.join(tmpdir, "args.txt"), "w") as f:
|
||||||
|
f.write(repr(args))
|
||||||
|
|
||||||
|
# Run the code
|
||||||
|
argv = [ sys.executable, "-B", "-s", "-u", codepath ] + args
|
||||||
|
pid = self.run_command(argv)
|
||||||
|
|
||||||
|
# Save the temp dir
|
||||||
|
self.tmpdirs[pid] = tmpdir
|
||||||
|
tmpdir = None # Don't need to remove it anymore
|
||||||
|
|
||||||
|
return pid
|
||||||
|
finally:
|
||||||
|
# Clean up tempdir if we didn't finish
|
||||||
|
if tmpdir is not None:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
|
||||||
def run_command(self, argv):
|
def run_command(self, argv):
|
||||||
"""Execute a command line program"""
|
"""Execute a command line program"""
|
||||||
@@ -213,12 +255,7 @@ class ProcessManager(object):
|
|||||||
return self.processes[pid].terminate()
|
return self.processes[pid].terminate()
|
||||||
|
|
||||||
def remove(self, pid):
|
def remove(self, pid):
|
||||||
if pid in self.tmpfiles:
|
self._cleanup_tmpdir(pid)
|
||||||
try:
|
|
||||||
os.unlink(self.tmpfiles[pid])
|
|
||||||
except OSError: # pragma: no cover
|
|
||||||
pass
|
|
||||||
del self.tmpfiles[pid]
|
|
||||||
del self.processes[pid]
|
del self.processes[pid]
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
|
@@ -22,6 +22,7 @@ from nilmdb.server.serverutil import (
|
|||||||
cherrypy_stop,
|
cherrypy_stop,
|
||||||
bool_param,
|
bool_param,
|
||||||
)
|
)
|
||||||
|
from nilmdb.utils import serializer_proxy
|
||||||
import nilmrun
|
import nilmrun
|
||||||
import nilmrun.testfilter
|
import nilmrun.testfilter
|
||||||
|
|
||||||
@@ -60,12 +61,16 @@ class AppProcess(object):
|
|||||||
self.manager = manager
|
self.manager = manager
|
||||||
|
|
||||||
def process_status(self, pid):
|
def process_status(self, pid):
|
||||||
|
# We need to convert the log (which is bytes) to Unicode
|
||||||
|
# characters, in order to send it via JSON. Treat it as UTF-8
|
||||||
|
# but replace invalid characters with markers.
|
||||||
|
log = self.manager[pid].log.decode('utf-8', errors='replace')
|
||||||
return {
|
return {
|
||||||
"pid": pid,
|
"pid": pid,
|
||||||
"alive": self.manager[pid].alive,
|
"alive": self.manager[pid].alive,
|
||||||
"exitcode": self.manager[pid].exitcode,
|
"exitcode": self.manager[pid].exitcode,
|
||||||
"start_time": self.manager[pid].start_time,
|
"start_time": self.manager[pid].start_time,
|
||||||
"log": self.manager[pid].log,
|
"log": log
|
||||||
}
|
}
|
||||||
|
|
||||||
# /process/status
|
# /process/status
|
||||||
@@ -200,8 +205,12 @@ class Server(object):
|
|||||||
# error messages.
|
# error messages.
|
||||||
cherrypy._cperror._ie_friendly_error_sizes = {}
|
cherrypy._cperror._ie_friendly_error_sizes = {}
|
||||||
|
|
||||||
|
# The manager maintains internal state and isn't necessarily
|
||||||
|
# thread-safe, so wrap it in the serializer.
|
||||||
|
manager = serializer_proxy(nilmrun.processmanager.ProcessManager)()
|
||||||
|
|
||||||
# Build up the application and mount it
|
# Build up the application and mount it
|
||||||
manager = nilmrun.processmanager.ProcessManager()
|
self._manager = manager
|
||||||
root = App()
|
root = App()
|
||||||
root.process = AppProcess(manager)
|
root.process = AppProcess(manager)
|
||||||
root.run = AppRun(manager)
|
root.run = AppRun(manager)
|
||||||
|
50
scripts/kill.py
Executable file
50
scripts/kill.py
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from nilmdb.client.httpclient import HTTPClient, ClientError, ServerError
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
import nilmrun
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Kill/remove a process from the NilmRun server"""
|
||||||
|
def_url = os.environ.get("NILMRUN_URL", "http://localhost/nilmrun/")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description = 'Kill/remove a process from the NilmRun server',
|
||||||
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
version = nilmrun.__version__)
|
||||||
|
group = parser.add_argument_group("Standard options")
|
||||||
|
group.add_argument('-u', '--url',
|
||||||
|
help = 'NilmRun server URL', default = def_url)
|
||||||
|
group.add_argument('-n', '--noverify', action="store_true",
|
||||||
|
help = 'Disable SSL certificate verification')
|
||||||
|
group = parser.add_argument_group("Program")
|
||||||
|
group.add_argument('-q', '--quiet', action="store_true",
|
||||||
|
help = "Don't print out the final log contents")
|
||||||
|
group.add_argument('pid', nargs='+', help="PIDs to kill")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = HTTPClient(baseurl = args.url, verify_ssl = not args.noverify)
|
||||||
|
|
||||||
|
# Kill or remove process
|
||||||
|
all_failed = True
|
||||||
|
for pid in args.pid:
|
||||||
|
try:
|
||||||
|
s = client.post("process/remove", { "pid": pid })
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stdout.write(s['log'])
|
||||||
|
all_failed = False
|
||||||
|
except ClientError as e:
|
||||||
|
if "404" in e.status:
|
||||||
|
fprintf(sys.stderr, "no such pid: %s\n", pid)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Return error if we failed to remove any of them
|
||||||
|
if all_failed:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@@ -10,10 +10,8 @@ def main():
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description = 'Run the NilmRun server',
|
description = 'Run the NilmRun server',
|
||||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter)
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
version = nilmrun.__version__)
|
||||||
parser.add_argument("-V", "--version", action="version",
|
|
||||||
version = nilmrun.__version__)
|
|
||||||
|
|
||||||
group = parser.add_argument_group("Standard options")
|
group = parser.add_argument_group("Standard options")
|
||||||
group.add_argument('-a', '--address',
|
group.add_argument('-a', '--address',
|
||||||
|
66
scripts/ps.py
Executable file
66
scripts/ps.py
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from nilmdb.client.httpclient import HTTPClient, ClientError, ServerError
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
from nilmdb.utils import datetime_tz
|
||||||
|
import nilmrun
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""List NilmRun processes"""
|
||||||
|
def_url = os.environ.get("NILMRUN_URL", "http://localhost/nilmrun/")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description = 'List NilmRun processes',
|
||||||
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
version = nilmrun.__version__)
|
||||||
|
group = parser.add_argument_group("Standard options")
|
||||||
|
group.add_argument('-u', '--url',
|
||||||
|
help = 'NilmRun server URL', default = def_url)
|
||||||
|
group.add_argument('-n', '--noverify', action="store_true",
|
||||||
|
help = 'Disable SSL certificate verification')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = HTTPClient(baseurl = args.url, verify_ssl = not args.noverify)
|
||||||
|
# Print overall system info
|
||||||
|
info = client.get("process/info")
|
||||||
|
total = info['total']
|
||||||
|
system = info['system']
|
||||||
|
printf(" procs: %d nilm, %d other\n", info['total']['procs'],
|
||||||
|
info['system']['procs'] - info['total']['procs'])
|
||||||
|
printf(" cpu: %d%% nilm, %d%% other, %d%% max\n",
|
||||||
|
round(info['total']['cpu_percent']),
|
||||||
|
round(info['system']['cpu_percent'] - info['total']['cpu_percent']),
|
||||||
|
round(info['system']['cpu_max']))
|
||||||
|
printf(" mem: %d MiB used, %d MiB total, %d%%\n",
|
||||||
|
round(info['system']['mem_used'] / 1048576.0),
|
||||||
|
round(info['system']['mem_total'] / 1048576.0),
|
||||||
|
round(info['system']['mem_used'] * 100.0
|
||||||
|
/ info['system']['mem_total']))
|
||||||
|
|
||||||
|
# Print process detail for each managed process
|
||||||
|
fmt = "%-36s %-6s %-15s %-4s %-3s %-5s\n"
|
||||||
|
printf(fmt, "PID", "STATE", "SINCE", "PROC", "CPU", "LOG")
|
||||||
|
|
||||||
|
if len(info['pids']) == 0:
|
||||||
|
printf("No running processes\n")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
for pid in sorted(info['pids'].keys()):
|
||||||
|
pidinfo = client.get("process/status", { "pid": pid })
|
||||||
|
if pidinfo['alive']:
|
||||||
|
status = "alive"
|
||||||
|
else:
|
||||||
|
if pidinfo['exitcode']:
|
||||||
|
status = "error"
|
||||||
|
else:
|
||||||
|
status = "done"
|
||||||
|
dt = datetime_tz.datetime_tz.fromtimestamp(pidinfo['start_time'])
|
||||||
|
since = dt.strftime("%m/%d-%H:%M:%S")
|
||||||
|
printf(fmt, pid, status, since, info['pids'][pid]['procs'],
|
||||||
|
str(int(round(info['pids'][pid]['cpu_percent']))),
|
||||||
|
len(pidinfo['log']))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
59
scripts/run.py
Executable file
59
scripts/run.py
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from nilmdb.client.httpclient import HTTPClient, ClientError, ServerError
|
||||||
|
from nilmdb.utils.printf import *
|
||||||
|
import nilmrun
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run a command on the NilmRun server"""
|
||||||
|
def_url = os.environ.get("NILMRUN_URL", "http://localhost/nilmrun/")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description = 'Run a command on the NilmRun server',
|
||||||
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
version = nilmrun.__version__)
|
||||||
|
group = parser.add_argument_group("Standard options")
|
||||||
|
group.add_argument('-u', '--url',
|
||||||
|
help = 'NilmRun server URL', default = def_url)
|
||||||
|
group.add_argument('-n', '--noverify', action="store_true",
|
||||||
|
help = 'Disable SSL certificate verification')
|
||||||
|
group = parser.add_argument_group("Program")
|
||||||
|
group.add_argument('-d', '--detach', action="store_true",
|
||||||
|
help = 'Run process and return immediately without '
|
||||||
|
'printing its output')
|
||||||
|
group.add_argument('cmd', help="Command to run")
|
||||||
|
group.add_argument('arg', nargs=argparse.REMAINDER,
|
||||||
|
help="Arguments for command")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = HTTPClient(baseurl = args.url, verify_ssl = not args.noverify)
|
||||||
|
|
||||||
|
# Run command
|
||||||
|
pid = client.post("run/command", { "argv": [ args.cmd ] + args.arg })
|
||||||
|
|
||||||
|
# If we're detaching, just print the PID
|
||||||
|
if args.detach:
|
||||||
|
print pid
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
# Otherwise, watch the log output, and kill the process when it's done
|
||||||
|
# or when this script terminates.
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
s = client.get("process/status", { "pid": pid, "clear": 1 })
|
||||||
|
sys.stdout.write(s['log'])
|
||||||
|
sys.stdout.flush()
|
||||||
|
if not s['alive']:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
finally:
|
||||||
|
s = client.post("process/remove", { "pid": pid })
|
||||||
|
|
||||||
|
raise SystemExit(s['exitcode'])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
5
setup.py
5
setup.py
@@ -61,7 +61,7 @@ setup(name='nilmrun',
|
|||||||
long_description = "NILM Database Filter Runner",
|
long_description = "NILM Database Filter Runner",
|
||||||
license = "Proprietary",
|
license = "Proprietary",
|
||||||
author_email = 'jim@jtan.com',
|
author_email = 'jim@jtan.com',
|
||||||
install_requires = [ 'nilmdb >= 1.8.2',
|
install_requires = [ 'nilmdb >= 1.9.5',
|
||||||
'psutil >= 0.3.0',
|
'psutil >= 0.3.0',
|
||||||
'cherrypy >= 3.2',
|
'cherrypy >= 3.2',
|
||||||
'simplejson',
|
'simplejson',
|
||||||
@@ -75,6 +75,9 @@ setup(name='nilmrun',
|
|||||||
entry_points = {
|
entry_points = {
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'nilmrun-server = nilmrun.scripts.nilmrun_server:main',
|
'nilmrun-server = nilmrun.scripts.nilmrun_server:main',
|
||||||
|
'nilmrun-ps = nilmrun.scripts.ps:main',
|
||||||
|
'nilmrun-run = nilmrun.scripts.run:main',
|
||||||
|
'nilmrun-kill = nilmrun.scripts.kill:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
zip_safe = False,
|
zip_safe = False,
|
||||||
|
@@ -372,3 +372,38 @@ class TestClient(object):
|
|||||||
# kill all processes
|
# kill all processes
|
||||||
for pid in client.get("process/list"):
|
for pid in client.get("process/list"):
|
||||||
client.post("process/remove", { "pid": pid })
|
client.post("process/remove", { "pid": pid })
|
||||||
|
|
||||||
|
def test_client_10_unicode(self):
|
||||||
|
client = HTTPClient(baseurl = testurl, post_json = True)
|
||||||
|
eq_(client.get("process/list"), [])
|
||||||
|
def verify(cmd, result):
|
||||||
|
pid = client.post("run/command", { "argv": [ "sh", "-c", cmd ] })
|
||||||
|
eq_(client.get("process/list"), [pid])
|
||||||
|
status = self.wait_end(client, pid)
|
||||||
|
eq_(result, status["log"])
|
||||||
|
|
||||||
|
# Unicode should work
|
||||||
|
verify("echo -n hello", "hello")
|
||||||
|
verify(u"echo -n ☠", u"☠")
|
||||||
|
verify("echo -ne \\\\xe2\\\\x98\\\\xa0", u"☠")
|
||||||
|
|
||||||
|
# Programs that spit out invalid UTF-8 should get replacement
|
||||||
|
# markers
|
||||||
|
verify("echo -ne \\\\xae", u"\ufffd")
|
||||||
|
|
||||||
|
def test_client_11_atexit(self):
|
||||||
|
# Leave a directory and running process behind, for the atexit
|
||||||
|
# handler to clean up. Here we trigger the atexit manually,
|
||||||
|
# since it's hard to trigger it as part of the test suite.
|
||||||
|
client = HTTPClient(baseurl = testurl, post_json = True)
|
||||||
|
code = textwrap.dedent("""
|
||||||
|
import time
|
||||||
|
time.sleep(10)
|
||||||
|
""")
|
||||||
|
client.post("run/code", { "code": code, "args": [ "hello"] })
|
||||||
|
|
||||||
|
# Trigger atexit function
|
||||||
|
test_server._manager._atexit()
|
||||||
|
|
||||||
|
# Ensure no processes exit
|
||||||
|
eq_(client.get("process/list"), [])
|
||||||
|
Reference in New Issue
Block a user