feature: adds state and locking
This commit is contained in:
parent
b56f07be42
commit
ad3ae1737e
1 changed files with 81 additions and 28 deletions
109
update_server.py
109
update_server.py
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
RCSTACK_PATH_NAME = "RCSTACK_PATH"
|
RCSTACK_PATH_NAME = "RCSTACK_PATH"
|
||||||
RCSTACK_PATH = os.environ.get(RCSTACK_PATH_NAME)
|
RCSTACK_PATH = os.environ.get(RCSTACK_PATH_NAME)
|
||||||
|
|
@ -16,64 +17,118 @@ except ImportError:
|
||||||
# Py2
|
# Py2
|
||||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
try:
|
||||||
|
from shlex import quote as shell_quote # Py3
|
||||||
|
except ImportError:
|
||||||
|
from pipes import quote as shell_quote # Py2 (Unix)
|
||||||
|
|
||||||
log = logging.getLogger("mini-server")
|
STATUS = {"state": "ready"} # "ready" or "processing"
|
||||||
|
|
||||||
|
|
||||||
|
def _log_line(text, log_path):
|
||||||
|
# compatible with python 2
|
||||||
|
out = open(log_path, "ab")
|
||||||
|
try:
|
||||||
|
if not isinstance(text, bytes):
|
||||||
|
text = text.encode("utf-8")
|
||||||
|
out.write(text + b"\n")
|
||||||
|
out.flush()
|
||||||
|
finally:
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _watch_process(popen_obj, log_path):
|
||||||
|
try:
|
||||||
|
rc = popen_obj.wait()
|
||||||
|
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
_log_line("[" + stamp + "] finished rc=" + str(rc), log_path=log_path)
|
||||||
|
except Exception as e:
|
||||||
|
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
_log_line("[" + stamp + "] watcher error: " + str(e), log_path=log_path)
|
||||||
|
finally:
|
||||||
|
STATUS["state"] = "ready"
|
||||||
|
|
||||||
|
|
||||||
class RcstackUpdater(BaseHTTPRequestHandler):
|
class RcstackUpdater(BaseHTTPRequestHandler):
|
||||||
def __init__(self, request, client_address, server):
|
def __init__(self, request, client_address, server):
|
||||||
super().__init__(request, client_address, server)
|
BaseHTTPRequestHandler.__init__(self, request, client_address, server)
|
||||||
|
|
||||||
def _send_json_ok(self):
|
def _send_json(self, obj, code=200):
|
||||||
body = json.dumps({"status": "ok"}).encode("utf-8")
|
body = json.dumps(obj).encode("utf-8")
|
||||||
self.send_response(200)
|
self.send_response(code)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def _send_json_ok(self):
|
||||||
|
return self._send_json({"status": "ok"})
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/health":
|
if self.path == "/health":
|
||||||
return self._send_json_ok()
|
return self._send_json_ok()
|
||||||
elif self.path == "/update-rcstack":
|
|
||||||
ok, err_msg = self.spawn_rcstack_status()
|
|
||||||
if not ok:
|
|
||||||
self._send_http_error(err_msg.encode("utf-8"), status_code=500)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._send_http_error(b"not found", status_code=404)
|
elif self.path == "/state":
|
||||||
|
return self._send_json({"state": STATUS["state"]})
|
||||||
|
|
||||||
|
elif self.path == "/update-rcstack":
|
||||||
|
# If already processing, return ok and do not spawn a new process.
|
||||||
|
if STATUS["state"] != "ready":
|
||||||
|
return self._send_json({"status": "ok", "state": STATUS["state"]})
|
||||||
|
|
||||||
|
ok, err_msg = self.spawn_rcstack_status()
|
||||||
|
# If spawn failed for real reasons (bad env/path/etc.), return 500
|
||||||
|
if not ok:
|
||||||
|
return self._send_http_error(err_msg.encode("utf-8"), status_code=500)
|
||||||
|
|
||||||
|
return self._send_json({"status": "ok", "state": STATUS["state"]})
|
||||||
|
|
||||||
|
return self._send_http_error(b"not found", status_code=404)
|
||||||
|
|
||||||
def spawn_rcstack_status(self):
|
def spawn_rcstack_status(self):
|
||||||
if not RCSTACK_PATH:
|
if not RCSTACK_PATH:
|
||||||
return False, f"{RCSTACK_PATH_NAME} env variable not set"
|
return False, "%s env variable not set" % (RCSTACK_PATH_NAME,)
|
||||||
|
|
||||||
if not os.path.exists(RCSTACK_PATH) or not os.path.isfile(RCSTACK_PATH):
|
if not os.path.exists(RCSTACK_PATH) or not os.path.isfile(RCSTACK_PATH):
|
||||||
return False, f"{RCSTACK_PATH} does not exist"
|
return False, "%s does not exist" % (RCSTACK_PATH,)
|
||||||
|
|
||||||
rcstack = os.path.expanduser(RCSTACK_PATH)
|
rcstack = os.path.expanduser(RCSTACK_PATH)
|
||||||
|
rcstack_dir = os.path.dirname(rcstack)
|
||||||
|
log_path = os.path.join(rcstack_dir, RCSTACK_LOG)
|
||||||
|
|
||||||
# Append mode; don't use PIPE unless you read it.
|
STATUS["state"] = "processing"
|
||||||
out = open(RCSTACK_LOG, "ab")
|
|
||||||
|
|
||||||
stamp = time.strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
out.write(b"[" + stamp + b"] spawn: " + rcstack.encode("utf-8") + b" status\n")
|
_log_line("[%s] spawn: %s status" % (stamp, rcstack), log_path=log_path)
|
||||||
out.flush()
|
|
||||||
|
|
||||||
# Detach from the request handler process group/session.
|
out = open(log_path, "ab")
|
||||||
# start_new_session=True is Py3; preexec_fn=os.setsid works in Py2/3 on Unix.
|
|
||||||
try:
|
try:
|
||||||
rcstack_dir = os.path.dirname(rcstack)
|
cmd = "%s self-update && %s self-upgrade" % (shell_quote(rcstack), shell_quote(rcstack))
|
||||||
|
|
||||||
subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
[rcstack, "status"],
|
["/bin/sh", "-c", cmd],
|
||||||
cwd=rcstack_dir,
|
cwd=rcstack_dir,
|
||||||
stdout=out,
|
stdout=out,
|
||||||
stderr=out,
|
stderr=out,
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
preexec_fn=os.setsid, # Unix only
|
preexec_fn=os.setsid, # Unix only
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
STATUS["state"] = "ready"
|
||||||
|
_log_line("[%s] spawn error: %s" % (stamp, str(e)), log_path=log_path)
|
||||||
|
return False, "spawn failed: %s" % (str(e),)
|
||||||
finally:
|
finally:
|
||||||
out.close()
|
try:
|
||||||
|
out.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
t = threading.Thread(target=_watch_process, args=(p, log_path))
|
||||||
|
try:
|
||||||
|
t.daemon = True
|
||||||
|
except Exception:
|
||||||
|
t.setDaemon(True) # Py2
|
||||||
|
t.start()
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
|
@ -83,18 +138,16 @@ class RcstackUpdater(BaseHTTPRequestHandler):
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(msg)
|
self.wfile.write(msg)
|
||||||
|
|
||||||
def log_message(self, fmt, *args):
|
|
||||||
log.info("%s - %s", self.address_string(), fmt % args)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||||
)
|
)
|
||||||
host = "127.0.0.1" # allow only run within the system, no external connections allowed
|
host = "127.0.0.1"
|
||||||
port = 10025
|
port = 10025
|
||||||
httpd = HTTPServer((host, port), RcstackUpdater)
|
httpd = HTTPServer((host, port), RcstackUpdater)
|
||||||
|
log = logging.getLogger("mini-server")
|
||||||
log.info("listening on http://%s:%d", host, port)
|
log.info("listening on http://%s:%d", host, port)
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue