From a743c26d99bea28caee400f294309d89f2f0acfd Mon Sep 17 00:00:00 2001 From: R2boyo25 Date: Wed, 9 Nov 2022 17:03:37 -0600 Subject: [PATCH] Output codes and docs --- README.MD | 21 +----- docs/api.md | 49 ++++++++++++++ docs/config.md | 35 ++++++++++ main.py | 107 ++++++++++++++++++++++--------- source/table.css | 43 ++++++++----- templates/bar.html | 107 ++++++++++++++++--------------- templates/edit.html | 0 templates/list.html | 49 +++++++------- templates/proc.html | 151 ++++++++++++++++++++++++-------------------- 9 files changed, 353 insertions(+), 209 deletions(-) create mode 100644 docs/api.md create mode 100644 docs/config.md create mode 100644 templates/edit.html diff --git a/README.MD b/README.MD index 79c6a1f..d208d84 100644 --- a/README.MD +++ b/README.MD @@ -6,22 +6,5 @@ Each program is a separate toml config file: `program_name.toml` The web interface it at port 4057. -# Config Options - -```toml -# The command to run. -command = "command --to run" - -# directory for command to be run from. -workdir = "/path/to/working/directory" - -# Whether or not to start the program -# when the program manager is started -# (so you have to start it manually) -start = true or false - -# environment variables -[env] -variablename1 = "variablevalue 1" -variablename2 = "variable value 2" -``` \ No newline at end of file +- [API](/docs/api.md) +- [Per-Process Configuration](/docs/config.md) \ No newline at end of file diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..8ca1a92 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,49 @@ +# API + +# `/stats` +The two following sections are in the order they will be returned from this endpoint. +## Memory +A JSON list of integers (MB): +- Used memory +- Shared Memory +- Disk Cache +- Available Memory +- Used Swap +- Free Swap +## CPU +A JSON list of integers (%): +- CPU Usage +- 100 - CPU Usage + +# `/kill/` +Kills `proc` + +# `/restart/` +Restarts `proc` + +# `/pause/` +Pauses `proc` - [SIGSTOP](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGSTOP) + +# `/unpause/` +Unpauses `proc` - [SIGCONT](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGCONT) + +# `/out/` +Returns the STDOUT & STDERR for `proc` with `\n` replaced with `
\n` + +# `/reload` +Reloads the configuration file and restarts any changed processes. + +# `/status` +Returns a JSON dictionary with strings (`000.00%`) for the percentage of processes in that state. + +## `running` +Processes that are currently running. + +## `paused` +Processes that have been [paused](#pauseproc) + +## `killed` +Processes with return codes other than `0` (So they crashed or were killed.) + +## `done` +Processes with a return code of `0` (So they exited normally.) \ No newline at end of file diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..1606374 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,35 @@ +# Config +Process configuration files are stored in `$HOME/.config/programmanager`. +They use [TOML syntax](https://toml.io/en/). +They can be named anything as long as it ends with `.toml`. The text before `.toml` will be the process name. + +## `command` +The command to run for the process. + +## `workdir` (optional; defaults to `progman`'s working directory) +The directory to run the process in. + +## `start` (optional) +Whether to start a command when `progman` is started. (Or when the configuration is reloaded for the process.) + +## `env` (optional) +`env` is a table where the key's names are environment variable's names and their values are assigned to the environment variables. +```toml +[env] +var = "value" +var2 = "value2" +``` + +# Basic Example +```toml +command = "pwd" +workingdirectory = "/etc/" +``` + +# Basic Example w/ `env` +```toml +command = 'echo "$GREETING"' + +[env] +GREETING = "Hallo!" +``` \ No newline at end of file diff --git a/main.py b/main.py index 802932e..b39a8f9 100644 --- a/main.py +++ b/main.py @@ -19,21 +19,25 @@ log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) + def secho(text, file=None, nl=None, err=None, color=None, **styles): pass + def echo(text, file=None, nl=None, err=None, color=None, **styles): pass + click.echo = echo click.secho = secho #### Disable Logging #### -LASTUPDATE = datetime(year = 1, month = 1, day = 1) +LASTUPDATE = datetime(year=1, month=1, day=1) cfg = {} + class Proc: def __init__(self, name, dct): self.name = name @@ -47,11 +51,11 @@ def __init__(self, name, dct): else: self.start() self.curout = [] - + def start(self): proc = self.dct if "command" in proc: - command = proc["command"]#.split() + command = proc["command"] # .split() else: return @@ -61,11 +65,12 @@ def start(self): for var in proc['env'].keys(): self.env[var] = proc['env'][var] - self.process = subprocess.Popen(command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, cwd = workdir, env = self.env, shell = True, preexec_fn=os.setsid) + self.process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=workdir, env=self.env, shell=True, preexec_fn=os.setsid) flags = fcntl(self.process.stdout, F_GETFL) fcntl(self.process.stdout, F_SETFL, flags | O_NONBLOCK) - + def kill(self): os.killpg(self.process.pid, signal.SIGTERM) @@ -77,15 +82,15 @@ def out(self): try: o += read(self.process.stdout.fileno(), 4096).decode() except OSError: - break + break else: o += self.process.stdout.read().decode() if o.strip().strip("\n") != "": - self.curout.append(o.strip("\n")) + self.curout.append(o.strip("\n")) return "\n".join(self.curout) - + @property def running(self): if not self.paused: @@ -96,37 +101,45 @@ def running(self): else: return True + @property + def returncode(self): + rt = self.process.returncode + return rt if rt else 0 + class Procs: def __init__(self): self.dct = {} - - def start(self, name, dct = None): + + def start(self, name, dct=None): if dct: if name in self.dct.keys(): if self.dct[name].running: self.dct[name].kill() self.dct[name] = Proc(name, dct) - + def proc(self, name): return self.dct[name] procs = Procs() + def listConfigFiles() -> list: c = [] for root, dirs, files in os.walk(configdir): for file in files: c.append(root.rstrip("/") + "/" + file) - + return c + def fname(path) -> str: return path.split("/")[-1].replace(".toml", "") + def changed() -> list: global LASTUPDATE @@ -139,6 +152,7 @@ def changed() -> list: return c + def loadConfig() -> dict: d = {} @@ -147,11 +161,13 @@ def loadConfig() -> dict: return d + def saveConfig(newconfig): for k in newconfig.keys(): with open(configdir + "/" + k + ".toml", "w") as pf: toml.dump(newconfig[k], pf) + def startProcs(): global cfg global procs @@ -168,22 +184,24 @@ def startProcs(): if proc["start"]: procs.start(proc, cfg[proc]) + def start(): global procs - + if not os.path.exists(configdir): os.mkdir(configdir) if os.path.exists(configfile): with open(configfile, "r") as cf: a = toml.load(cf) - + saveConfig(a) os.remove(configfile) startProcs() + def memUsage(): aa = subprocess.check_output("free -m".split()).decode().split("\n") @@ -191,43 +209,49 @@ def memUsage(): while " " in a: a = a.replace(" ", " ") - + ae = a.split() af = aa[2] while " " in a: af = af.replace(" ", " ") - + af = af.split() return [int(e) for e in [ae[2], ae[4], ae[5], ae[6], af[2], af[3]]] + def cpuUsage(): - a = subprocess.check_output(["bash", "-c", "top -b -n1 | grep \"Cpu(s)\" | awk '{print $2+$4}'"]).decode().rstrip("\n") + a = subprocess.check_output( + ["bash", "-c", "top -b -n1 | grep \"Cpu(s)\" | awk '{print $2+$4}'"]).decode().rstrip("\n") return [float(a), 100 - float(a)] + @app.route("/stats") def statusAPI(): return json.dumps([memUsage(), cpuUsage()]) + @app.route("/kill/") def killProc(proc): if proc in procs.dct.keys(): procs.proc(proc).kill() - + return redirect("/proc/" + proc) + @app.route("/restart/") def restartProc(proc): global cfg if proc in procs.dct.keys(): procs.proc(proc).kill() procs.start(proc, cfg[proc]) - + return redirect("/proc/" + proc) + @app.route("/start/") def startProc(proc): global cfg @@ -235,6 +259,7 @@ def startProc(proc): return redirect("/proc/" + proc) + @app.route("/pause/") def pauseProc(proc): procs.proc(proc).paused = True @@ -242,6 +267,7 @@ def pauseProc(proc): return redirect("/proc/" + proc) + @app.route("/unpause/") def unpauseProc(proc): procs.proc(proc).paused = False @@ -249,60 +275,79 @@ def unpauseProc(proc): return redirect("/proc/" + proc) + @app.route("/out/") def statTest(proc): return str(procs.proc(proc).out).replace("\n", "
\n") + @app.route("/reload") def reloadAll(): start() return redirect("/list") + @app.route("/status") def jsonStatus(): if len(procs.dct.keys()) > 0: running = 0 paused = 0 + crashed = 0 + done = 0 for proc in procs.dct.keys(): prc = procs.proc(proc) - if prc.running and not prc.paused: + if not (prc.running and not prc.paused) and prc.returncode == 0: + done += 1 + elif not prc.running: + crashed += 1 + elif prc.running and not prc.paused: running += 1 - if prc.paused: + elif prc.paused: paused += 1 - - runningpercent = running / len(procs.dct.keys()) - pausedpercent = paused / len(procs.dct.keys()) - crashedpercent = 1-runningpercent-pausedpercent + + tprocs = len(procs.dct.keys()) + + runningpercent = running / tprocs + pausedpercent = paused / tprocs + crashedpercent = crashed / tprocs + donepercent = done / tprocs return json.dumps({ "running": f"{round(runningpercent*100)}%", "paused": f"{round(pausedpercent*100)}%", - "killed": f"{round(crashedpercent*100)}%" + "killed": f"{round(crashedpercent*100)}%", + "done": f"{round(donepercent*100)}%", }) else: return json.dumps({ "running": "0%", "paused": "0%", - "killed": "100%" + "killed": "100%", + "done": "0%", }) + @app.route('/') def home(): - return render_template('main.html', mem = str(memUsage()), cpu = str(cpuUsage())) + return render_template('main.html', mem=str(memUsage()), cpu=str(cpuUsage())) + @app.route("/list") def listProcs(): - return render_template("list.html", procs = [[procs.dct[i].running, i, procs.dct[i].paused] for i in procs.dct]) + return render_template("list.html", procs=[{"running": procs.dct[i].running, "process":i, "paused":procs.dct[i].paused, "returncode":procs.dct[i].returncode} for i in procs.dct], str=str) + @app.route("/proc/") def procShow(proc): - return render_template("proc.html", proc = proc, procdata = procs.proc(proc).dct, running = procs.proc(proc).running, paused = procs.proc(proc).paused) + return render_template("proc.html", proc=proc, procdata=procs.proc(proc).dct, running=procs.proc(proc).running, paused=procs.proc(proc).paused, returncode=procs.proc(proc).returncode) + @app.route('/source/') def returnSourceFile(filename): return send_from_directory('source', filename) + if __name__ == '__main__': start() - app.run(host = '0.0.0.0', port = 4057) \ No newline at end of file + app.run(host='0.0.0.0', port=4057) diff --git a/source/table.css b/source/table.css index a030377..4085d2b 100644 --- a/source/table.css +++ b/source/table.css @@ -4,36 +4,45 @@ table { width: 90%; margin-top: 50px; table-layout: fixed; - } - - td, th { +} + +td, +th { border: 1px solid #b6b5b5; text-align: left; padding: 8px; width: 50%; word-wrap: break-word; max-height: 70vh; - } +} - th { +th { background-color: #f7f7f7; - } - - tr:nth-child(even) { +} + +tr:nth-child(odd) { background-color: #dddddd; - } +} - tr:nth-child(odd) { +tr:nth-child(even) { background-color: #f3f3f3; - } +} - #output { +#output { max-width: 90%; max-height: 70vh; overflow-y: auto; - } +} - img { - width: 25px; - height:25px - } \ No newline at end of file +img { + width: 25px; + height: 25px +} + +.addItem { + margin-right: 5%; + margin-left: 5%; + width: 90%; + text-align: right; + display: none; +} \ No newline at end of file diff --git a/templates/bar.html b/templates/bar.html index 8c87d51..42baab6 100644 --- a/templates/bar.html +++ b/templates/bar.html @@ -1,60 +1,63 @@ - - - - Program Manager - {% block head %} + + + + + Program Manager + {% block head %} + {% endblock %} + + + +
+
0%
+
0%
+
100%
+
0%
+
+ +