├── screenshot.png ├── .gitignore ├── assets ├── setting.js ├── index.js └── lib.js ├── tools └── init.js ├── package.json ├── views ├── nav.ejs ├── head.ejs ├── setting.ejs └── index.ejs ├── config └── default.json5 ├── LICENSE ├── README.zh-tw.md ├── README.md ├── Port.js ├── Project.js └── app.js /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ALiangLiang/cloud9-launcher/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules 3 | npm-debug*.log 4 | 5 | # pm2 6 | ecosystem.config.js 7 | 8 | # system 9 | tmux-* 10 | 11 | .c9 -------------------------------------------------------------------------------- /assets/setting.js: -------------------------------------------------------------------------------- 1 | /* global addPort */ 2 | const portEle = document.getElementById('port'); 3 | 4 | document.getElementById('add-port').addEventListener('click', () => { 5 | addPort(portEle.value); 6 | }); 7 | -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | /* global addProject */ 2 | const 3 | projectName = document.getElementById('project-name'), 4 | projectPath = document.getElementById('project-path'); 5 | 6 | document.getElementById('add-workspace').onclick = () => { 7 | addProject(projectName.value, projectPath.value) 8 | } 9 | -------------------------------------------------------------------------------- /tools/init.js: -------------------------------------------------------------------------------- 1 | require('json5/lib/require'); 2 | 3 | const 4 | Configstore = require('configstore'), 5 | defaultConfig = require('./../config/default.json5'), 6 | config = new Configstore('cloud9-launcher', defaultConfig); 7 | 8 | console.log('default configure: ', config.all); 9 | console.log('Please change this in ~/.config/configstore/cloud9-launcher.json'); 10 | 11 | process.exit(); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud9-launcher", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "init": "node tools/init.js", 8 | "start": "node app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "c9" 13 | ], 14 | "author": "ALiangLiang", 15 | "license": "MIT", 16 | "dependencies": { 17 | "body-parser": "^1.17.1", 18 | "configstore": "^3.1.0", 19 | "ejs": "^2.5.6", 20 | "express": "^4.15.3", 21 | "express-basic-auth": "^1.0.2", 22 | "get-port": "^3.1.0", 23 | "json5": "^0.5.1", 24 | "portfinder": "^1.0.13" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /views/nav.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/default.json5: -------------------------------------------------------------------------------- 1 | // Recommend you copy this file into "local-0.json5". And replace the configs. 2 | { 3 | ssl: { // ssl option. If you don't need https, remove this property. 4 | key: "", // key absolute path 5 | cert: "", 6 | ca: "" 7 | }, 8 | c9Path: "/home/c9sdk", // The absolute path of c9sdk. 9 | nodePath: "node", // The absolute path of node. Or you can directed use "node". 10 | account: "account", // Account of basic authentication. 11 | password: "password", // Password of basic authentication. 12 | port: 8080, // Server post. 13 | ports: [ 8081, 8082 ], // Free posts, Be sure these ports are public. 14 | autoFindPort: true, // (Useless now) Auto find a random free public port. 15 | c9Host: 'c9.foobar.com', // The host of cloud9 instances. 16 | projects: [{ 17 | name: 'project1', // Used to display. 18 | path: '/home/project1' // The absolute path of the project workspace. 19 | }], 20 | limitTime: 5000 // Used to limit time of launching c9 process. 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 阿良良 ALiangLiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /views/head.ejs: -------------------------------------------------------------------------------- 1 | ☁️Cloud9 Launcher🚀 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.zh-tw.md: -------------------------------------------------------------------------------- 1 | ## ️☁️Cloud9 Launcher🚀 (開發中) 2 | 3 | 用圖形化介面管理你所有的 Cloud9 IDE,省下使用 CLI 的麻煩。 4 | 5 | **更新(2017/5/23):** 6 | **我現在正在研究 [c9 core](https://github.com/c9/core) 中,目的是要找出一個,能夠像 [cloud9](https://c9.io/) 一樣只使用一個 port 便可以管理不同 workspace 的 c9**,目前是稍微有點頭緒,大概是建立一個 vps-server,並且讓使用者使用不同設定給「ide.html」,裡面包含有不同的 workspace 位置。 7 | 8 | 其他語言的 README:[English](README.md), [正體中文](README.zh-tw.md) 9 | 10 | ![截圖](https://raw.githubusercontent.com/ALiangLiang/cloud9-launcher/master/screenshot.png) 11 | 12 | ### 特色 13 | 14 | - 管理你的 Cloud9 程序。 15 | - 圖形化介面 16 | - 使用 basic authorization。 17 | - 不需要資料庫來儲存設定。 18 | 19 | ### 安裝 20 | 21 | #### Cloud9 IDE 22 | 23 | 請參考 [c9/core](https://github.com/c9/core) 24 | ```sh 25 | git clone git://github.com/c9/core.git c9sdk 26 | cd c9sdk 27 | scripts/install-sdk.sh 28 | ``` 29 | 真的很好安裝啦,不騙你。 30 | 31 | 如果你想要讓你的 c9 ide 使用 https,你可以參考這篇 [issue](https://github.com/c9/core/issues/229)。 32 | 33 | #### Cloud9 Launcher 34 | 35 | ```sh 36 | git clone https://github.com/ALiangLiang/cloud9-launcher.git 37 | cd cloud9-launcher 38 | npm run init 39 | vim ~/.config/configstore/cloud9-launcher.json # 填寫設定 40 | npm start 41 | ``` 42 | 43 | ### TODO 44 | 45 | - 自動掃描可以讓外部連入且沒在使用的 port。 46 | - 可以對 node 或 cloud9 使用參數 47 | - 凍結 cloud9 程序。 48 | - 在圖形化介面上監控程序 49 | 50 | ### 關於 51 | 52 | 這個專案是啟發自 [c9ui](https://github.com/orditeck/c9ui)。另外因為我不是很精通英文,所以這個專案中的英文如果有錯,麻煩幫我修一下順便 PR 上來,拜託各位了QQ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ️☁️Cloud9 Launcher🚀 (In dev) 2 | 3 | It's a Nodejs application allows you manage your Cloud9 IDE workflows on your own server without the need of a terminal. 4 | 5 | **UPDATE (2017/5/23):** 6 | **For now, I'm still researching [c9 core](https://github.com/c9/core) and find a method to generate multiple workspace in one port.** I guess the key is about "plugins". Create a vfs-server and use various settings to "ide.html" which contain difference workspace path. 7 | 8 | Read this in other languages: [English](README.md), [正體中文](README.zh-tw.md) 9 | 10 | ![Screenshot](https://raw.githubusercontent.com/ALiangLiang/cloud9-launcher/master/screenshot.png) 11 | 12 | ### Feature 13 | 14 | - Manage your cloud9 processes. 15 | - GUI 16 | - Use basic authorization 17 | - Don't need database to save setting. 18 | 19 | ### Install 20 | 21 | #### Cloud9 IDE 22 | 23 | reference [c9/core](https://github.com/c9/core) 24 | ```sh 25 | git clone git://github.com/c9/core.git c9sdk 26 | cd c9sdk 27 | scripts/install-sdk.sh 28 | ``` 29 | This is simple, isn't it? 30 | 31 | If you want your c9 use https, you can refer this [issue](https://github.com/c9/core/issues/229). 32 | 33 | #### Cloud9 Launcher 34 | 35 | ```sh 36 | git clone https://github.com/ALiangLiang/cloud9-launcher.git 37 | cd cloud9-launcher 38 | npm run init 39 | vim ~/.config/configstore/cloud9-launcher.json # fill this configure file 40 | npm start 41 | ``` 42 | 43 | ### Update 44 | 45 | ```sh 46 | git pull 47 | ``` 48 | 49 | ### TODO 50 | 51 | - Find a method to generate multiple workspace with one port. 52 | - Auto find a port which allow inbound connection. 53 | - Add arguments setting to node or c9. 54 | - Pause c9 process. 55 | - Monitor processes on client side. 56 | 57 | ### About 58 | 59 | This project is reference [c9ui](https://github.com/orditeck/c9ui). Coz I don't want to prepare environment about PHP 😛. BTW, I am not a English-speaker. So if you discover some grammar error, please help me fixed and PR. lol -------------------------------------------------------------------------------- /Port.js: -------------------------------------------------------------------------------- 1 | const portfinder = require('portfinder'); 2 | 3 | class Port { 4 | constructor(port, project) { 5 | this._port = port; 6 | this._project = project; 7 | this._usableCache = void 0; 8 | 9 | this._usable().then((usable) => this._usableCache = usable); 10 | } 11 | 12 | free() { 13 | this._project = void 0; 14 | this._updateUsableCache(); 15 | } 16 | 17 | get number() { 18 | return this._port; 19 | } 20 | 21 | get project() { 22 | return this._project; 23 | } 24 | 25 | set project(project) { 26 | // Two way assign 27 | if (!project.port) { 28 | this._project = project; 29 | project.port = this; 30 | } 31 | this._updateUsableCache(); 32 | } 33 | 34 | /** 35 | * Return lastest port check result. 36 | * @return {Boolean} 37 | */ 38 | get usableCache() { 39 | return this._usableCache; 40 | } 41 | 42 | /** 43 | * If this port is non-occuppied and no project use this port. 44 | * @return {Promise.} 45 | */ 46 | get usable() { 47 | return this._usable(); 48 | } 49 | 50 | /** 51 | * If this port is non-occuppied. 52 | * @return {Promise.} 53 | */ 54 | get isOccuppied() { 55 | return this._isOccuppied(); 56 | } 57 | 58 | async _usable() { 59 | return this._usableCache = !(await this._isOccuppied()) && (!this._project); 60 | } 61 | 62 | async _isOccuppied() { 63 | const 64 | portNumber = await portfinder.getPortPromise({ 65 | port: this._port 66 | }), 67 | isOccuppied = (portNumber !== this._port); 68 | 69 | // update usableCache 70 | this._usableCache = !isOccuppied && !this._project; 71 | 72 | return isOccuppied; 73 | } 74 | 75 | /** 76 | * @private 77 | * @return {Promise.} 78 | */ 79 | async _updateUsableCache() { 80 | return this._usableCache = !(await this._isOccuppied()) && (!this._project); 81 | } 82 | } 83 | 84 | module.exports = Port; 85 | -------------------------------------------------------------------------------- /views/setting.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% include head %> 5 | 6 | 7 | 8 | <% include nav %> 9 | 10 |
11 |
12 |
13 |

Free Ports

14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <% ports.forEach((port) => { %> 39 | 40 | 41 | 42 | 43 | 45 | 57 | 58 | <% }); %> 59 | 60 |
PortPublicUsableUsed byController
<%= port.number %><%= 'unknown' %><%= (port.usableCache) ? 'Yes' : 'No' %><%= (port.project) ? port.project.name : 44 | ((!port.usableCache) ? '(unknown)' : '(is free)') %> 46 |
47 | 55 |
56 |
61 |
62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /assets/lib.js: -------------------------------------------------------------------------------- 1 | /* global URL */ 2 | /* global sweetAlert */ 3 | function request(api, method, body) { 4 | if (window.XMLHttpRequest) 5 | return new Promise((resolve, reject) => { 6 | const xhr = new window.XMLHttpRequest(); 7 | xhr.open(method, '/api/' + api, true); 8 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 9 | xhr.responseType = 'json'; 10 | xhr.onload = function() { 11 | if (xhr.status > 399 && xhr.status < 600) 12 | return reject((xhr.response.error) ? xhr.response.error : xhr.response); 13 | resolve(xhr.response); 14 | }; 15 | xhr.onabort = xhr.onerror = function(e) { 16 | reject(e); 17 | }; 18 | xhr.send(JSON.stringify(body)); 19 | }); 20 | else 21 | sweetAlert('Error', 'Your browser doesn\' support XMLHttpRequest. Please update your browser to latest version.', 'error'); 22 | } 23 | 24 | function launchProject(projectName) { 25 | return request('launch', 'post', { 26 | target: projectName 27 | }) 28 | .then((result) => { 29 | const 30 | hostname = new URL(document.location.href).hostname, 31 | port = result.succsess.data.port, 32 | url = `https://${hostname}:${port}/ide.html`; 33 | window.open(url, '_blank'); 34 | }) 35 | .then(() => document.location.reload()) 36 | .catch((err) => { 37 | console.log(err); 38 | sweetAlert('Error', err.message, 'error'); 39 | }); 40 | } 41 | 42 | function stopProject(projectName) { 43 | return request('launch', 'delete', { 44 | target: projectName 45 | }) 46 | .then(() => document.location.reload()); 47 | } 48 | 49 | function addProject(projectName, path) { 50 | return request('project', 'post', { 51 | name: projectName, 52 | path: path 53 | }) 54 | .then(() => document.location.reload()); 55 | } 56 | 57 | function editProject(projectName, path) { 58 | return request('project', 'put', { 59 | name: projectName, 60 | path: path 61 | }) 62 | .then(() => document.location.reload()); 63 | } 64 | 65 | function removeProject(projectName) { 66 | return request('project', 'delete', { 67 | name: projectName 68 | }) 69 | .then(() => document.location.reload()); 70 | } 71 | 72 | function addPort(port) { 73 | return request('Port', 'post', { 74 | number: port 75 | }) 76 | .then(() => document.location.reload()); 77 | } 78 | 79 | function removePort(port) { 80 | return request('Port', 'delete', { 81 | number: port 82 | }) 83 | .then(() => document.location.reload()); 84 | } 85 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% include head %> 5 | 6 | 7 | 8 | <% include nav %> 9 | 10 |
11 |
12 |
13 |

Workflows

14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | <% projects.forEach((project) => { %> 42 | 43 | 44 | 45 | 46 | 73 | 74 | <% }); %> 75 | 76 |
NamePathPortController
<%- (project.isActive) ? project.name.link(url + ':' + project.port.number ) : project.name %><%= project.path %><%= (project.isActive) ? project.port.number : '' %> 47 | <% if(!project.isActive) { %> 48 | 54 | <% } else { %> 55 | 61 | <% } %> 62 | 63 | 64 | 65 | 66 | 67 | 72 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Project.js: -------------------------------------------------------------------------------- 1 | const 2 | spawn = require('child_process').spawn, 3 | proc = cmd(), 4 | Configstore = require('configstore'), 5 | pkg = require('./package.json'), 6 | defaultConfig = require('./config/default.json5'), 7 | config = new Configstore(pkg.name, defaultConfig), 8 | TIMEOUT_TIME = config.get('limitTime') || 5000; // limit runC9 time. 9 | 10 | class Project { 11 | constructor(options) { 12 | this._name = options.name; 13 | this._path = options.path; 14 | this._port = options.port; 15 | this._c9 = options.c9; 16 | } 17 | 18 | start(port) { 19 | return runC9(port, this); 20 | } 21 | 22 | stop() { 23 | const c9 = this._c9; 24 | if (c9 && c9.pid && !c9.killed) { 25 | try { 26 | process.kill(c9.pid); 27 | } catch (err) { 28 | return false; 29 | } 30 | this._port.free(); 31 | this._port = void 0; 32 | return true; 33 | } else 34 | return false; 35 | } 36 | 37 | get isActive() { 38 | return !!this._port; 39 | } 40 | 41 | get name() { 42 | return this._name; 43 | } 44 | 45 | set path(path) { 46 | this._path = path; 47 | } 48 | 49 | get path() { 50 | return this._path; 51 | } 52 | 53 | set port(port) { 54 | // Two way assign 55 | if (!port._project) { 56 | this._port = port; 57 | port.project = this; 58 | } else 59 | console.log('Ths port is not free. Used by project ' + port.project.name); 60 | } 61 | 62 | get port() { 63 | return this._port; 64 | } 65 | 66 | get isActive() { 67 | return !!this._port; 68 | } 69 | 70 | set c9(c9) { 71 | this._c9 = c9; 72 | } 73 | 74 | get c9() { 75 | return this._c9; 76 | } 77 | } 78 | 79 | module.exports = Project; 80 | 81 | // Spawn a C9 process. 82 | function runC9(port, project) { 83 | return new Promise((resolve, reject) => { 84 | // If over time we set, throw a error. 85 | setTimeout(() => { 86 | /* TODO: need to check again is this cloud9 up. */ 87 | reject(new Error('TIMEOUT')); 88 | }, TIMEOUT_TIME); 89 | 90 | const workspace = project.path; 91 | if (!workspace) 92 | throw new Error('Cannot found workspace.'); 93 | 94 | const runPort = port.number; 95 | if (!runPort) 96 | throw new Error('Cannot found port number.'); 97 | 98 | const 99 | script = `node ${config.get('c9Path')}/server.js -p ${runPort} -w ${workspace} -l 0.0.0.0 -a ${config.get('account')}:${config.get('password')}`, 100 | c9 = proc.run(script, { 101 | env: process.env 102 | }, 103 | function() { 104 | env: process.env; 105 | }, 106 | function(stderr, stdout, code, signal) { 107 | console.log('c9s died with', code, signal); 108 | }); 109 | 110 | c9.stdout.on('data', function(data) { 111 | const stdout = data.toString(); 112 | if (stdout.indexOf('Cloud9 is up and running') !== -1) { 113 | project.c9 = c9; 114 | project.port = port; 115 | resolve(runPort); 116 | } 117 | }); 118 | }); 119 | } 120 | 121 | function cmd() { 122 | var command = {}; 123 | command.run = function(commandLine, _options, callback) { 124 | var options, 125 | __undefined__; 126 | if (!callback && typeof _options === "function") { 127 | callback = _options; 128 | options = __undefined__; 129 | } else if (typeof _options === "object") { 130 | options = _options; 131 | } 132 | var args = commandLine.split(" "); 133 | var cmd = args[0]; 134 | args.shift(); 135 | var oneoff = spawn(cmd, args, options); 136 | var stderr, 137 | stdout; 138 | oneoff.stdout.on('data', function(data) { 139 | stdout = data.toString(); 140 | // console.log("stdout", data.toString()); 141 | }); 142 | oneoff.stderr.on('data', function(data) { 143 | stderr = data.toString(); 144 | console.log("stderr", data.toString()); 145 | }); 146 | oneoff.on('exit', function(code, signal) { 147 | oneoff.killed = true; 148 | if (typeof callback === "function") 149 | callback(stderr, stdout, code, signal); 150 | }); 151 | return oneoff; 152 | }; 153 | return command; 154 | } 155 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('json5/lib/require'); // Override "require" function. Make "require" can load json5 format. 2 | 3 | const 4 | fs = require('fs'), 5 | URL = require('url'), 6 | express = require('express'), 7 | bodyParser = require('body-parser'), 8 | Configstore = require('configstore'), 9 | basicAuth = require('express-basic-auth'), 10 | 11 | Project = require('./Project'), 12 | Port = require('./Port'), 13 | defaultConfig = require('./config/default.json5'), 14 | pkg = require('./package.json'), 15 | 16 | config = new Configstore(pkg.name, defaultConfig), 17 | app = express(), 18 | 19 | isHttps = !!config.get('ssl') || false, 20 | TIMEOUT_TIME = config.get('limitTime') || 5000, // limit runC9 time. 21 | ports = [], 22 | projects = [], 23 | c9s = []; 24 | 25 | /* Initial config */ 26 | const defaultPorts = config.get('ports') || []; 27 | defaultPorts.forEach((portNumber) => ports.push(new Port(portNumber))); 28 | const defaultProject = config.get('projects') || []; 29 | defaultProject.forEach((project) => projects.push(new Project(project))); 30 | Project.TIMEOUT_TIME = TIMEOUT_TIME; 31 | 32 | app.set('view engine', 'ejs'); 33 | 34 | /* Basic auth */ 35 | const authConfig = {}; 36 | authConfig[config.get('account')] = config.get('password'); 37 | app.use(basicAuth({ 38 | users: authConfig, 39 | challenge: true, 40 | realm: 'Cloud9 Launcher' 41 | })); 42 | 43 | /* Static */ 44 | app.use('/assets', express.static('assets')); 45 | 46 | app.use(bodyParser.json()); 47 | app.use(bodyParser.urlencoded({ 48 | extended: false 49 | })); 50 | 51 | /* Router */ 52 | app.get('/', function(req, res) { 53 | res.render('index', { 54 | projects: projects, 55 | url: `http${(isHttps)?'s':''}:\/\/${config.get('c9Host')}` 56 | }); 57 | }); 58 | 59 | app.get('/setting', function(req, res) { 60 | res.render('setting', { 61 | ports: ports 62 | }); 63 | }); 64 | 65 | const api = express.Router(); 66 | 67 | api.route('/launch') // used to open or close process 68 | .post(async function(req, res) { 69 | const targetProjectName = req.body.target; 70 | const project = getProject(targetProjectName); 71 | 72 | if (!project) 73 | return res.status(404).send({ 74 | error: { 75 | message: 'Cannot find the project by name.' 76 | } 77 | }); 78 | 79 | // If the project is active, exit. 80 | if (project.isActive) 81 | return res.status(403).send({ 82 | error: { 83 | message: 'The project is running on port ' + project.port.number + '.', 84 | data: { 85 | port: project.port.number 86 | } 87 | } 88 | }); 89 | 90 | // Find a free port and assign to target project. 91 | const port = await getFreePort(); 92 | 93 | if (!port) 94 | return res.status(404).send({ 95 | error: { 96 | message: 'No free port. Please add or release some free ports.' 97 | } 98 | }); 99 | 100 | try { 101 | const runPortNumber = await project.start(port); 102 | console.info(`Project "${project.name}" is running. Use port ${runPortNumber}.`); 103 | res.send({ 104 | succsess: { 105 | data: { 106 | port: runPortNumber 107 | } 108 | } 109 | }); 110 | } catch (err) { 111 | console.error(err); 112 | return res.status(403).send(err); 113 | } 114 | }) 115 | .delete(function(req, res) { 116 | const targetProjectName = req.body.target; 117 | const project = getProject(targetProjectName); 118 | 119 | if (!project) 120 | return res.status(404).send({ 121 | error: { 122 | message: 'Cannot find the project by name.' 123 | } 124 | }); 125 | 126 | // If the project is active, exit. 127 | if (!project.isActive) 128 | return res.status(403).send({ 129 | error: { 130 | message: 'The project is not running.' 131 | } 132 | }); 133 | 134 | const 135 | usedPort = project.port.number, 136 | result = project.stop(); 137 | console.info(`Project "${project.name}" is shutdown. Port ${usedPort} is free.`); 138 | if (result) 139 | return res.send({ 140 | success: true 141 | }); 142 | else 143 | return res.status(403).send({ 144 | error: { 145 | message: 'The project is not running.' 146 | } 147 | }); 148 | }); 149 | 150 | api.route('/project') // used to add, edit or delete project setting. 151 | .post(function(req, res) { 152 | const 153 | projectName = req.body.name, 154 | path = req.body.path; 155 | if (!projectName || !path) 156 | return res.status(403).send({ 157 | error: { 158 | message: 'Lack of arguments.' 159 | } 160 | }); 161 | 162 | const project = new Project({ 163 | name: projectName, 164 | path: path 165 | }); 166 | 167 | projects.push(project); 168 | 169 | updateProjectsConfig(); 170 | 171 | return res.send({ 172 | success: true 173 | }); 174 | }) 175 | .put(function(req, res) { 176 | const 177 | targetProjectName = req.body.name, 178 | path = req.body.path; 179 | 180 | const project = getProject(targetProjectName); 181 | if (!project) 182 | return res.status(403).send({ 183 | error: { 184 | message: 'Cannot find the project by name' 185 | } 186 | }); 187 | project.path = path; 188 | 189 | updateProjectsConfig(); 190 | 191 | if (project.isActive) 192 | return res.send({ 193 | success: { 194 | message: 'Success but it\'s effective in next launch.' 195 | } 196 | }); 197 | else 198 | return res.send({ 199 | success: true 200 | }); 201 | }) 202 | .delete(function(req, res) { 203 | const targetProjectName = req.body.name; 204 | 205 | const project = popProject(targetProjectName); 206 | if (!project) 207 | return res.status(404).send({ 208 | error: { 209 | message: 'Cannot find the project by name' 210 | } 211 | }); 212 | 213 | updateProjectsConfig(); 214 | 215 | res.send({ 216 | success: true 217 | }); 218 | }); 219 | 220 | api.route('/port') // used to add or delete port setting 221 | .post(async function(req, res) { 222 | let portNumber = Number(req.body.number); 223 | 224 | if (!portNumber || portNumber === 0) 225 | return res.status(403).send({ 226 | error: { 227 | message: 'Lack of port number.' 228 | } 229 | }); 230 | 231 | const port = new Port(portNumber); 232 | 233 | if (ports.find((portNum) => portNum === portNumber)) 234 | return res.status(403).send({ 235 | error: { 236 | message: 'Already has same port in ports list.' 237 | } 238 | }); 239 | 240 | ports.push(port); 241 | 242 | updatePortsConfig(); 243 | 244 | return res.send({ 245 | success: true 246 | }); 247 | }) 248 | .delete(function(req, res) { 249 | const portNumber = req.body.number; 250 | 251 | const port = popPort(portNumber); 252 | if (!port) 253 | return res.status(404).send({ 254 | error: { 255 | message: 'Cannot find the port by port number.' 256 | } 257 | }); 258 | 259 | updatePortsConfig(); 260 | 261 | res.send({ 262 | success: true 263 | }); 264 | }); 265 | 266 | app.use('/api', api); 267 | 268 | // Launch UI server 269 | const 270 | sslConfig = config.get('ssl'), 271 | port = config.get('port') || 8080; 272 | if (isHttps) { 273 | const option = {}; 274 | for (let k in sslConfig) { 275 | if (fs.existsSync(sslConfig[k])) 276 | option[k] = fs.readFileSync(sslConfig[k]); 277 | else 278 | console.warn('Config: property of ' + k + ' is not a correct path.'); 279 | } 280 | 281 | require('https') 282 | .createServer(option, app) 283 | .listen(port, function() { 284 | console.info('UI server listening on port ' + port); 285 | }); 286 | } else 287 | app.listen(port, function() { 288 | console.info('UI server listening on port ' + port); 289 | }); 290 | 291 | process.on('exit', function() { 292 | c9s.forEach((c9) => { 293 | if (c9 && c9.pid && !c9.killed) { 294 | process.kill(c9.pid); 295 | console.info('c9 killed'); 296 | } 297 | }); 298 | }); 299 | 300 | // Return a free port. 301 | /* TODO: change to port.usable. */ 302 | function getFreePort() { 303 | return ports.find((port) => port.usableCache); 304 | } 305 | 306 | // Use project name to find out project. 307 | function getProject(name) { 308 | return projects.find((project) => project.name === name); 309 | } 310 | 311 | function popProject(name) { 312 | const index = projects.findIndex((project) => project.name === name); 313 | // If cannot find the project, return false. 314 | if (!index) 315 | return false; 316 | 317 | // Remove project from array projects, and return this project. 318 | const project = projects[index]; 319 | projects.splice(index, 1); 320 | return project; 321 | } 322 | 323 | function popPort(number) { 324 | const index = ports.findIndex((port) => port.number === number); 325 | // If cannot find the port, return false. 326 | if (!index) 327 | return false; 328 | 329 | // Remove port from array ports, and return this port. 330 | const port = ports[index]; 331 | ports.splice(index, 1); 332 | return port; 333 | } 334 | 335 | // save projects to config file. 336 | function updateProjectsConfig() { 337 | config.set('projects', projects.map((project) => { 338 | return { 339 | name: project.name, 340 | path: project.path 341 | }; 342 | })); 343 | } 344 | 345 | // save ports to config file. 346 | function updatePortsConfig() { 347 | config.set('ports', ports.map((port) => port.number)); 348 | } 349 | --------------------------------------------------------------------------------