kopia lustrzana https://github.com/c9/core
431 wiersze
15 KiB
JavaScript
431 wiersze
15 KiB
JavaScript
define(function(require, exports, module) {
|
|
"use strict";
|
|
|
|
main.consumes = [
|
|
"Plugin", "auth", "vfs.endpoint", "dialog.error",
|
|
"dialog.alert", "error_handler", "metrics"
|
|
];
|
|
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 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;
|
|
|
|
smith.debug = DEBUG;
|
|
|
|
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) {
|
|
|
|
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) return callback(null, true);
|
|
|
|
disconnect();
|
|
});
|
|
});
|
|
|
|
function disconnect() {
|
|
pingUrl = null;
|
|
reconnect(function(err) {
|
|
if (err && err.fatal)
|
|
return;
|
|
callback(err, !err);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/***** 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") {
|
|
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"
|
|
+ (filename ? "=" + escape(filename) : "")
|
|
+ (isfile ? "&isfile=1" : ""));
|
|
}
|
|
|
|
function reconnectNow() {
|
|
reconnect(function(_err) {
|
|
connection.connect();
|
|
});
|
|
}
|
|
|
|
function reconnect(callback) {
|
|
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
|
|
};
|
|
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;
|
|
});
|
|
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 onDisconnect() {
|
|
vfs = null;
|
|
emit("disconnect");
|
|
}
|
|
|
|
function onConnect() {
|
|
var transport = new smith.EngineIoTransport(connection);
|
|
|
|
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]);
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function(){
|
|
load();
|
|
});
|
|
plugin.on("unload", function(){
|
|
loaded = false;
|
|
|
|
id = null;
|
|
buffer = [];
|
|
region = null;
|
|
vfsBaseUrl = null;
|
|
homeUrl = null;
|
|
projectUrl = null;
|
|
pingUrl = null;
|
|
serviceUrl = null;
|
|
eioOptions = null;
|
|
consumer = 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")
|
|
});
|
|
|
|
register(null, {
|
|
"vfs": plugin
|
|
});
|
|
}
|
|
}); |