Update.
This commit is contained in:
parent
f02058b0c0
commit
7b2c93bcef
src/snek
@ -51,6 +51,7 @@ from snek.view.register import RegisterView
|
||||
from snek.view.repository import RepositoryView
|
||||
from snek.view.rpc import RPCView
|
||||
from snek.view.search_user import SearchUserView
|
||||
from snek.view.container import ContainerView
|
||||
from snek.view.settings.containers import (
|
||||
ContainersCreateView,
|
||||
ContainersDeleteView,
|
||||
@ -250,6 +251,7 @@ class Application(BaseApplication):
|
||||
show_index=True,
|
||||
)
|
||||
self.router.add_view("/profiler.html", profiler_handler)
|
||||
self.router.add_view("/container/sock/{channel_uid}.json", ContainerView)
|
||||
self.router.add_view("/about.html", AboutHTMLView)
|
||||
self.router.add_view("/about.md", AboutMDView)
|
||||
self.router.add_view("/logout.json", LogoutView)
|
||||
|
@ -12,25 +12,26 @@ class ContainerService(BaseService):
|
||||
self.compose = ComposeFileManager(self.compose_path,self.container_event_handler)
|
||||
self.event_listeners = {}
|
||||
|
||||
async def add_event_listener(self, name, event):
|
||||
async def add_event_listener(self, name, event,event_handler):
|
||||
if not name in self.event_listeners:
|
||||
self.event_listeners[name] = {}
|
||||
if not event in self.event_listeners[name]:
|
||||
self.event_listeners[name][event] = []
|
||||
queue = asyncio.Queue()
|
||||
self.event_listeners[name][event].append(queue)
|
||||
return queue.get
|
||||
self.event_listeners[name][event].append(event_handler)
|
||||
|
||||
async def container_event_handler(self, name, event, data):
|
||||
event_listeners = self.event_listeners.get(name, {})
|
||||
queues = event_listeners.get(event, [])
|
||||
for queue in queues:
|
||||
await queue.put(data)
|
||||
handlers = event_listeners.get(event, [])
|
||||
for handler in handlers:
|
||||
if not await handler(data):
|
||||
handlers.remove(handler)
|
||||
|
||||
async def get_instances(self):
|
||||
return list(self.compose.list_instances())
|
||||
|
||||
async def get_container_name(self, channel_uid):
|
||||
if channel_uid.startswith("channel-"):
|
||||
return channel_uid
|
||||
return f"channel-{channel_uid}"
|
||||
|
||||
async def get(self,channel_uid):
|
||||
@ -45,6 +46,9 @@ class ContainerService(BaseService):
|
||||
async def get_status(self, channel_uid):
|
||||
return await self.compose.get_instance_status(await self.get_container_name(channel_uid))
|
||||
|
||||
async def write_stdin(self, channel_uid, data):
|
||||
return await self.compose.write_stdin(await self.get_container_name(channel_uid), data)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
channel_uid,
|
||||
|
@ -9,6 +9,10 @@ html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
padding: 50px;
|
||||
height: auto;
|
||||
|
94
src/snek/static/container.js
Normal file
94
src/snek/static/container.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { app } from "./app.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
|
||||
export class Container extends EventHandler{
|
||||
status = "unknown"
|
||||
cpus = 0
|
||||
memory = "0m"
|
||||
image = "unknown:unknown"
|
||||
name = null
|
||||
channelUid = null
|
||||
log = false
|
||||
bytesSent = 0
|
||||
bytesReceived = 0
|
||||
_container = null
|
||||
render(el){
|
||||
if(this._container == null){
|
||||
this._container = el
|
||||
this.terminal.open(this._container)
|
||||
|
||||
this.terminal.onData(data => this.ws.send(new TextEncoder().encode(data)));
|
||||
}
|
||||
this._fitAddon.fit();
|
||||
this.refresh()
|
||||
|
||||
}
|
||||
refresh(){
|
||||
this._fitAddon.fit();
|
||||
this.terminal.write("\x0C");
|
||||
}
|
||||
toggle(){
|
||||
this._container.classList.toggle("hidden")
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
constructor(channelUid,log){
|
||||
super()
|
||||
this.terminal = new Terminal({ cursorBlink: true });
|
||||
this._fitAddon = new FitAddon.FitAddon();
|
||||
this.terminal.loadAddon(this._fitAddon);
|
||||
window.addEventListener("resize", () => this._fitAddon.fit());
|
||||
this.log = log ? true : false
|
||||
this.channelUid = channelUid
|
||||
this.update()
|
||||
this.addEventListener("stdout", (data) => {
|
||||
this.bytesReceived += data.length
|
||||
if(this.log){
|
||||
console.log(`Container ${this.name}: ${data}`)
|
||||
}
|
||||
const fixedData = new Uint8Array(data);
|
||||
this.terminal.write(new TextDecoder().decode(fixedData));
|
||||
|
||||
})
|
||||
this.ws = new WebSocket(`/container/sock/${channelUid}.json`)
|
||||
this.ws.binaryType = "arraybuffer"; // Support binary data
|
||||
this.ws.onmessage = (event) => {
|
||||
this.emit("stdout", event.data)
|
||||
}
|
||||
this.ws.onopen = () => {
|
||||
this.refresh()
|
||||
}
|
||||
window.container = this
|
||||
|
||||
}
|
||||
async start(){
|
||||
const result = await app.rpc.startContainer(this.channelUid)
|
||||
await this.refresh()
|
||||
return result && this.status == 'running'
|
||||
}
|
||||
async stop(){
|
||||
const result = await app.rpc.stopContainer(this.channelUid)
|
||||
await this.refresh()
|
||||
return result && this.status == 'stopped'
|
||||
}
|
||||
async write(data){
|
||||
await this.ws.send(data)
|
||||
this.bytesSent += data.length
|
||||
return true
|
||||
}
|
||||
async update(){
|
||||
|
||||
const container = await app.rpc.getContainer(this.channelUid)
|
||||
this.status = container["status"]
|
||||
this.cpus = container["cpus"]
|
||||
this.memory = container["memory"]
|
||||
this.image = container["image"]
|
||||
this.name = container["name"]
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
window.getContainer = function(){
|
||||
return new Container(app.channelUid)
|
||||
}*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
import copy
|
||||
|
||||
import json
|
||||
import yaml
|
||||
import asyncio
|
||||
import subprocess
|
||||
@ -25,6 +25,25 @@ class ComposeFileManager:
|
||||
def list_instances(self):
|
||||
return list(self.compose.get("services", {}).keys())
|
||||
|
||||
async def _create_readers(self, container_name):
|
||||
instance = await self.get_instance(container_name)
|
||||
if not instance:
|
||||
return False
|
||||
proc = self.running_instances.get(container_name)
|
||||
if not proc:
|
||||
return False
|
||||
async def reader(event_handler,stream):
|
||||
while True:
|
||||
line = await stream.readline()
|
||||
print("XXX",line)
|
||||
if not line:
|
||||
break
|
||||
await event_handler(container_name,"stdout",line)
|
||||
await self.stop(container_name)
|
||||
asyncio.create_task(reader(self.event_handler,proc.stdout))
|
||||
asyncio.create_task(reader(self.event_handler,proc.stderr))
|
||||
|
||||
|
||||
def create_instance(
|
||||
self,
|
||||
name,
|
||||
@ -38,8 +57,7 @@ class ComposeFileManager:
|
||||
service = {
|
||||
"image": image,
|
||||
}
|
||||
if command:
|
||||
service["command"] = command
|
||||
service["command"] = command or "tail -f /dev/null"
|
||||
if cpus or memory:
|
||||
service["deploy"] = {"resources": {"limits": {}}}
|
||||
if cpus:
|
||||
@ -68,7 +86,7 @@ class ComposeFileManager:
|
||||
instance['status'] = await self.get_instance_status(name)
|
||||
print("INSTANCE",instance)
|
||||
|
||||
return instance
|
||||
return json.loads(json.dumps(instance,default=str))
|
||||
|
||||
def duplicate_instance(self, name, new_name):
|
||||
orig = self.get_instance(name)
|
||||
@ -100,6 +118,21 @@ class ComposeFileManager:
|
||||
running_services = stdout.decode().split()
|
||||
return "running" if name in running_services else "stopped"
|
||||
|
||||
async def write_stdin(self, name, data):
|
||||
await self.event_handler(name, "stdin", data)
|
||||
proc = self.running_instances.get(name)
|
||||
print("Found proc:",proc)
|
||||
print(name,data)
|
||||
if not proc:
|
||||
return False
|
||||
try:
|
||||
proc.stdin.write(data.encode())
|
||||
return True
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
await self.stop(name)
|
||||
return False
|
||||
|
||||
async def stop(self, name):
|
||||
"""Asynchronously stop a container by doing 'docker compose stop [name]'."""
|
||||
if name not in self.list_instances():
|
||||
@ -118,7 +151,12 @@ class ComposeFileManager:
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to stop {name}: {stderr.decode()}")
|
||||
return stdout.decode()
|
||||
if stdout:
|
||||
await self.event_handler(name,"stdout",stdout)
|
||||
return stdout.decode(errors="ignore")
|
||||
|
||||
await self.event_handler(name,"stdout",stderr)
|
||||
return stderr.decode(errors="ignore")
|
||||
|
||||
async def start(self, name):
|
||||
"""Asynchronously start a container by doing 'docker compose up -d [name]'."""
|
||||
@ -126,20 +164,41 @@ class ComposeFileManager:
|
||||
return False
|
||||
|
||||
status = await self.get_instance_status(name)
|
||||
if name in self.running_instances and status == "running":
|
||||
if name in self.running_instances and status == "running" and self.running_instances.get(name):
|
||||
await self.stop(name)
|
||||
return True
|
||||
else:
|
||||
elif name in self.running_instances:
|
||||
del self.running_instances[name]
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "compose", "-f", self.compose_path, "up", "-d", name,
|
||||
"docker", "compose", "-f", self.compose_path, "up", name, "-d",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
stdout,stderr = await proc.communicate()
|
||||
print(stdout, stderr)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to start {name}: {stderr.decode()}")
|
||||
return stdout.decode()
|
||||
print(f"Failed to start {name}: {stderr.decode(errors='ignore')}")
|
||||
return False
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "compose", "-f", self.compose_path, "exec", name, "/bin/bash",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
# stdin,stderr = await proc.communicate()
|
||||
self.running_instances[name] = proc
|
||||
#if stdout:
|
||||
# await self.event_handler(name, "stdout", stdout)
|
||||
#if stderr:
|
||||
# await self.event_handler(name,"stdout",stderr)
|
||||
|
||||
await self._create_readers(name)
|
||||
|
||||
return True
|
||||
#return stdout and stdout.decode(errors="ignore") or stderr.decode(errors="ignore")
|
||||
|
||||
# Example usage:
|
||||
# mgr = ComposeFileManager()
|
||||
|
@ -17,6 +17,11 @@
|
||||
<script src="/user-list.js"></script>
|
||||
<script src="/message-list.js" type="module"></script>
|
||||
<script src="/chat-input.js" type="module"></script>
|
||||
<script src="/container.js" type="module"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/sandbox.css">
|
||||
<link rel="stylesheet" href="/user-list.css">
|
||||
<link rel="stylesheet" href="/fa640.all.min.css">
|
||||
@ -50,7 +55,13 @@
|
||||
<chat-window class="chat-area"></chat-window>
|
||||
{% endblock %}
|
||||
</main>
|
||||
<script>
|
||||
<script type="module">
|
||||
import { app } from "/app.js";
|
||||
import { Container } from "/container.js";
|
||||
app.channelUid = '{{ channel.uid.value }}'
|
||||
window.getContainer = function(){
|
||||
return new Container(app.channelUid,true)
|
||||
}
|
||||
let installPrompt = null
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
//e.preventDefault();
|
||||
|
@ -5,6 +5,7 @@
|
||||
{% block main %}
|
||||
|
||||
<section class="chat-area">
|
||||
<div id="terminal" class="hidden"></div>
|
||||
<message-list class="chat-messages">
|
||||
{% if not messages %}
|
||||
|
||||
@ -42,14 +43,22 @@ const messagesContainer = document.querySelector(".chat-messages");
|
||||
const chatArea = document.querySelector(".chat-area");
|
||||
const channelUid = "{{ channel.uid.value }}";
|
||||
const username = "{{ user.username.value }}";
|
||||
|
||||
let container = null
|
||||
// --- Command completions ---
|
||||
chatInputField.autoCompletions = {
|
||||
"/online": showOnline,
|
||||
"/clear": () => { messagesContainer.innerHTML = ''; },
|
||||
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
||||
"/help": showHelp,
|
||||
"/container": () =>{ containerDialog.openWithStatus()}
|
||||
"/container": async() =>{
|
||||
if(container == null){
|
||||
container = await window.getContainer()
|
||||
const terminal = document.querySelector("#terminal")
|
||||
terminal.classList.toggle("hidden")
|
||||
container.render(terminal)
|
||||
}
|
||||
containerDialog.openWithStatus()
|
||||
}
|
||||
};
|
||||
|
||||
// --- Throttle utility ---
|
||||
|
60
src/snek/view/container.py
Normal file
60
src/snek/view/container.py
Normal file
@ -0,0 +1,60 @@
|
||||
from snek.system.view import BaseView
|
||||
import functools
|
||||
from aiohttp import web
|
||||
|
||||
class ContainerView(BaseView):
|
||||
|
||||
|
||||
async def stdout_event_handler(self, ws, data):
|
||||
try:
|
||||
await ws.send_bytes(data)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
await ws.close()
|
||||
return False
|
||||
return True
|
||||
|
||||
async def create_stdout_event_handler(self, ws):
|
||||
return functools.partial(self.stdout_event_handler, ws)
|
||||
|
||||
async def get(self):
|
||||
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(self.request)
|
||||
if not self.request.session.get("logged_in"):
|
||||
return self.HTTPUnauthorized()
|
||||
|
||||
channel_uid = self.request.match_info.get("channel_uid")
|
||||
channel_member = await self.services.channel_member.get(channel_uid=channel_uid, user_uid=self.request.session.get("uid"))
|
||||
if not channel_member:
|
||||
return web.HTTPUnauthorized()
|
||||
|
||||
container = await self.services.container.get(channel_uid)
|
||||
if not container:
|
||||
return web.HTTPNotFound()
|
||||
|
||||
if not container['status'] == 'running':
|
||||
resp = await self.services.container.start(channel_uid)
|
||||
await ws.send_str(str(resp))
|
||||
|
||||
container_name = await self.services.container.get_container_name(channel_uid)
|
||||
|
||||
event_handler = await self.create_stdout_event_handler(ws)
|
||||
|
||||
await self.services.container.add_event_listener(container_name, "stdout", event_handler)
|
||||
|
||||
while True:
|
||||
data = await ws.receive()
|
||||
if data.type == web.WSMsgType.TEXT:
|
||||
|
||||
await self.services.container.write_stdin(container_name, data.data)
|
||||
elif data.type == web.WSMsgType.CLOSE:
|
||||
break
|
||||
elif data.type == web.WSMsgType.ERROR:
|
||||
break
|
||||
|
||||
await self.services.container.remove_event_listener(container_name, channel_uid, "stdout")
|
||||
|
||||
return ws
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user