├── 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 | 
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 | 
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 |
27 |
28 |
29 |
30 | | Port |
31 | Public |
32 | Usable |
33 | Used by |
34 | Controller |
35 |
36 |
37 |
38 | <% ports.forEach((port) => { %>
39 |
40 | | <%= port.number %> |
41 | <%= 'unknown' %> |
42 | <%= (port.usableCache) ? 'Yes' : 'No' %> |
43 | <%= (port.project) ? port.project.name :
44 | ((!port.usableCache) ? '(unknown)' : '(is free)') %> |
45 |
46 |
56 | |
57 |
58 | <% }); %>
59 |
60 |
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 |
31 |
32 |
33 |
34 | | Name |
35 | Path |
36 | Port |
37 | Controller |
38 |
39 |
40 |
41 | <% projects.forEach((project) => { %>
42 |
43 | | <%- (project.isActive) ? project.name.link(url + ':' + project.port.number ) : project.name %> |
44 | <%= project.path %> |
45 | <%= (project.isActive) ? project.port.number : '' %> |
46 |
47 | <% if(!project.isActive) { %>
48 |
54 | <% } else { %>
55 |
61 | <% } %>
62 |
63 |
64 |
65 |
66 |
67 |
72 | |
73 |
74 | <% }); %>
75 |
76 |
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 |
--------------------------------------------------------------------------------