This commit is contained in:
Athena Funderburg
2026-05-25 07:05:17 +00:00
commit 4b463a3432
682 changed files with 47796 additions and 0 deletions
View File
+6
View File
@@ -0,0 +1,6 @@
def main() -> None:
import run_all
run_all.main(devmode = True)
if __name__ == '__main__':
main()
+125
View File
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Webconsole</title>
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', () => {
new App('{{ wsurl }}');
});
class App {
constructor(wsurl) {
this.ws = new WebSocket(wsurl);
this.ws.onmessage = this._onWSMessage.bind(this);
this.$input = document.getElementById('input');
this.$output = document.getElementById('output');
this.$input.addEventListener('keypress', this._onKeyPress.bind(this));
this.hist = [];
this.histIndex = 0;
}
_onWSMessage(evt) {
this.appendOutput(evt.data, 'out');
}
appendOutput(text, cls) {
const $elm = document.createElement('p');
$elm.textContent = text;
if (cls) $elm.setAttribute('class', cls);
this.$output.appendChild($elm);
this.$output.scrollTop = this.$output.scrollHeight;
}
_onKeyPress(evt) {
if (evt.shiftKey) return;
const k = evt.keyCode;
const $input = this.$input;
if (k == 13) {
const text = $input.value;
$input.value = '';
this.appendOutput(text, 'in');
this.ws.send(text);
this.hist.push(text);
if (this.hist.length > 150) {
this.hist.splice(0, this.hist.length - 100);
}
this.histIndex = this.hist.length;
evt.preventDefault();
return false;
} else if (k == 38) {
this.histIndex -= 1;
if (this.histIndex < 0) {
this.histIndex = this.hist.length - 1;
}
if (this.histIndex >= 0) {
$input.value = this.hist[this.histIndex];
} else {
$input.value = '';
}
evt.preventDefault();
return false;
} else if (k == 40) {
this.histIndex += 1;
if (this.histIndex >= this.hist.length) {
this.histIndex = 0;
}
if (this.histIndex < this.hist.length) {
$input.value = this.hist[this.histIndex];
} else {
$input.value = '';
}
evt.preventDefault();
return false;
}
}
}
</script>
<div id="output"></div>
<textarea id="input"></textarea>
<style>
* {
box-sizing: border-box;
}
body {
margin: 1vh;
background: #999999;
}
#output {
height: 82vh;
background: #DDDDDD;
border: 1px solid #000000;
padding: 0.3em;
font-family: monospace;
white-space: pre-wrap;
overflow: scroll;
}
#input {
height: 15vh;
width: 100%;
}
#output p {
margin: 0em 0em 0.2em;
padding: 0.2em 0.3em;
}
#output .in {
font-weight: bold;
}
#output .in::before {
color: #666666;
content: "\3C \3C \3C ";
}
#output p:hover {
background: #99CC99;
}
</style>
</body>
</html>
+121
View File
@@ -0,0 +1,121 @@
from typing import Dict, Set, Tuple, Any, List, Optional
import asyncio, sys, io, cProfile, pstats, traceback, util.misc
from aiohttp import web
from aiohttp.web import WSMsgType
from core.backend import Backend
from core.http import render
def register(loop: asyncio.AbstractEventLoop, backend: Backend, http_app: web.Application) -> None:
util.misc.add_to_jinja_env(http_app, 'dev', 'dev/tmpl')
http_app['webconsole'] = Webconsole(loop, backend)
http_app.router.add_get('/dev', handle_index)
http_app.router.add_get('/dev/ws', handle_websocket)
async def handle_index(req: web.Request) -> web.Response:
return render(req, 'dev:index.html', { 'wsurl': 'ws://localhost/dev/ws' })
async def handle_websocket(req: web.Request) -> web.StreamResponse:
webconsole: Webconsole = req.app['webconsole']
ws = web.WebSocketResponse()
await ws.prepare(req)
webconsole.websocks.add(ws)
await ws.send_str("Webconsole on.")
async for msg in ws:
type = msg.type
data = msg.data
if type == WSMsgType.ERROR:
print("ws error with exception {}".format(ws.exception()))
continue
if type == WSMsgType.TEXT:
data = data.strip()
if data:
ret = webconsole.run(data)
if ret:
await ws.send_str(ret)
continue
print("ws unknown", type, data[:30])
webconsole.websocks.remove(ws)
return ws
class Webconsole:
__slots__ = ('loop', 'locals', 'objs', 'websocks', 'i')
loop: asyncio.AbstractEventLoop
locals: Dict[str, object]
objs: Dict[object, str]
websocks: Set[web.WebSocketResponse]
i: int
def __init__(self, loop: asyncio.AbstractEventLoop, backend: Backend) -> None:
self.loop = loop
self.locals = {
'_': None,
'be': backend,
'dir': useful_dir,
'dirfull': dir,
'prof': profile,
}
self.objs = {}
self.websocks = set()
self.i = 0
backend._dev = self
def run(self, cmd: str) -> str:
tmp = io.StringIO()
sys.stdout = tmp
try:
self.locals['_'] = exec(compile(cmd + '\n', '<stdin>', 'single'), None, self.locals) # type: ignore
except:
(exctype, excvalue, tb) = sys.exc_info() # type: Tuple[Any, Any, Any]
ret = '\n'.join(traceback.format_exception(exctype, excvalue, tb))
else:
ret = tmp.getvalue()
finally:
sys.stdout = sys.__stdout__
return ret
def connect(self, obj: object) -> None:
varname = 'k{}'.format(self.i)
self.i += 1
self.objs[obj] = varname
self.locals[varname] = obj
msg = "# Connect: `{}`".format(varname)
for ws in self.websocks:
self.loop.create_task(ws.send_str(msg))
def disconnect(self, obj: object) -> None:
varname = self.objs.pop(obj)
msg = "# Disconnect: `{}`".format(varname)
self.locals.pop(varname, None)
for ws in self.websocks:
self.loop.create_task(ws.send_str(msg))
_PROFILE: Optional[cProfile.Profile] = None
def profile(*restrictions: Any) -> None:
global _PROFILE
if _PROFILE is None:
_PROFILE = cProfile.Profile()
_PROFILE.enable()
print("Profiling ON")
return
_PROFILE.disable()
ps = pstats.Stats(_PROFILE).sort_stats('cumulative')
ps.print_stats(*restrictions)
_PROFILE = None
def useful_dir(*args: Any) -> List[str]:
return [
x for x in dir(*args)
if not x.endswith('__')
]