tildefriends/core/terminal.js

358 lines
11 KiB
JavaScript
Raw Normal View History

"use strict";
var kStaticFiles = [
{uri: '', path: 'index.html', type: 'text/html; charset=utf-8'},
{uri: '/edit', path: 'edit.html', type: 'text/html; charset=utf-8'},
{uri: '/style.css', path: 'style.css', type: 'text/css; charset=utf-8'},
{uri: '/favicon.png', path: 'favicon.png', type: 'image/png'},
{uri: '/client.js', path: 'client.js', type: 'text/javascript; charset=utf-8'},
{uri: '/editor.js', path: 'editor.js', type: 'text/javascript; charset=utf-8'},
{uri: '/agplv3-88x31.png', path: 'agplv3-88x31.png', type: 'image/png'},
];
var auth = require('auth');
var form = require('form');
function Terminal() {
this._waiting = [];
this._index = 0;
this._firstLine = 0;
this._lines = [];
this._lastRead = null;
this._lastWrite = null;
this._echo = true;
this._readLine = null;
this._selected = null;
this._corked = 0;
return this;
}
Terminal.kBacklog = 64;
Terminal.prototype.dispatch = function(data) {
for (var i in this._waiting) {
this.feedWaiting(this._waiting[i], data);
}
this._waiting.length = 0;
}
Terminal.prototype.feedWaiting = function(waiting, data) {
var terminal = this;
var payload = terminal._lines.slice(Math.max(0, waiting.haveIndex + 1 - terminal._firstLine));
if (data) {
payload.push(data);
}
if (waiting.haveIndex < terminal._index - 1 || data) {
waiting.resolve({index: terminal._index - 1, lines: payload});
}
}
Terminal.prototype.print = function() {
var data = arguments;
if (this._selected) {
data = {
terminal: this._selected,
value: data
};
}
this._lines.push(data);
this._index++;
if (this._lines.length >= Terminal.kBacklog * 2) {
this._firstLine = this._index - Terminal.kBacklog;
this._lines = this._lines.slice(this._lines.length - Terminal.kBacklog);
}
if (this._corked == 0) {
this.dispatch();
}
this._lastWrite = new Date();
}
Terminal.prototype.notify = function(title, options) {
this.print({action: "notify", title: title, options: options});
}
Terminal.prototype.setTitle = function(value) {
this.print({action: "title", value: value});
}
Terminal.prototype.setPrompt = function(value) {
this.print({action: "prompt", value: value});
}
Terminal.prototype.setPassword = function(value) {
this.print({action: "password", value: value});
}
Terminal.prototype.setHash = function(value) {
this.print({action: "hash", value: value});
}
Terminal.prototype.notifyUpdate = function() {
this.print({action: "update"});
}
Terminal.prototype.split = function(options) {
this.print({action: "split", options: options});
}
Terminal.prototype.select = function(name) {
this._selected = name;
}
Terminal.prototype.postMessageToIframe = function(name, message) {
this.print({action: "postMessageToIframe", name: name, message: message});
}
Terminal.prototype.clear = function() {
//this._lines.length = 0;
//this._firstLine = this._index;
this.print({action: "clear"});
}
Terminal.prototype.ping = function() {
this.dispatch({action: "ping"});
}
Terminal.prototype.getOutput = function(haveIndex) {
var terminal = this;
terminal._lastRead = new Date();
return new Promise(function(resolve) {
if (haveIndex < terminal._index - 1) {
resolve({index: terminal._index - 1, lines: terminal._lines.slice(Math.max(0, haveIndex + 1 - terminal._firstLine))});
} else {
terminal._waiting.push({haveIndex: haveIndex, resolve: resolve});
}
});
}
Terminal.prototype.setEcho = function(echo) {
this._echo = echo;
}
Terminal.prototype.readLine = function() {
var self = this;
if (self._readLine) {
self._readLine[1]();
}
return new Promise(function(resolve, reject) {
self._readLine = [resolve, reject];
});
}
Terminal.prototype.cork = function() {
this._corked++;
}
Terminal.prototype.uncork = function() {
if (--this._corked == 0) {
for (var i = 0; i < this._waiting.length; i++) {
this.feedWaiting(this._waiting[i]);
}
this._waiting.length = 0;
}
}
function invoke(handlers, argv) {
var promises = [];
if (handlers) {
for (var i = 0; i < handlers.length; ++i) {
promises.push(handlers[i].apply({}, argv));
}
}
return Promise.all(promises);
}
function handler(request, response, packageOwner, packageName, uri) {
var found = false;
if (badName(packageOwner) || badName(packageName)) {
var data = "File not found";
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length});
response.end(data);
found = true;
}
if (!found) {
for (var i in kStaticFiles) {
if (uri === kStaticFiles[i].uri) {
found = true;
var data = File.readFile("core/" + kStaticFiles[i].path);
if (kStaticFiles[i].uri == "") {
if (gGlobalSettings && gGlobalSettings['google-signin-client_id']) {
data = data.replace("<!--HEAD-->", `
<script src="https://apis.google.com/js/platform.js" async defer></script>
<meta name="google-signin-client_id" content="${gGlobalSettings['google-signin-client_id']}">`);
}
data = data.replace("$(VIEW_SOURCE)", "/~" + packageOwner + "/" + packageName + "/view");
data = data.replace("$(EDIT_SOURCE)", "/~" + packageOwner + "/" + packageName + "/edit");
} else if (kStaticFiles[i].uri == "/edit") {
var source = File.readFile("packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js") || "";
source = source.replace(/([&<>"])/g, function(x, item) {
return {'&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;'}[item];
});
data = data.replace("$(SOURCE)", source);
}
response.writeHead(200, {"Content-Type": kStaticFiles[i].type, "Content-Length": data.length});
response.end(data);
break;
}
}
}
if (!found) {
var process;
if (uri === "/view") {
var data = File.readFile("packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js");
response.writeHead(200, {"Content-Type": "text/javascript; charset=utf-8", "Content-Length": data.length});
response.end(data);
} else if (uri == "/save") {
var credentials = auth.query(request.headers);
var userName = credentials && credentials.session && credentials.session.name ? credentials.session.name : "guest";
if (badName(packageName)) {
response.writeHead(403, {"Content-Type": "text/plain; charset=utf-8"});
response.end("Invalid package name: " + packageName);
} else if (badName(userName)) {
response.writeHead(403, {"Content-Type": "text/plain; charset=utf-8"});
response.end("Invalid user name: " + userName);
} else {
File.makeDirectory("packages/" + userName);
File.makeDirectory("packages/" + userName + "/" + packageName);
if (!File.writeFile("packages/" + userName + "/" + packageName + "/" + packageName + ".js", request.body || "")) {
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
response.end("/~" + userName + "/" + packageName);
updateProcesses(userName, packageName);
} else {
response.writeHead(500, {"Content-Type": "text/plain; charset=utf-8"});
response.end("Problem saving: " + packageName);
}
}
} else {
var options = {};
var credentials = auth.query(request.headers);
if (credentials && credentials.session) {
options.userName = credentials.session.name;
}
options.credentials = credentials;
if (uri == "/submit") {
process = getServiceProcess(packageOwner, packageName, "submit");
} else if (uri == "/atom") {
process = getServiceProcess(packageOwner, packageName, "atom");
} else {
var sessionId = form.decodeForm(request.query).sessionId;
var isNewSession = false;
if (!getSessionProcess(packageOwner, packageName, sessionId, {create: false})) {
sessionId = makeSessionId();
isNewSession = true;
}
process = getSessionProcess(packageOwner, packageName, sessionId, options);
}
process.lastActive = Date.now();
if (uri === "/send") {
if (isNewSession) {
response.writeHead(403, {"Content-Type": "text/plain; charset=utf-8"});
response.end("Too soon.");
} else {
var command = JSON.parse(request.body);
var eventName = 'unknown';
if (typeof command == "string") {
if (process.terminal._echo) {
process.terminal.print("> " + command);
}
if (process.terminal._readLine) {
let promise = process.terminal._readLine;
process.terminal._readLine = null;
promise[0](command);
}
eventName = 'onInput';
} else if (command.event) {
eventName = command.event;
}
return invoke(process.eventHandlers[eventName], [command]).then(function() {
response.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": "0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
});
response.end("");
}).catch(function(error) {
process.terminal.print(error);
});
}
} else if (uri === "/submit") {
return process.ready.then(function() {
var payload = form.decodeForm(request.body, form.decodeForm(request.query));
return invoke(process.eventHandlers['onSubmit'], [payload]).then(function() {
response.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": "0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
});
return response.end("");
});
});
} else if (uri === "/atom") {
return process.ready.then(function() {
var payload = form.decodeForm(request.body, form.decodeForm(request.query));
return invoke(process.eventHandlers['onAtom'], [payload]).then(function(content) {
var atomContent = content.join();
response.writeHead(200, {
"Content-Type": "application/atom+xml; charset=utf-8",
"Content-Length": atomContent.length.toString(),
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
});
return response.end(atomContent);
});
});
} else if (uri === "/receive") {
if (isNewSession) {
var data = JSON.stringify({
lines: [
{
action: "session",
session: {
sessionId: sessionId,
credentials: credentials,
}
},
]
});
response.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": data.length.toString(),
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
});
process.ready.then(function() {
process.terminal.print({action: "ready", ready: true});
}).catch(function(error) {
process.terminal.print({action: "ready", error: error});
});
response.end(data);
} else {
return process.terminal.getOutput(parseInt(request.body)).then(function(output) {
var data = JSON.stringify(output);
response.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": data.length.toString(),
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
});
response.end(data);
});
}
}
}
}
}
exports.handler = handler;