Kaynağa Gözat

dev: get basic PTY integration working

KernelDeimos 8 ay önce
ebeveyn
işleme
cc6790c7f9

+ 10 - 0
src/emulator/assets/template.html

@@ -22,12 +22,22 @@
           line-height: 16px;
       }
       BODY {
+          padding: 0;
+          margin: 0;
           background-color: #111;
           display: flex;
           justify-content: center;
           align-items: center;
           height: 100vh;
           overflow: hidden;
+          background: linear-gradient(135deg, #232323 50%, transparent 50%) 0% 0% / 3em 3em #101010;
+          background-position: center center;
+          background-size: 5px 5px;
+      }
+      #screen_container {
+        padding: 5px;
+        background-color: #000;
+        box-shadow: 0 0 32px 0 rgba(0,0,0,0.7);
       }
   </style>
 </head>

+ 126 - 289
src/emulator/src/main.js

@@ -1,282 +1,13 @@
 "use strict";
-// puter.ui.launchApp('editor');
 
-// Libs    
-    // SO: 40031688
-    function buf2hex(buffer) { // buffer is an ArrayBuffer
-        return [...new Uint8Array(buffer)]
-            .map(x => x.toString(16).padStart(2, '0'))
-            .join('');
-    }
-
-class ATStream {
-    constructor ({ delegate, acc, transform, observe }) {
-        this.delegate = delegate;
-        if ( acc ) this.acc = acc;
-        if ( transform ) this.transform = transform;
-        if ( observe ) this.observe = observe;
-        this.state = {};
-        this.carry = [];
-    }
-    [Symbol.asyncIterator]() { return this; }
-    async next_value_ () {
-        if ( this.carry.length > 0 ) {
-            console.log('got from carry!', this.carry);
-            return {
-                value: this.carry.shift(),
-                done: false,
-            };
-        }
-        return await this.delegate.next();
-    }
-    async acc ({ value }) {
-        return value;
-    }
-    async next_ () {
-        for (;;) {
-            const ret = await this.next_value_();
-            if ( ret.done ) return ret;
-            const v = await this.acc({
-                state: this.state,
-                value: ret.value,
-                carry: v => this.carry.push(v),
-            });
-            if ( this.carry.length >= 0 && v === undefined ) {
-                throw new Error(`no value, but carry value exists`);
-            }
-            if ( v === undefined ) continue;
-            // We have a value, clear the state!
-            this.state = {};
-            if ( this.transform ) {
-                const new_value = await this.transform(
-                    { value: ret.value });
-                return { ...ret, value: new_value };
-            }
-            return { ...ret, value: v };
-        }
-    }
-    async next () {
-        const ret = await this.next_();
-        if ( this.observe && !ret.done ) {
-            this.observe(ret);
-        }
-        return ret;
-    }
-    async enqueue_ (v) {
-        this.queue.push(v);
-    }
-}
-
-const NewCallbackByteStream = () => {
-    let listener;
-    let queue = [];
-    const NOOP = () => {};
-    let signal = NOOP;
-    (async () => {
-        for (;;) {
-            const v = await new Promise((rslv, rjct) => {
-                listener = rslv;
-            });
-            queue.push(v);
-            signal();
-        }
-    })();
-    const stream = {
-        [Symbol.asyncIterator](){
-            return this;
-        },
-        async next () {
-            if ( queue.length > 0 ) {
-                return {
-                    value: queue.shift(),
-                    done: false,
-                };
-            }
-            await new Promise(rslv => {
-                signal = rslv;
-            });
-            signal = NOOP;
-            const v = queue.shift();
-            return { value: v, done: false };
-        }
-    };
-    stream.listener = data => {
-        listener(data);
-    };
-    return stream;
-}
-
-// Tiny inline little-endian integer library
-const get_int = (n_bytes, array8, signed=false) => {
-    return (v => signed ? v : v >>> 0)(
-        array8.slice(0,n_bytes).reduce((v,e,i)=>v|=e<<8*i,0));
-}
-const to_int = (n_bytes, num) => {
-    return (new Uint8Array()).map((_,i)=>(num>>8*i)&0xFF);
-}
-
-const NewVirtioFrameStream = byteStream => {
-    return new ATStream({
-        delegate: byteStream,
-        async acc ({ value, carry }) {
-            if ( ! this.state.buffer ) {
-                const size = get_int(4, value);
-                // 512MiB limit in case of attempted abuse or a bug
-                // (assuming this won't happen under normal conditions)
-                if ( size > 512*(1024**2) ) {
-                    throw new Error(`Way too much data! (${size} bytes)`);
-                }
-                value = value.slice(4);
-                this.state.buffer = new Uint8Array(size);
-                this.state.index = 0;
-            }
-                
-            const needed = this.state.buffer.length - this.state.index;
-            if ( value.length > needed ) {
-                const remaining = value.slice(needed);
-                console.log('we got more bytes than we needed',
-                    needed,
-                    remaining,
-                    value.length,
-                    this.state.buffer.length,
-                    this.state.index,
-                );
-                carry(remaining);
-            }
-            
-            const amount = Math.min(value.length, needed);
-            const added = value.slice(0, amount);
-            this.state.buffer.set(added, this.state.index);
-            this.state.index += amount;
-            
-            if ( this.state.index > this.state.buffer.length ) {
-                throw new Error('WUT');
-            }
-            if ( this.state.index == this.state.buffer.length ) {
-                return this.state.buffer;
-            }
-        }
-    });
-};
-
-const wisp_types = [
-    {
-        id: 3,
-        label: 'CONTINUE',
-        describe: ({ payload }) => {
-            return `buffer: ${get_int(4, payload)}B`;
-        },
-        getAttributes ({ payload }) {
-            return {
-                buffer_size: get_int(4, payload),
-            };
-        }
-    },
-    {
-        id: 5,
-        label: 'INFO',
-        describe: ({ payload }) => {
-            return `v${payload[0]}.${payload[1]} ` +
-                buf2hex(payload.slice(2));
-        },
-        getAttributes ({ payload }) {
-            return {
-                version_major: payload[0],
-                version_minor: payload[1],
-                extensions: payload.slice(2),
-            }
-        }
-    },
-];
-
-class WispPacket {
-    static SEND = Symbol('SEND');
-    static RECV = Symbol('RECV');
-    constructor ({ data, direction, extra }) {
-        this.direction = direction;
-        this.data_ = data;
-        this.extra = extra ?? {};
-        this.types_ = {
-            1: { label: 'CONNECT' },
-            2: { label: 'DATA' },
-            4: { label: 'CLOSE' },
-        };
-        for ( const item of wisp_types ) {
-            this.types_[item.id] = item;
-        }
-    }
-    get type () {
-        const i_ = this.data_[0];
-        return this.types_[i_];
-    }
-    get attributes () {
-        if ( ! this.type.getAttributes ) return {};
-        const attrs = {};
-        Object.assign(attrs, this.type.getAttributes({
-            payload: this.data_.slice(5),
-        }));
-        Object.assign(attrs, this.extra);
-        return attrs;
-    }
-    toVirtioFrame () {
-        const arry = new Uint8Array(this.data_.length + 4);
-        arry.set(to_int(4, this.data_.length), 0);
-        arry.set(this.data_, 4);
-        return arry;
-    }
-    describe () {
-        return this.type.label + '(' +
-            (this.type.describe?.({
-                payload: this.data_.slice(5),
-            }) ?? '?') + ')';
-    }
-    log () {
-        const arrow =
-            this.direction === this.constructor.SEND ? '->' :
-            this.direction === this.constructor.RECV ? '<-' :
-            '<>' ;
-        console.groupCollapsed(`WISP ${arrow} ${this.describe()}`);
-        const attrs = this.attributes;
-        for ( const k in attrs ) {
-            console.log(k, attrs[k]);
-        }
-        console.groupEnd();
-    }
-    reflect () {
-        const reflected = new WispPacket({
-            data: this.data_,
-            direction:
-                this.direction === this.constructor.SEND ?
-                    this.constructor.RECV :
-                this.direction === this.constructor.RECV ?
-                    this.constructor.SEND :
-                undefined,
-            extra: {
-                reflectedFrom: this,
-            }
-        });
-        return reflected;
-    }
-}
-
-for ( const item of wisp_types ) {
-    WispPacket[item.label] = item;
-}
-
-const NewWispPacketStream = frameStream => {
-    return new ATStream({
-        delegate: frameStream,
-        transform ({ value }) {
-            return new WispPacket({
-                data: value,
-                direction: WispPacket.RECV,
-            });
-        },
-        observe ({ value }) {
-            value.log();
-        }
-    });
-}
+const { XDocumentPTT } = require("../../phoenix/src/pty/XDocumentPTT");
+const {
+    NewWispPacketStream,
+    WispPacket,
+    NewCallbackByteStream,
+    NewVirtioFrameStream,
+    DataBuilder,
+} = require("../../puter-wisp/src/exports");
 
 class WispClient {
     constructor ({
@@ -347,28 +78,132 @@ window.onload = async function()
         byteStream.listener);
     const virtioStream = NewVirtioFrameStream(byteStream);
     const wispStream = NewWispPacketStream(virtioStream);
+
+    const shell = puter.ui.parentApp();
+    const ptt = new XDocumentPTT(shell, {
+        disableReader: true,
+    })
+
+    ptt.termios.echo = false;
     
     class PTYManager {
+        static STATE_INIT = {
+            name: 'init',
+            handlers: {
+                [WispPacket.INFO.id]: function ({ packet }) {
+                    this.client.send(packet.reflect());
+                    this.state = this.constructor.STATE_READY;
+                }
+            }
+        };
+        static STATE_READY = {
+            name: 'ready',
+            handlers: {
+                [WispPacket.DATA.id]: function ({ packet }) {
+                    console.log('stream id?', packet.streamId);
+                    const pty = this.stream_listeners_[packet.streamId];
+                    pty.on_payload(packet.payload);
+                }
+            },
+            on: function () {
+                const pty = this.getPTY();
+                console.log('PTY created', pty);
+                pty.on_payload = data => {
+                    ptt.out.write(data);
+                }
+                (async () => {
+                    // for (;;) {
+                    //     const buff = await ptt.in.read();
+                    //     if ( buff === undefined ) continue;
+                    //     console.log('this is what ptt in gave', buff);
+                    //     pty.send(buff);
+                    // }
+                    const stream = ptt.readableStream;
+                    for await ( const chunk of stream ) {
+                        if ( chunk === undefined ) {
+                            console.error('huh, missing chunk', chunk);
+                            continue;
+                        }
+                        pty.send(chunk);
+                    }
+                })()
+            },
+        }
+
+        set state (value) {
+            console.log('[PTYManager] State updated: ', value.name);
+            this.state_ = value;
+            if ( this.state_.on ) {
+                this.state_.on.call(this)
+            }
+        }
+        get state () { return this.state_ }
+
         constructor ({ client }) {
+            this.streamId = 0;
+            this.state_ = null;
             this.client = client;
+            this.state = this.constructor.STATE_INIT;
+            this.stream_listeners_ = {};
         }
         init () {
             this.run_();
         }
         async run_ () {
-            const handlers_ = {
-                [WispPacket.INFO.id]: ({ packet }) => {
-                    // console.log('guess we doing info packets now', packet);
-                    this.client.send(packet.reflect());
-                }
-            };
             for await ( const packet of this.client.packetStream ) {
-                // console.log('what we got here?',
-                //     packet.type,
-                //     packet,
-                // );
-                handlers_[packet.type.id]?.({ packet });
+                const handlers_ = this.state_.handlers;
+                if ( ! handlers_[packet.type.id] ) {
+                    console.error(`No handler for packet type ${packet.type.id}`);
+                    console.log(handlers_, this);
+                    continue;
+                }
+                handlers_[packet.type.id].call(this, { packet });
+            }
+        }
+
+        getPTY () {
+            const streamId = ++this.streamId;
+            const data = new DataBuilder({ leb: true })
+                .uint8(0x01)
+                .uint32(streamId)
+                .uint8(0x03)
+                .uint16(10)
+                .utf8('/bin/bash')
+                // .utf8('/usr/bin/htop')
+                .build();
+            const packet = new WispPacket(
+                { data, direction: WispPacket.SEND });
+            this.client.send(packet);
+            const pty = new PTY({ client: this.client, streamId });
+            console.log('setting to stream id', streamId);
+            this.stream_listeners_[streamId] = pty;
+            return pty;
+        }
+    }
+
+    class PTY {
+        constructor ({ client, streamId }) {
+            this.client = client;
+            this.streamId = streamId;
+        }
+
+        on_payload (data) {
+
+        }
+
+        send (data) {
+            // convert text into buffers
+            if ( typeof data === 'string' ) {
+                data = (new TextEncoder()).encode(data, 'utf-8')
             }
+            data = new DataBuilder({ leb: true })
+                .uint8(0x02)
+                .uint32(this.streamId)
+                .cat(data)
+                .build();
+            const packet = new WispPacket(
+                { data, direction: WispPacket.SEND });
+            this.client.send(packet);
         }
     }
     
@@ -376,9 +211,11 @@ window.onload = async function()
         client: new WispClient({
             packetStream: wispStream,
             sendFn: packet => {
+                const virtioframe = packet.toVirtioFrame();
+                console.log('virtio frame', virtioframe);
                 emulator.bus.send(
                     "virtio-console0-input-bytes",
-                    packet.toVirtioFrame(),
+                    virtioframe,
                 );
             }
         })

+ 5 - 3
src/phoenix/src/pty/XDocumentPTT.js

@@ -26,7 +26,7 @@ export class XDocumentPTT {
             id: 104,
         },
     }
-    constructor(terminalConnection) {
+    constructor(terminalConnection, opts = {}) {
         for ( const k in XDocumentPTT.IOCTL ) {
             this[k] = async () => {
                 return await new Promise((resolve, reject) => {
@@ -75,8 +75,10 @@ export class XDocumentPTT {
             }
         });
         this.out = this.writableStream.getWriter();
-        this.in = this.readableStream.getReader();
-        this.in = new BetterReader({ delegate: this.in });
+        if ( ! opts.disableReader ) {
+            this.in = this.readableStream.getReader();
+            this.in = new BetterReader({ delegate: this.in });
+        }
 
         terminalConnection.on('message', message => {
             if (message.$ === 'ioctl.set') {

+ 102 - 4
src/puter-wisp/src/exports.js

@@ -13,7 +13,7 @@ lib.get_int = (n_bytes, array8, signed=false) => {
         array8.slice(0,n_bytes).reduce((v,e,i)=>v|=e<<8*i,0));
 }
 lib.to_int = (n_bytes, num) => {
-    return (new Uint8Array()).map((_,i)=>(num>>8*i)&0xFF);
+    return (new Uint8Array(n_bytes)).map((_,i)=>(num>>8*i)&0xFF);
 }
 
 // Accumulator and/or Transformer (and/or Observer) Stream
@@ -183,6 +183,26 @@ const wisp_types = [
             };
         }
     },
+    {
+        id: 1,
+        label: 'CONNECT',
+        describe: ({ attributes }) => {
+            return `${
+                attributes.type === 1 ? 'TCP' :
+                attributes.type === 2 ? 'UDP' :
+                attributes.type === 3 ? 'PTY' :
+                'UNKNOWN'
+            } ${attributes.host}:${attributes.port}`;
+        },
+        getAttributes: ({ payload }) => {
+            const type = payload[0];
+            const port = lib.get_int(2, payload.slice(1));
+            const host = new TextDecoder().decode(payload.slice(3));
+            return {
+                type, port, host,
+            };
+        }
+    },
     {
         id: 5,
         label: 'INFO',
@@ -198,6 +218,20 @@ const wisp_types = [
             }
         }
     },
+    {
+        id: 2,
+        label: 'DATA',
+        describe: ({ attributes }) => {
+            return `${attributes.length}B`;
+        },
+        getAttributes ({ payload }) {
+            return {
+                length: payload.length,
+                contents: payload,
+                utf8: new TextDecoder().decode(payload),
+            }
+        }
+    },
 ];
 
 class WispPacket {
@@ -208,8 +242,6 @@ class WispPacket {
         this.data_ = data;
         this.extra = extra ?? {};
         this.types_ = {
-            1: { label: 'CONNECT' },
-            2: { label: 'DATA' },
             4: { label: 'CLOSE' },
         };
         for ( const item of wisp_types ) {
@@ -222,14 +254,28 @@ class WispPacket {
     }
     get attributes () {
         if ( ! this.type.getAttributes ) return {};
-        const attrs = {};
+        const attrs = {
+            streamId: this.streamId,
+        };
         Object.assign(attrs, this.type.getAttributes({
             payload: this.data_.slice(5),
         }));
         Object.assign(attrs, this.extra);
         return attrs;
     }
+    get payload () {
+        return this.data_.slice(5);
+    }
+    get streamId () {
+        return lib.get_int(4, this.data_.slice(1));
+    }
     toVirtioFrame () {
+        console.log(
+            'WISP packet to virtio frame',
+            this.data_,
+            this.data_.length,
+            lib.to_int(4, this.data_.length),
+        );
         const arry = new Uint8Array(this.data_.length + 4);
         arry.set(lib.to_int(4, this.data_.length), 0);
         arry.set(this.data_, 4);
@@ -238,6 +284,7 @@ class WispPacket {
     describe () {
         return this.type.label + '(' +
             (this.type.describe?.({
+                attributes: this.attributes,
                 payload: this.data_.slice(5),
             }) ?? '?') + ')';
     }
@@ -290,9 +337,60 @@ const NewWispPacketStream = frameStream => {
     });
 }
 
+class DataBuilder {
+    constructor ({ leb } = {}) {
+        this.pos = 0;
+        this.steps = [];
+        this.leb = leb;
+    }
+    uint8(value) {
+        this.steps.push(['setUint8', this.pos, value]);
+        this.pos++;
+        return this;
+    }
+    uint16(value, leb) {
+        leb ??= this.leb;
+        this.steps.push(['setUint8', this.pos, value, leb]);
+        this.pos += 2;
+        return this;
+    }
+    uint32(value, leb) {
+        leb ??= this.leb;
+        this.steps.push(['setUint32', this.pos, value, leb]);
+        this.pos += 4;
+        return this;
+    }
+    utf8(value) {
+        const encoded = new TextEncoder().encode(value);
+        this.steps.push(['array', 'set', encoded, this.pos]);
+        this.pos += encoded.length;
+        return this;
+    }
+    cat(data) {
+        this.steps.push(['array', 'set', data, this.pos]);
+        this.pos += data.length;
+        return this;
+    }
+    build () {
+        const array = new Uint8Array(this.pos);
+        const view = new DataView(array.buffer);
+        for ( const step of this.steps ) {
+            let target = view;
+            let fn_name = step.shift();
+            if ( fn_name === 'array' ) {
+                fn_name = step.shift();
+                target = array;
+            }
+            target[fn_name](...step);
+        }
+        return array;
+    }
+}
+
 module.exports = {
     NewCallbackByteStream,
     NewVirtioFrameStream,
     NewWispPacketStream,
     WispPacket,
+    DataBuilder,
 };