📦 Out of Context リポジトリ

リポジトリ / emssr / independent_common_compiler

r2
r2 emssr1
# -*- coding: utf-8 -*-
r2 emssr2
"""Common scope notebook の web server。標準機能だけで書く。
r2 emssr3
r2 emssr4
    python server.py                 8787 番で起動し、ブラウザを開く
r2 emssr5
    python server.py --port 9000 --no-browser
r2 emssr6
r2 emssr7
役目は三つ。
r2 emssr8
r2 emssr9
  1. web/ の静的 file、notebooks/、assets/ の画像と動画を配る
r2 emssr10
  2. 実行時のファイル(vcpu.py、toolchain.py、commonc の image…)を /runtime/ で配る。
r2 emssr11
     ブラウザ側の Pyodide がこれを取り込んで lane64 を動かす
r2 emssr12
  3. ウィンドウの同期。/events が Server-Sent Events、POST /state が書き込みになる
r2 emssr13
r2 emssr14
ウィンドウを分けるのは表示量のためになる。lane の作業領域は交わらない(20 式 1.2)ので、
r2 emssr15
ウィンドウごとに自分の kernel を持たせても state が混ざらない。同期するのは平文と初期値だけで、
r2 emssr16
計算はそれぞれのウィンドウが自分で回す。
r2 emssr17
"""
r2 emssr18
r2 emssr19
import json
r2 emssr20
import os
r2 emssr21
import sys
r2 emssr22
import threading
r2 emssr23
import time
r2 emssr24
import webbrowser
r2 emssr25
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
r2 emssr26
r2 emssr27
HERE = os.path.dirname(os.path.abspath(__file__))
r2 emssr28
WEB = os.path.join(HERE, "web")
r2 emssr29
NOTEBOOKS = os.path.join(HERE, "notebooks")
r2 emssr30
VENDOR = os.path.join(HERE, "vendor")
r2 emssr31
ASSETS = os.path.join(HERE, "assets")   # notebook が貼る画像と動画
r2 emssr32
r2 emssr33
r2 emssr34
# ===== 実行時のファイル =====
r2 emssr35
#
r2 emssr36
# 場所が二通りある。repository の中では Python_vCPU と builds を直に指し、
r2 emssr37
# 配布版では runtime/ に写しが揃っている。どちらでも同じ名前で配る。
r2 emssr38
r2 emssr39
def runtime_map():
r2 emssr40
    local = os.path.join(HERE, "runtime")
r2 emssr41
    names = ("vcpu.py", "vcpu_vec.py", "lex.py", "toolchain.py",
r2 emssr42
             "commonc_lane64.bin", "joinrank.bin", "anf.bin", "joinrank.dic")
r2 emssr43
    out = {}
r2 emssr44
    for n in names:
r2 emssr45
        p = os.path.join(local, n)
r2 emssr46
        if os.path.exists(p):
r2 emssr47
            out[n] = p
r2 emssr48
    if len(out) == len(names):
r2 emssr49
        return out
r2 emssr50
r2 emssr51
    root = os.path.abspath(os.path.join(HERE, os.pardir, os.pardir, os.pardir))
r2 emssr52
    vcpu_dir = os.path.join(root, "continuous_republic", "common_systems", "Python_vCPU")
r2 emssr53
    jr = os.path.join(root, "continuous_republic", "common_apps", "joinrank")
r2 emssr54
    fall = {
r2 emssr55
        "vcpu.py": os.path.join(vcpu_dir, "vcpu.py"),
r2 emssr56
        "vcpu_vec.py": os.path.join(vcpu_dir, "vcpu_vec.py"),
r2 emssr57
        "lex.py": os.path.join(vcpu_dir, "lex.py"),
r2 emssr58
        "toolchain.py": os.path.join(vcpu_dir, "toolchain.py"),
r2 emssr59
        "commonc_lane64.bin": os.path.join(root, "builds", "commonc", "commonc_lane64.bin"),
r2 emssr60
        "joinrank.bin": os.path.join(jr, "build", "joinrank.bin"),
r2 emssr61
        "anf.bin": os.path.join(jr, "build", "anf.bin"),
r2 emssr62
        "joinrank.dic": os.path.join(jr, "joinrank", "joinrank.dic"),
r2 emssr63
    }
r2 emssr64
    for n, p in fall.items():
r2 emssr65
        if n not in out and os.path.exists(p):
r2 emssr66
            out[n] = p
r2 emssr67
    for n in ("generators.py", "scope.py", "session.py", "joint.py"):
r2 emssr68
        out[n] = os.path.join(HERE, "kernel", n)
r2 emssr69
    return out
r2 emssr70
r2 emssr71
r2 emssr72
def kernel_map():
r2 emssr73
    return {n: os.path.join(HERE, "kernel", n)
r2 emssr74
            for n in ("generators.py", "scope.py", "session.py", "joint.py")}
r2 emssr75
r2 emssr76
r2 emssr77
# ===== ウィンドウの同期 =====
r2 emssr78
r2 emssr79
class Hub(object):
r2 emssr80
    """いま編集している平文と種を持ち、繋がっているウィンドウへ配る"""
r2 emssr81
r2 emssr82
    def __init__(self):
r2 emssr83
        self.state = {"rev": 0}
r2 emssr84
        self.clients = []
r2 emssr85
        self.lock = threading.Lock()
r2 emssr86
r2 emssr87
    def snapshot(self):
r2 emssr88
        with self.lock:
r2 emssr89
            return dict(self.state)
r2 emssr90
r2 emssr91
    def publish(self, patch):
r2 emssr92
        with self.lock:
r2 emssr93
            self.state.update(patch)
r2 emssr94
            self.state["rev"] = self.state.get("rev", 0) + 1
r2 emssr95
            body = ("data: %s\n\n" % json.dumps(self.state, ensure_ascii=False)).encode("utf-8")
r2 emssr96
            live = []
r2 emssr97
            for w in self.clients:
r2 emssr98
                try:
r2 emssr99
                    w.write(body)
r2 emssr100
                    w.flush()
r2 emssr101
                    live.append(w)
r2 emssr102
                except Exception:
r2 emssr103
                    pass
r2 emssr104
            self.clients = live
r2 emssr105
            return self.state["rev"]
r2 emssr106
r2 emssr107
    def attach(self, w):
r2 emssr108
        with self.lock:
r2 emssr109
            self.clients.append(w)
r2 emssr110
            return ("data: %s\n\n" % json.dumps(self.state, ensure_ascii=False)).encode("utf-8")
r2 emssr111
r2 emssr112
r2 emssr113
HUB = Hub()
r2 emssr114
r2 emssr115
r2 emssr116
# 拡張子から媒体の型を引く。module の import は JavaScript の型でないと browser が断る
r2 emssr117
_MEDIA = {
r2 emssr118
    ".js": "text/javascript", ".mjs": "text/javascript", ".json": "application/json",
r2 emssr119
    ".wasm": "application/wasm", ".css": "text/css", ".html": "text/html; charset=utf-8",
r2 emssr120
    ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf",
r2 emssr121
    ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
r2 emssr122
    ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
r2 emssr123
    ".mp4": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg",
r2 emssr124
    ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4",
r2 emssr125
    ".pdf": "application/pdf", ".txt": "text/plain; charset=utf-8",
r2 emssr126
}
r2 emssr127
r2 emssr128
r2 emssr129
def _media(path):
r2 emssr130
    return _MEDIA.get(os.path.splitext(path)[1].lower(), "application/octet-stream")
r2 emssr131
r2 emssr132
r2 emssr133
class Handler(SimpleHTTPRequestHandler):
r2 emssr134
r2 emssr135
    def __init__(self, *a, **k):
r2 emssr136
        SimpleHTTPRequestHandler.__init__(self, *a, directory=WEB, **k)
r2 emssr137
r2 emssr138
    def log_message(self, fmt, *args):
r2 emssr139
        pass
r2 emssr140
r2 emssr141
    def end_headers(self):
r2 emssr142
        # 直した web/ がそのまま反映されるように、静的 file もキャッシュさせない
r2 emssr143
        if "Cache-Control" not in self._headers_buffer_names():
r2 emssr144
            self.send_header("Cache-Control", "no-store")
r2 emssr145
        SimpleHTTPRequestHandler.end_headers(self)
r2 emssr146
r2 emssr147
    def _headers_buffer_names(self):
r2 emssr148
        return b"".join(getattr(self, "_headers_buffer", []) or []).decode("latin-1")
r2 emssr149
r2 emssr150
    # ---- 送る ----
r2 emssr151
r2 emssr152
    def _json(self, obj):
r2 emssr153
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
r2 emssr154
        self.send_response(200)
r2 emssr155
        self.send_header("Content-Type", "application/json; charset=utf-8")
r2 emssr156
        self.send_header("Content-Length", str(len(body)))
r2 emssr157
        self.send_header("Cache-Control", "no-store")
r2 emssr158
        self.end_headers()
r2 emssr159
        self.wfile.write(body)
r2 emssr160
r2 emssr161
    def _file(self, path, ctype):
r2 emssr162
        try:
r2 emssr163
            data = open(path, "rb").read()
r2 emssr164
        except OSError:
r2 emssr165
            self.send_error(404, "無い: %s" % os.path.basename(path))
r2 emssr166
            return
r2 emssr167
        self.send_response(200)
r2 emssr168
        self.send_header("Content-Type", ctype)
r2 emssr169
        self.send_header("Content-Length", str(len(data)))
r2 emssr170
        self.send_header("Cache-Control", "no-store")
r2 emssr171
        self.end_headers()
r2 emssr172
        self.wfile.write(data)
r2 emssr173
r2 emssr174
    def do_GET(self):
r2 emssr175
        p = self.path.split("?")[0]
r2 emssr176
r2 emssr177
        if p == "/api/runtime":
r2 emssr178
            files = runtime_map()
r2 emssr179
            files.update(kernel_map())
r2 emssr180
            missing = [n for n, q in files.items() if not os.path.exists(q)]
r2 emssr181
            local = os.path.isfile(os.path.join(VENDOR, "pyodide", "pyodide.js"))
r2 emssr182
            web = os.path.isfile(os.path.join(VENDOR, "web", "marked.umd.js"))
r2 emssr183
            self._json({"files": sorted(files), "missing": missing,
r2 emssr184
                        "pyodide": "/vendor/pyodide/" if local else None,
r2 emssr185
                        "web": "/vendor/web/" if web else None})
r2 emssr186
            return
r2 emssr187
r2 emssr188
        if p.startswith("/runtime/"):
r2 emssr189
            name = p[len("/runtime/"):]
r2 emssr190
            files = runtime_map()
r2 emssr191
            files.update(kernel_map())
r2 emssr192
            if name not in files:
r2 emssr193
                self.send_error(404, "配信の表に無い: %s" % name)
r2 emssr194
                return
r2 emssr195
            self._file(files[name], "application/octet-stream")
r2 emssr196
            return
r2 emssr197
r2 emssr198
        if p == "/api/notebooks":
r2 emssr199
            if not os.path.isdir(NOTEBOOKS):
r2 emssr200
                self._json([])
r2 emssr201
                return
r2 emssr202
            out = []
r2 emssr203
            for n in sorted(os.listdir(NOTEBOOKS)):
r2 emssr204
                if n.endswith(".json"):
r2 emssr205
                    d = json.load(open(os.path.join(NOTEBOOKS, n), encoding="utf-8"))
r2 emssr206
                    out.append({"file": n, "title": d.get("title", n)})
r2 emssr207
            self._json(out)
r2 emssr208
            return
r2 emssr209
r2 emssr210
        if p.startswith("/api/notebook/"):
r2 emssr211
            self._file(self._nb_path(p), "application/json; charset=utf-8")
r2 emssr212
            return
r2 emssr213
r2 emssr214
        if p == "/api/state":
r2 emssr215
            self._json(HUB.snapshot())
r2 emssr216
            return
r2 emssr217
r2 emssr218
        if p == "/events":
r2 emssr219
            self.send_response(200)
r2 emssr220
            self.send_header("Content-Type", "text/event-stream; charset=utf-8")
r2 emssr221
            self.send_header("Cache-Control", "no-store")
r2 emssr222
            self.send_header("Connection", "keep-alive")
r2 emssr223
            self.end_headers()
r2 emssr224
            try:
r2 emssr225
                self.wfile.write(HUB.attach(self.wfile))
r2 emssr226
                self.wfile.flush()
r2 emssr227
            except Exception:
r2 emssr228
                return
r2 emssr229
            # 接続は publish が書けなくなるまで開けたままにする
r2 emssr230
            while True:
r2 emssr231
                with HUB.lock:
r2 emssr232
                    alive = self.wfile in HUB.clients
r2 emssr233
                if not alive:
r2 emssr234
                    return
r2 emssr235
                time.sleep(0.5)
r2 emssr236
r2 emssr237
        if p == "/api/assets":
r2 emssr238
            os.path.isdir(ASSETS) or os.makedirs(ASSETS)
r2 emssr239
            self._json(sorted(os.listdir(ASSETS)))
r2 emssr240
            return
r2 emssr241
r2 emssr242
        if p.startswith("/assets/"):
r2 emssr243
            q = os.path.join(ASSETS, os.path.basename(p[len("/assets/"):]))
r2 emssr244
            self._file(q, _media(q))
r2 emssr245
            return
r2 emssr246
r2 emssr247
        if p.startswith("/vendor/"):
r2 emssr248
            q = os.path.join(VENDOR, p[len("/vendor/"):].replace("/", os.sep))
r2 emssr249
            if not os.path.abspath(q).startswith(VENDOR):
r2 emssr250
                self.send_error(403)
r2 emssr251
                return
r2 emssr252
            self._file(q, _media(q))
r2 emssr253
            return
r2 emssr254
r2 emssr255
        SimpleHTTPRequestHandler.do_GET(self)
r2 emssr256
r2 emssr257
    # ---- notebook の保存と削除 ----
r2 emssr258
    #
r2 emssr259
    # 書き込める場所は notebooks/ の直下だけにする。名前は basename を取り、
r2 emssr260
    # 拡張子を .json に固定するので、上の階層へは出られない。
r2 emssr261
r2 emssr262
    def _nb_path(self, p):
r2 emssr263
        name = os.path.basename(p.split("/api/notebook/")[-1])
r2 emssr264
        if not name.endswith(".json"):
r2 emssr265
            name += ".json"
r2 emssr266
        return os.path.join(NOTEBOOKS, name)
r2 emssr267
r2 emssr268
    def _body(self):
r2 emssr269
        n = int(self.headers.get("Content-Length", 0))
r2 emssr270
        return json.loads(self.rfile.read(n).decode("utf-8"))
r2 emssr271
r2 emssr272
    def do_POST(self):
r2 emssr273
        p = self.path.split("?")[0]
r2 emssr274
        if p == "/state":
r2 emssr275
            try:
r2 emssr276
                patch = self._body()
r2 emssr277
            except ValueError:
r2 emssr278
                self.send_error(400, "JSON ではありません")
r2 emssr279
                return
r2 emssr280
            self._json({"rev": HUB.publish(patch)})
r2 emssr281
            return
r2 emssr282
        if p.startswith("/assets/"):
r2 emssr283
            name = os.path.basename(p[len("/assets/"):])
r2 emssr284
            if not name:
r2 emssr285
                self.send_error(400, "名前がありません")
r2 emssr286
                return
r2 emssr287
            n = int(self.headers.get("Content-Length", 0))
r2 emssr288
            if n > 64 * 1024 * 1024:
r2 emssr289
                self.send_error(413, "64 MB を越えています")
r2 emssr290
                return
r2 emssr291
            os.path.isdir(ASSETS) or os.makedirs(ASSETS)
r2 emssr292
            with open(os.path.join(ASSETS, name), "wb") as f:
r2 emssr293
                f.write(self.rfile.read(n))
r2 emssr294
            self._json({"path": "assets/" + name})
r2 emssr295
            return
r2 emssr296
r2 emssr297
        if p.startswith("/api/notebook/"):
r2 emssr298
            try:
r2 emssr299
                doc = self._body()
r2 emssr300
            except ValueError:
r2 emssr301
                self.send_error(400, "JSON ではありません")
r2 emssr302
                return
r2 emssr303
            os.path.isdir(NOTEBOOKS) or os.makedirs(NOTEBOOKS)
r2 emssr304
            path = self._nb_path(p)
r2 emssr305
            with open(path, "w", encoding="utf-8", newline="\n") as f:
r2 emssr306
                json.dump(doc, f, ensure_ascii=False, indent=2)
r2 emssr307
                f.write("\n")
r2 emssr308
            self._json({"file": os.path.basename(path)})
r2 emssr309
            return
r2 emssr310
        self.send_error(404)
r2 emssr311
r2 emssr312
    def do_DELETE(self):
r2 emssr313
        p = self.path.split("?")[0]
r2 emssr314
        if not p.startswith("/api/notebook/"):
r2 emssr315
            self.send_error(404)
r2 emssr316
            return
r2 emssr317
        path = self._nb_path(p)
r2 emssr318
        if os.path.exists(path):
r2 emssr319
            os.remove(path)
r2 emssr320
        self._json({"file": os.path.basename(path)})
r2 emssr321
r2 emssr322
r2 emssr323
def main(argv):
r2 emssr324
    port, open_browser = 8787, True
r2 emssr325
    it = iter(argv)
r2 emssr326
    for a in it:
r2 emssr327
        if a == "--port":
r2 emssr328
            port = int(next(it))
r2 emssr329
        elif a == "--no-browser":
r2 emssr330
            open_browser = False
r2 emssr331
        else:
r2 emssr332
            print(__doc__)
r2 emssr333
            return 2
r2 emssr334
    files = runtime_map()
r2 emssr335
    files.update(kernel_map())
r2 emssr336
    missing = [n for n, p in files.items() if not os.path.exists(p)]
r2 emssr337
    if missing:
r2 emssr338
        print("配れないファイル: %s" % ", ".join(missing))
r2 emssr339
        print("  setup.py を実行するか、runtime/ へコピーを置く")
r2 emssr340
    url = "http://127.0.0.1:%d/" % port
r2 emssr341
    srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
r2 emssr342
    srv.daemon_threads = True
r2 emssr343
    print("Common scope notebook  %s" % url)
r2 emssr344
    print("  パネルを別ウィンドウで開く: %s?pane=dag  ?pane=chamber  ?pane=tsui  ?pane=lane  ?pane=joint" % url)
r2 emssr345
    if open_browser:
r2 emssr346
        threading.Timer(0.4, lambda: webbrowser.open(url)).start()
r2 emssr347
    try:
r2 emssr348
        srv.serve_forever()
r2 emssr349
    except KeyboardInterrupt:
r2 emssr350
        print("")
r2 emssr351
    return 0
r2 emssr352
r2 emssr353
r2 emssr354
if __name__ == "__main__":
r2 emssr355
    sys.exit(main(sys.argv[1:]))