diff --git a/src/emulator/assets/template.html b/src/emulator/assets/template.html
index 15d555bce..750a05491 100644
--- a/src/emulator/assets/template.html
+++ b/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);
}
diff --git a/src/emulator/src/main.js b/src/emulator/src/main.js
index 37d5a3c69..6ef3b3dc3 100644
--- a/src/emulator/src/main.js
+++ b/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,38 +78,144 @@ 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);
+ }
}
const ptyMgr = new PTYManager({
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,
);
}
})
diff --git a/src/phoenix/src/pty/XDocumentPTT.js b/src/phoenix/src/pty/XDocumentPTT.js
index b1054d396..7cb6a9bb8 100644
--- a/src/phoenix/src/pty/XDocumentPTT.js
+++ b/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') {
diff --git a/src/puter-wisp/src/exports.js b/src/puter-wisp/src/exports.js
index 709bb3eba..8d9d8eeb0 100644
--- a/src/puter-wisp/src/exports.js
+++ b/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,
};