mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
dev: get basic PTY integration working
This commit is contained in:
parent
dd8fe8f03e
commit
cc6790c7f9
@ -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>
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
})
|
||||
|
@ -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') {
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user