define(function(require, exports, module) { "use strict"; main.consumes = ["Plugin", "auth", "http", "api", "error_handler", "metrics"]; main.provides = ["vfs.endpoint"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var auth = imports.auth; var http = imports.http; var api = imports.api; var errorHandler = imports.error_handler; var metrics = imports.metrics; var PARALLEL_SEARCHES = 2; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var urlServers, lastVfs; var query = require("url").parse(document.location.href, true).query; if (query.vfs) { if (!query.vfs.match(/^https:\/\/.*\/vfs$/)) alert("Bad VFS URL passed, expected: https://host/vfs"); urlServers = [{ url: query.vfs, region: "url" }]; } if (query.vfs || query.region) { var vfs = recallVfs(); if (vfs) { if (query.vfs && query.vfs !== vfs.url) deleteOldVfs(); else if (query.region && query.region !== vfs.region) deleteOldVfs(); } } if (query.vfs) options.updateServers = false; var strictRegion = query.region || options.strictRegion; var ignoreProtocolVersion = options.ignoreProtocolVersion; var region = strictRegion || options.region; var servers; var pendingServerReqs = []; initDefaultServers(); options.pid = options.pid || 1; /***** Methods *****/ function initDefaultServers(baseURI) { if (options.getServers) return options.getServers(init); init(); function init() { options.getServers = undefined; var loc = require("url").parse(baseURI || document.baseURI || window.location.href); var defaultServers = [{ url: loc.protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + "/vfs", region: "default" }]; servers = (urlServers || options.servers || defaultServers).map(function(server) { server.url = server.url.replace(/\/*$/, ""); return server; }); pendingServerReqs.forEach(function(cb) { cb(null, servers); }); } } function getServers(callback) { if (typeof options.getServers == "function") return pendingServerReqs.push(callback); if (!options.updateServers) return callback(null, servers); // first time take the ones from the options var _servers = servers; if (_servers && _servers.length) { servers = null; return callback(null, _servers); } api.vfs.get("servers", function(err, servers) { if (err) return callback(err); return callback(null, servers.servers); }); } function getVfsEndpoint(version, callback) { getServers(function(err, _servers) { if (err) { if (err.code !== "EDISCONNECT") errorHandler.reportError(new Error("Could not get list of VFS servers"), { cause: err }); metrics.increment("vfs.failed.connect_getservers", 1, true); initDefaultServers(); _servers = servers; } getVfsUrl(version, _servers, function(err, vfsid, url, region) { if (err) return callback(err); callback(null, { url: url, region: region, home: vfsid + "/home", project: vfsid + "/workspace", socket: vfsid + "/socket", ping: vfsid, serviceUrl: vfsid, }); }); }); } function isOnline(callback) { http.request("/_ping", { timeout: 3000, headers: { Accept: "application/json" } }, function(err, data, res) { callback(err, !err); }); } function isServerAlive(url, callback) { auth.request(url, { headers: { Accept: "application/json" } }, function(err, data, res) { if (err) deleteOldVfs(); callback(err, !err); }); } function getVfsUrl(version, vfsServers, callback) { var vfs = recallVfs(); if (vfs && vfs.vfsid) { auth.request(vfs.vfsid, { method: "GET", headers: { Accept: "application/json" } }, function(err, res) { if (err) { deleteOldVfs(); return getVfsUrl(version, vfsServers, callback); } callback(null, vfs.vfsid, vfs.url, vfs.region); }); return; } servers = shuffleServers(version, vfsServers); // check for version if (vfsServers.length && !servers.length) { if (strictRegion) return callback(fatalError("No VFS server(s) found for region " + strictRegion, "reload")); return onProtocolChange(callback); } var latestServer = 0; var foundServer = false; /* Create a callback that is only ever called once */ var mainCallback = callback; callback = function() { if (!foundServer) { foundServer = true; var args = Array.prototype.slice.call(arguments); return mainCallback.apply(this, args); } }; // just take the first server that doesn't return an error function tryNext(i) { if (foundServer) return false; if (i >= servers.length) { metrics.increment("vfs.failed.connect_all", 1, true); return callback(new Error("Disconnected: Could not reach your workspace. Please try again later.")); } var server = servers[i]; auth.request(server.url + "/" + options.pid, { method: "POST", timeout: 120000, body: { version: version }, headers: { Accept: "application/json" } }, function(err, res) { // the workspace is not configured correctly if (err && res && res.error) { if (err.code == 429) { // rate limited setTimeout(function() { tryNext(i); }, res.error.retryIn || 10000); return; } else if (err.code == 412 && res.error && res.error.subtype == "protocol_mismatch") { return onProtocolChange(callback); } else if (err.code == 412) { callback(fatalError(res.error.message, "dashboard")); return; } else if (err.code === 428 && res.error) { emit("restore", { projectState: res.error.projectState, premium: res.error.premium, progress: res.error.progress || { progress: 0, nextProgress: 0, message: "" } }); setTimeout(function() { tryNext(i); }, res.error.retryIn || 10000); return; } else if (err.code == 403) { if (res.error.blocked) callback(fatalError(res.error.message, "dashboard")); // forbidden. User doesn't have access // wait a while before trying again setTimeout(function() { tryNext(i); }, 10000); return; } else if (err.code === 500 && res && res.error && res.error.cause) { return callback(res.error.cause.message); } } if (err) { setTimeout(function() { tryNext(++latestServer); }, 2000); return; } if (!foundServer) { var vfs = rememberVfs(server, res.vfsid); callback(null, vfs.vfsid, server.url, server.region); } }); } function startParallelSearches (totalRunners) { var attemptedServers = {}; for (var s = 0; s < servers.length && s < totalRunners; s++) { latestServer = s; var server = servers[s]; var serverHostUrl = getHostFromServerUrl(server.url); if (!attemptedServers[serverHostUrl]) { attemptedServers[serverHostUrl] = true; tryNext(s); } } } startParallelSearches(PARALLEL_SEARCHES); } function getHostFromServerUrl(serverUrl) { // server.url looks like: https://vfs-gce-ae-09-2.c9.io or https://vfs.c9.dev/vfs we're grabbing the base url of the host (without the -2) var serverHostUrl = serverUrl.replace(/^(https:..[^.]+-\d+)(-\d+)(.*)/, "$1$3"); if (serverHostUrl) { return serverHostUrl; } return serverUrl; } function onProtocolChange(callback) { // I'm keeping this vague because we don't want users to blame // a "cloud9 update" for losing work deleteOldVfs(); metrics.increment("vfs.failed.protocol_mismatch", 1, true); return callback(fatalError("Protocol change detected", "reload")); } function shuffleServers(version, servers) { // If a strict region is specified, only use that region servers = servers.slice(); if (strictRegion) { servers = servers.filter(function(s) { return s.region === strictRegion; }); } // Never use staging servers if we're not on staging, // even though they appear in the production VFS registry var isBetaClient = region === "beta"; servers = servers.filter(function(s) { var isBetaServer = s.region === "beta"; return isBetaServer === isBetaClient; }); servers = servers.filter(function(s) { return ignoreProtocolVersion || s.version == undefined || s.version == version; }); return servers.sort(function(a, b) { if (a.region == b.region) { if (a.packageVersion == b.packageVersion) { if (a.load < b.load) { return -1; } else { return 1; } } else if (a.packageVersion > b.packageVersion) { return -1; } else { return 1; } } else if (a.region == region) { return -1; } else if (b.region == region) { return 1; } else { return 0; } }); } function rememberVfs(server, vfsid) { var vfs = { url: server.url, region: server.region, pid: options.pid, vfsid: server.url + "/" + options.pid + "/" + vfsid, readonly: options.readonly }; var data = JSON.stringify(vfs); var oldData = lastVfs || window.sessionStorage.getItem("vfsid"); if (oldData && oldData !== data) deleteOldVfs(); lastVfs = data; return vfs; } function recallVfs() { var vfs; try { vfs = JSON.parse(lastVfs || window.sessionStorage.getItem("vfsid") || null); if (!lastVfs && vfs) { window.sessionStorage.removeItem("vfsid"); lastVfs = JSON.stringify(vfs); } } catch (e) {} if (!vfs) return null; if (vfs.pid !== options.pid || vfs.readonly != options.readonly) { deleteOldVfs(); return null; } return vfs; } function deleteOldVfs() { var vfs; try { vfs = JSON.parse(lastVfs || window.sessionStorage.getItem("vfsid")); } catch (e) {} window.sessionStorage.removeItem("vfsid"); lastVfs = null; if (!vfs) return; auth.request(vfs.vfsid, { method: "DELETE", headers: { Accept: "application/json" } }, function(err) { if (err) console.error(vfs.vfsid, "deleted", err); }); } function fatalError(msg, action) { var err = new Error(msg); err.fatal = true; err.action = action || "reload"; return err; } function saveToSessionStorage() { try { window.sessionStorage.setItem("vfsid", lastVfs); } catch(e) { // could throw a quota exception } } plugin.on("load", function() { window.addEventListener("unload", saveToSessionStorage); }); plugin.on("unload", function() { window.removeEventListener("unload", saveToSessionStorage); }); /***** Register and define API *****/ /** **/ plugin.freezePublicAPI({ /** * Returns the URLs for the home and project REST API and the socket */ get: getVfsEndpoint, /** * Checks if the client has a network connection */ isOnline: isOnline, /** * */ clearCache: deleteOldVfs, /** * Checks if the current VFS server is still alive */ isServerAlive: isServerAlive }); register(null, { "vfs.endpoint": plugin }); } });