c9-core/plugins/c9.vfs.client/vfs_client.js

475 wiersze
17 KiB
JavaScript

define(function(require, exports, module) {
"use strict";
main.consumes = [
"Plugin", "auth", "vfs.endpoint", "dialog.error",
"dialog.alert", "error_handler", "metrics", "apf"
];
main.provides = ["vfs"];
return main;
/**
* login flow
*
* init:
* - receive list of VFS servers
* - choose one of the servers (based on some metric)
* - create VFS connection to that VFS server and remember the ID in sessionStorage
*
* offline:
* - ping a stable URL to detect if it is a network error
* - if it is a network error try to reconnect to the same VFS server with the same ID
* - if it is not a network error pick another server
*/
function main(options, imports, register) {
var Plugin = imports.Plugin;
var auth = imports.auth;
var vfsEndpoint = imports["vfs.endpoint"];
var errorDialog = imports["dialog.error"];
var showError = errorDialog.show;
var hideError = errorDialog.hide;
var showAlert = imports["dialog.alert"].show;
var errorHandler = imports.error_handler;
var metrics = imports.metrics;
var apf = imports.apf;
var eio = require("engine.io");
var Consumer = require("vfs-socket/consumer").Consumer;
var connectClient = require("kaefer");
var protocolVersion = require("kaefer/version").protocol;
var smith = require("smith");
var URL = require("url");
var DEBUG = options.debug
&& (typeof location == "undefined"
|| location.href.indexOf("debug=3") > -1);
// The connected vfs unique id
var id;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
// Give reference to vfs to plugins
errorDialog.vfs = plugin;
var buffer = [];
var dashboardUrl = options.dashboardUrl;
var region, vfsBaseUrl, homeUrl, projectUrl, pingUrl, serviceUrl;
var eioOptions, connection, consumer, vfs;
var showErrorTimer, showErrorTimerMessage;
var lastError;
function emptyBuffer() {
var b = buffer;
buffer = [];
b.forEach(function(item) {
if (!item) return;
var xhr = rest.apply(null, item);
if (item.length > 3)
item[3].abort = xhr.abort.bind(xhr);
});
}
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
emit("loaded");
smith.debug = DEBUG;
emit.sticky("connectClient");
connection = connectClient(connectEngine, {
preConnectCheck: preConnectCheck,
debug: DEBUG
});
connection.on("away", emit.bind(null, "away"));
connection.on("back", function(e) {
emit("back");
emptyBuffer();
});
connection.on("disconnect", onDisconnect);
connection.on("connect", onConnect);
reconnectNow();
function connectEngine() {
if (auth.accessToken) {
eioOptions.query = {
access_token: auth.accessToken
};
}
return eio(eioOptions);
}
function preConnectCheck(callback) {
emit.sticky("preConnectCheckStart");
vfsEndpoint.isOnline(function(err, isOnline) {
if (err || !isOnline) return callback(null, false);
if (!eioOptions) return disconnect();
if (!pingUrl) return disconnect();
vfsEndpoint.isServerAlive(pingUrl, function(err, isAlive) {
if (!err && isAlive) {
emit("preConnectCheckEnd");
return callback(null, true);
}
disconnect();
});
});
function disconnect() {
pingUrl = null;
reconnect(function(err) {
if (err && err.fatal)
return;
callback(err, !err);
});
}
}
plugin.once("connect", warnBrokenSafariVersion);
}
/***** Methods *****/
function join(a, b) {
return (a || "").replace(/\/?$/, "/") + (b || "").replace(/^\//, "");
}
function vfsUrl(path) {
// resolve home and project url
return path.charAt(0) == "~"
? join(homeUrl, escape(path.slice(1)))
: join(projectUrl, escape(path));
}
function rest(path, options, callback) {
if (!vfs || !connection || connection.readyState != "open") {
// console.error("[vfs-client] Cannot perform rest action for ", path, " vfs is disconnected");
var stub = { abort: function() { buffer[this.id] = null; } };
stub.id = buffer.push([path, options, callback, stub]) - 1;
return stub;
}
// resolve home and project url
var url = vfsUrl(path);
options.overrideMimeType = options.contentType || "text/plain";
options.contentType = options.contentType || "text/plain";
return auth.request(url, options, function(err, data, res) {
var reErrorCode = /(ENOENT|EISDIR|ENOTDIR|EEXIST|EACCES|ENOTCONNECTED)/;
if (err) {
var isConnected = !connection || connection.readyState == "open";
if (err.code === 499 || (err.code === 0) && !isConnected) {
if (isConnected)
buffer.push([path, options, callback]);
else
rest(path, options, callback);
return;
}
if (!res) return callback(err);
var message = (res.body || "").replace(/^Error:\s+/, "");
var code = res.status === 0
? "ENOTCONNECTED"
: message.match(reErrorCode) && RegExp.$1;
err = new Error(res.body);
err.code = code || undefined;
err.status = res.status;
return callback(err);
}
callback(null, data, res);
});
}
function download(path, filename, isfile) {
var extraPaths = "";
if (Array.isArray(path)) {
extraPaths = path;
path = path[0];
extraPaths = "," + extraPaths.map(function(p) {
return p[0] == path[0] && p != path ? escape(p) : "";
}).filter(Boolean).join(",");
}
window.open(vfsUrl(path) + extraPaths
+ "?download"
// Escape '+', otherwise it gets interpreted as a space.
+ (filename ? "=" + escape(filename) : "").replace(/\+/g, "%2B")
+ (isfile ? "&isfile=1" : ""));
}
function reconnectNow() {
reconnect(function(_err) {
connection && connection.connect();
});
}
function reconnect(callback) {
if (!connection) return;
connection.socket.setSocket(null);
vfsEndpoint.get(protocolVersion, function(err, urls) {
if (err) {
metrics.increment("vfs.failed.connect", 1, true);
if (!showErrorTimer) {
showErrorTimer = setTimeout(function() {
showVfsError(showErrorTimerMessage);
}, err.fatal ? 0 : 20000);
}
showErrorTimerMessage = err;
return callback(err);
}
if (lastError)
hideError(lastError);
region = urls.region;
vfsBaseUrl = urls.url;
homeUrl = urls.home;
projectUrl = urls.project;
pingUrl = urls.ping;
serviceUrl = urls.serviceUrl;
id = pingUrl.split("/").pop();
var parsedSocket = URL.parse(urls.socket);
eioOptions = {
path: parsedSocket.path,
host: parsedSocket.host,
port: parsedSocket.port
|| (parsedSocket.protocol == "https:" ? "443" : null),
secure: parsedSocket.protocol
? parsedSocket.protocol == "https:" : true,
rejectUnauthorized: options.rejectUnauthorized
};
callback();
});
}
function showVfsError(err) {
switch (err.action) {
case "dashboard":
if (/Permission denied \(public key/.test(err.message))
err.message = "SSH permission denied. Please review your workspace configuration.";
return showAlert("Workspace Error", "Unable to access your workspace", err.message, function() {
window.location = dashboardUrl;
}, { yes: "Return to dashboard" });
case "reload":
lastError = showError(err.message + ". Please reload this window.", -1);
setTimeout(function() {
window.location.reload();
}, (Math.random() * 8) + 2 * 60 * 1000);
break;
default:
lastError = showError(err, -1);
}
if (err.fatal)
console.error("Fatal connection error:", err);
}
function warnBrokenSafariVersion() {
if (apf.isSafari && apf.versionSafari == "10.1") {
showAlert("Broken Browser Version Detected", "Websockets are broken in Safari version 10.1.",
"Due to https://bugs.webkit.org/show_bug.cgi?id=170463, websockets do not work well"
+ " on this browser version which may result in frequent disconnects from the service."
+ " Please use Chrome, 'Safari Technology Preview' or another browser "
+ " until the Safari updates to version 10.2.");
}
}
function onDisconnect() {
vfs = null;
emit("disconnect");
}
function onConnect() {
var transport = new smith.EngineIoTransport(connection);
emit("onConnect");
if (consumer)
consumer.disconnect();
clearTimeout(showErrorTimer);
showErrorTimer = null;
consumer = new Consumer();
consumer.connectionTimeout = 5000;
consumer.connect(transport, function(err, _vfs) {
// TODO
if (err) {
errorHandler.reportError(new Error("Error connecting to VFS", { err: err }));
console.error("error connecting to VFS", err);
return;
}
if (emit("beforeConnect", { done: callback, vfs: _vfs }) !== false)
callback();
function callback(shouldReconnect) {
if (shouldReconnect) {
vfsEndpoint.clearCache();
reconnectNow();
return;
}
vfs = _vfs;
bufferedVfsCalls.forEach(vfsCall);
bufferedVfsCalls = [];
emit("connect");
emptyBuffer();
}
});
consumer.on("error", function(err) {
connection.disconnect();
});
}
var bufferedVfsCalls = [];
function vfsCall(method, path, options, callback) {
if (Array.isArray(method))
return vfsCall.apply(null, method);
if (vfs)
return vfs[method](path, options, callback);
else
bufferedVfsCalls.push([method, path, options, callback]);
}
function isIdle() {
if (!connection || !consumer)
return false;
return !Object.keys(connection.unacked).length &&
!Object.keys(consumer.callbacks || {}).length;
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("unload", function() {
loaded = false;
if (connection && connection.socket)
connection.socket.destroying = true;
if (consumer)
consumer.disconnect();
if (connection)
connection.disconnect();
id = null;
buffer = [];
region = null;
vfsBaseUrl = null;
homeUrl = null;
projectUrl = null;
pingUrl = null;
serviceUrl = null;
eioOptions = null;
consumer = null;
connection = null;
vfs = null;
showErrorTimer = null;
showErrorTimerMessage = null;
lastError = null;
});
/***** Register and define API *****/
/**
* @event connect Fires ...
* @event disconnect Fires ...
* @event message Fires ...
* @event away Fires ...
* @event back Fires ...
* @event error Fires ...
*/
plugin.freezePublicAPI({
get connection() { return connection; },
get connecting() { return connection ? connection.readyState == "reconnecting" : true; },
get connected() { return vfs ? connection.readyState == "open" : false; },
get previewUrl() { throw new Error("gone"); },
get serviceUrl() { return serviceUrl; },
get id() { return id; },
get baseUrl() { return vfsBaseUrl; },
get region() { return region; },
/**
* Performs a VFS REST API call
* @param path {String} Path of the resource. Can be prefixed
* with '~' to resolve the path relative
* to the user's home dir
* @param options {Object} Same format as 'http.request'
* @param callback(err, data) {Function}
*/
rest: rest,
download: download,
url: vfsUrl,
reconnect: reconnectNow,
// File management
resolve: vfsCall.bind(null, "resolve"),
stat: vfsCall.bind(null, "stat"),
readfile: vfsCall.bind(null, "readfile"),
readdir: vfsCall.bind(null, "readdir"),
mkfile: vfsCall.bind(null, "mkfile"),
mkdir: vfsCall.bind(null, "mkdir"),
mkdirP: vfsCall.bind(null, "mkdirP"),
appendfile: vfsCall.bind(null, "appendfile"),
rmfile: vfsCall.bind(null, "rmfile"),
rmdir: vfsCall.bind(null, "rmdir"),
rename: vfsCall.bind(null, "rename"),
copy: vfsCall.bind(null, "copy"),
chmod: vfsCall.bind(null, "chmod"),
symlink: vfsCall.bind(null, "symlink"),
// Retrieve Metadata
metadata: vfsCall.bind(null, "metadata"),
// Wrapper around fs.watch or fs.watchFile
watch: vfsCall.bind(null, "watch"),
// Network connection
connect: vfsCall.bind(null, "connect"),
// Process Management
spawn: vfsCall.bind(null, "spawn"),
pty: vfsCall.bind(null, "pty"),
tmux: vfsCall.bind(null, "tmux"),
execFile: vfsCall.bind(null, "execFile"),
killtree: vfsCall.bind(null, "killtree"),
// Extending the API
use: vfsCall.bind(null, "use"),
extend: vfsCall.bind(null, "extend"),
unextend: vfsCall.bind(null, "unextend"),
isIdle: isIdle,
});
register(null, {
"vfs": plugin
});
}
});