mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-28 06:49:51 +00:00
Merge pull request #294 from GNS3/local-server-runner
Local server runner, Ref: #254, Fix wrong toaster position for error/success, Fixes: #291
This commit is contained in:
commit
c17f115085
223
local-server.js
Normal file
223
local-server.js
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
const { spawn } = require('child_process');
|
||||||
|
const kill = require('tree-kill');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { ipcMain } = require('electron')
|
||||||
|
|
||||||
|
const isWin = /^win/.test(process.platform);
|
||||||
|
|
||||||
|
let runningServers = {};
|
||||||
|
|
||||||
|
exports.getLocalServerPath = async () => {
|
||||||
|
const distDirectory = path.join(__dirname, 'dist');
|
||||||
|
if (!fs.existsSync(distDirectory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(distDirectory);
|
||||||
|
|
||||||
|
let serverPath = null;
|
||||||
|
|
||||||
|
files.forEach((directory) => {
|
||||||
|
if(directory.startsWith('exe.')) {
|
||||||
|
if (isWin) {
|
||||||
|
serverPath = path.join(__dirname, 'dist', directory, 'gns3server.exe');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
serverPath = path.join(__dirname, 'dist', directory, 'gns3server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(serverPath !== null && fs.existsSync(serverPath)) {
|
||||||
|
return serverPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.startLocalServer = async (server) => {
|
||||||
|
return await run(server, {
|
||||||
|
logStdout: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.stopLocalServer = async (server) => {
|
||||||
|
return await stop(server.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getRunningServers = () => {
|
||||||
|
return Object.keys(runningServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.stopAllLocalServers = async () => {
|
||||||
|
return await stopAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerArguments(server, overrides) {
|
||||||
|
let serverArguments = [];
|
||||||
|
return serverArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChannelForServer(server) {
|
||||||
|
return `local-server-run-${server.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyStatus(status) {
|
||||||
|
ipcMain.emit('local-server-status-events', status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterOutput(line) {
|
||||||
|
const index = line.search('CRITICAL');
|
||||||
|
if(index > -1) {
|
||||||
|
return {
|
||||||
|
isCritical: true,
|
||||||
|
errorMessage: line.substr(index)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isCritical: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopAll() {
|
||||||
|
for(var serverName in runningServers) {
|
||||||
|
let result, error = await stop(serverName);
|
||||||
|
}
|
||||||
|
console.log(`Stopped all servers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop(serverName) {
|
||||||
|
let pid = undefined;
|
||||||
|
|
||||||
|
const runningServer = runningServers[serverName];
|
||||||
|
|
||||||
|
if(runningServer !== undefined && runningServer.process) {
|
||||||
|
pid = runningServer.process.pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Stopping '${serverName}' with PID='${pid}'`);
|
||||||
|
|
||||||
|
const stopped = new Promise((resolve, reject) => {
|
||||||
|
if(pid === undefined) {
|
||||||
|
resolve(`Server '${serverName} is already stopped`);
|
||||||
|
delete runningServers[serverName];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
kill(pid, (error) => {
|
||||||
|
if(error) {
|
||||||
|
console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
delete runningServers[serverName];
|
||||||
|
console.log(`Stopped '${serverName}' with PID='${pid}'`);
|
||||||
|
resolve(`Stopped '${serverName}' with PID='${pid}'`);
|
||||||
|
|
||||||
|
notifyStatus({
|
||||||
|
serverName: serverName,
|
||||||
|
status: 'stopped',
|
||||||
|
message: `Server '${serverName}' stopped'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(server, options) {
|
||||||
|
if(!options) {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const logStdout = options.logStdout || false;
|
||||||
|
const logSterr = options.logSterr || false;
|
||||||
|
|
||||||
|
console.log(`Running '${server.path}'`);
|
||||||
|
|
||||||
|
let serverProcess = spawn(server.path, getServerArguments(server));
|
||||||
|
|
||||||
|
notifyStatus({
|
||||||
|
serverName: server.name,
|
||||||
|
status: 'started',
|
||||||
|
message: `Server '${server.name}' started'`
|
||||||
|
});
|
||||||
|
|
||||||
|
runningServers[server.name] = {
|
||||||
|
process: serverProcess
|
||||||
|
};
|
||||||
|
|
||||||
|
serverProcess.stdout.on('data', function(data) {
|
||||||
|
const line = data.toString();
|
||||||
|
const { isCritical, errorMessage } = filterOutput(line);
|
||||||
|
if(isCritical) {
|
||||||
|
notifyStatus({
|
||||||
|
serverName: server.name,
|
||||||
|
status: 'stderr',
|
||||||
|
message: `Server reported error: '${errorMessage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(logStdout) {
|
||||||
|
console.log(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.stderr.on('data', function(data) {
|
||||||
|
if(logSterr) {
|
||||||
|
console.log(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.on('exit', (code, signal) => {
|
||||||
|
notifyStatus({
|
||||||
|
serverName: server.name,
|
||||||
|
status: 'errored',
|
||||||
|
message: `Server '${server.name}' has exited with status='${code}'`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.on('error', (err) => {
|
||||||
|
notifyStatus({
|
||||||
|
serverName: server.name,
|
||||||
|
status: 'errored',
|
||||||
|
message: `Server errored: '${errorMessage}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await run({
|
||||||
|
name: 'my-local',
|
||||||
|
path: 'c:\\Program Files\\GNS3\\gns3server.EXE',
|
||||||
|
port: 3080
|
||||||
|
}, {
|
||||||
|
logStdout: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on('local-server-run', async function (event, server) {
|
||||||
|
const responseChannel = getChannelForServer();
|
||||||
|
await run(server);
|
||||||
|
event.sender.send(responseChannel, {
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
process.on('SIGINT', function() {
|
||||||
|
console.log("Caught interrupt signal");
|
||||||
|
stopAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.log(`UnhandledRejection occured '${reason}'`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
main();
|
||||||
|
}
|
60
main.js
60
main.js
@ -1,18 +1,13 @@
|
|||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const fs = require('fs');
|
|
||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
const BrowserWindow = electron.BrowserWindow;
|
const BrowserWindow = electron.BrowserWindow;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const yargs = require('yargs');
|
const yargs = require('yargs');
|
||||||
|
|
||||||
const { ipcMain } = require('electron')
|
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let serverProc = null;
|
|
||||||
let isWin = /^win/.test(process.platform);
|
|
||||||
let isDev = false;
|
let isDev = false;
|
||||||
|
|
||||||
const argv = yargs
|
const argv = yargs
|
||||||
@ -29,49 +24,6 @@ if (argv.e == 'dev') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const createServerProc = () => {
|
|
||||||
const directory = path.join(__dirname, 'dist');
|
|
||||||
|
|
||||||
if (!fs.existsSync(directory)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.readdir(path.join(__dirname, 'dist'), (err, files) => {
|
|
||||||
var serverPath = null;
|
|
||||||
|
|
||||||
files.forEach((filename) => {
|
|
||||||
if(filename.startsWith('exe.')) {
|
|
||||||
if (isWin) {
|
|
||||||
serverPath = path.join(__dirname, 'dist', filename, 'gns3server.exe');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
serverPath = path.join(__dirname, 'dist', filename, 'gns3server');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (serverPath == null) {
|
|
||||||
console.error('gns3server cannot be found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverPath != null) {
|
|
||||||
serverProc = require('child_process').execFile(serverPath, []);
|
|
||||||
|
|
||||||
if (serverProc != null) {
|
|
||||||
console.log('gns3server started from path: ' + serverPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const exitServerProc = () => {
|
|
||||||
if(serverProc) {
|
|
||||||
serverProc.kill();
|
|
||||||
serverProc = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createWindow () {
|
function createWindow () {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@ -107,11 +59,17 @@ function createWindow () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emitted when the window is closed.
|
// Emitted when the window is closed.
|
||||||
mainWindow.on('closed', function () {
|
mainWindow.on('closed',async function () {
|
||||||
// Dereference the window object, usually you would store windows
|
// Dereference the window object, usually you would store windows
|
||||||
// in an array if your app supports multi windows, this is the time
|
// in an array if your app supports multi windows, this is the time
|
||||||
// when you should delete the corresponding element.
|
// when you should delete the corresponding element.
|
||||||
mainWindow = null
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// forward event to renderer
|
||||||
|
electron.ipcMain.on('local-server-status-events', (event) => {
|
||||||
|
mainWindow.webContents.send('local-server-status-events', event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +94,7 @@ app.on('activate', function () {
|
|||||||
// On OS X it's common to re-create a window in the app when the
|
// On OS X it's common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
if (mainWindow === null) {
|
if (mainWindow === null) {
|
||||||
createWindow()
|
createWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -61,14 +61,14 @@
|
|||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"ng2-file-upload": "^1.3.0",
|
"ng2-file-upload": "^1.3.0",
|
||||||
"ngx-electron": "^2.0.0",
|
"ngx-electron": "^2.0.0",
|
||||||
|
"node-fetch": "^2.3.0",
|
||||||
"notosans-fontface": "^1.1.0",
|
"notosans-fontface": "^1.1.0",
|
||||||
"raven-js": "^3.27.0",
|
"raven-js": "^3.27.0",
|
||||||
"rxjs": "^6.3.3",
|
"rxjs": "^6.3.3",
|
||||||
"rxjs-compat": "^6.3.3",
|
"rxjs-compat": "^6.3.3",
|
||||||
"typeface-roboto": "^0.0.54",
|
"typeface-roboto": "^0.0.54",
|
||||||
"yargs": "^12.0.5",
|
"yargs": "^12.0.5",
|
||||||
"zone.js": "^0.8.26",
|
"zone.js": "^0.8.26"
|
||||||
"node-fetch": "^2.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.11.4",
|
"@angular-devkit/build-angular": "~0.11.4",
|
||||||
@ -97,6 +97,7 @@
|
|||||||
"prettier": "^1.15.2",
|
"prettier": "^1.15.2",
|
||||||
"protractor": "~5.4.2",
|
"protractor": "~5.4.2",
|
||||||
"replace": "^1.0.1",
|
"replace": "^1.0.1",
|
||||||
|
"tree-kill": "^1.2.1",
|
||||||
"ts-mockito": "^2.3.1",
|
"ts-mockito": "^2.3.1",
|
||||||
"ts-node": "~7.0.1",
|
"ts-node": "~7.0.1",
|
||||||
"tslint": "~5.12.0",
|
"tslint": "~5.12.0",
|
||||||
|
@ -95,6 +95,7 @@ import { TextEditorDialogComponent } from './components/project-map/drawings-edi
|
|||||||
import { InstalledSoftwareService } from './services/installed-software.service';
|
import { InstalledSoftwareService } from './services/installed-software.service';
|
||||||
import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service';
|
import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service';
|
||||||
import { PlatformService } from './services/platform.service';
|
import { PlatformService } from './services/platform.service';
|
||||||
|
import { ServerManagementService } from './services/server-management.service';
|
||||||
|
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
|
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
|
||||||
@ -197,7 +198,8 @@ if (environment.production) {
|
|||||||
ToolsService,
|
ToolsService,
|
||||||
InstalledSoftwareService,
|
InstalledSoftwareService,
|
||||||
ExternalSoftwareDefinitionService,
|
ExternalSoftwareDefinitionService,
|
||||||
PlatformService
|
PlatformService,
|
||||||
|
ServerManagementService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddServerDialogComponent,
|
AddServerDialogComponent,
|
||||||
|
@ -158,7 +158,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.nodesDataSource.changes.subscribe((nodes: Node[]) => {
|
this.nodesDataSource.changes.subscribe((nodes: Node[]) => {
|
||||||
nodes.forEach((node: Node) => {
|
nodes.forEach((node: Node) => {
|
||||||
node.symbol_url = `http://${this.server.ip}:${this.server.port}/v2/symbols/${node.symbol}/raw`;
|
node.symbol_url = `http://${this.server.host}:${this.server.port}/v2/symbols/${node.symbol}/raw`;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nodes = nodes;
|
this.nodes = nodes;
|
||||||
|
@ -87,7 +87,7 @@ describe('AddBlankProjectDialogComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
server = new Server();
|
server = new Server();
|
||||||
server.ip = 'localhost';
|
server.host = 'localhost';
|
||||||
server.port = 80;
|
server.port = 80;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ describe('ImportProjectDialogComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
server = new Server();
|
server = new Server();
|
||||||
server.ip = 'localhost';
|
server.host = 'localhost';
|
||||||
server.port = 80;
|
server.port = 80;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -138,6 +138,6 @@ export class ImportProjectDialogComponent implements OnInit {
|
|||||||
|
|
||||||
prepareUploadPath(): string {
|
prepareUploadPath(): string {
|
||||||
const projectName = this.projectNameForm.controls['projectName'].value;
|
const projectName = this.projectNameForm.controls['projectName'].value;
|
||||||
return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`;
|
return `http://${this.server.host}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
<h1 mat-dialog-title>Add server</h1>
|
<h1 mat-dialog-title>Add server</h1>
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.name" placeholder="Name" /> </mat-form-field>
|
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.name" placeholder="Name" /> </mat-form-field>
|
||||||
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.ip" placeholder="IP" /> </mat-form-field>
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-select placeholder="Location" [(value)]="server.location">
|
||||||
|
<mat-option *ngFor="let location of locations" [value]="location.key"> {{ location.name }} </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field *ngIf="server.location === 'local'">
|
||||||
|
<input matInput tabindex="1" [(ngModel)]="server.path" placeholder="Local server path" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.host" placeholder="Host" /> </mat-form-field>
|
||||||
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.port" placeholder="Port" /> </mat-form-field>
|
<mat-form-field> <input matInput tabindex="1" [(ngModel)]="server.port" placeholder="Port" /> </mat-form-field>
|
||||||
|
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<mat-card class="info" *ngIf="discoveredServer">
|
<mat-card class="info" *ngIf="discoveredServer">
|
||||||
<mat-card-content align="center">
|
<mat-card-content align="center">
|
||||||
We've discovered GNS3 server on <b>{{ discoveredServer.ip }}:{{ discoveredServer.port }}</b
|
We've discovered GNS3 server on <b>{{ discoveredServer.host }}:{{ discoveredServer.port }}</b
|
||||||
>, would you like to add to the list?
|
>, would you like to add to the list?
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
<mat-card-actions align="right">
|
<mat-card-actions align="right">
|
||||||
|
@ -55,12 +55,12 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(Observable.of(version));
|
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(Observable.of(version));
|
||||||
|
|
||||||
component.isServerAvailable('127.0.0.1', 3080).subscribe(s => {
|
component.isServerAvailable('127.0.0.1', 3080).subscribe(s => {
|
||||||
expect(s.ip).toEqual('127.0.0.1');
|
expect(s.host).toEqual('127.0.0.1');
|
||||||
expect(s.port).toEqual(3080);
|
expect(s.port).toEqual(3080);
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = '127.0.0.1';
|
server.host = '127.0.0.1';
|
||||||
server.port = 3080;
|
server.port = 3080;
|
||||||
|
|
||||||
expect(getVersionSpy).toHaveBeenCalledWith(server);
|
expect(getVersionSpy).toHaveBeenCalledWith(server);
|
||||||
@ -68,7 +68,7 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
|
|
||||||
it('should throw error once server is not available', () => {
|
it('should throw error once server is not available', () => {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = '127.0.0.1';
|
server.host = '127.0.0.1';
|
||||||
server.port = 3080;
|
server.port = 3080;
|
||||||
|
|
||||||
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(
|
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(
|
||||||
@ -96,13 +96,13 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
|
|
||||||
spyOn(component, 'isServerAvailable').and.callFake((ip, port) => {
|
spyOn(component, 'isServerAvailable').and.callFake((ip, port) => {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = ip;
|
server.host = ip;
|
||||||
server.port = port;
|
server.port = port;
|
||||||
return Observable.of(server);
|
return Observable.of(server);
|
||||||
});
|
});
|
||||||
|
|
||||||
component.discovery().subscribe(discovered => {
|
component.discovery().subscribe(discovered => {
|
||||||
expect(discovered[0].ip).toEqual('127.0.0.1');
|
expect(discovered[0].host).toEqual('127.0.0.1');
|
||||||
expect(discovered[0].port).toEqual(3080);
|
expect(discovered[0].port).toEqual(3080);
|
||||||
|
|
||||||
expect(discovered.length).toEqual(1);
|
expect(discovered.length).toEqual(1);
|
||||||
@ -117,7 +117,7 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
server = new Server();
|
server = new Server();
|
||||||
(server.ip = '199.111.111.1'), (server.port = 3333);
|
(server.host = '199.111.111.1'), (server.port = 3333);
|
||||||
|
|
||||||
spyOn(component, 'discovery').and.callFake(() => {
|
spyOn(component, 'discovery').and.callFake(() => {
|
||||||
return Observable.of([server]);
|
return Observable.of([server]);
|
||||||
@ -128,7 +128,7 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
expect(component.discoveredServer).toBeUndefined();
|
expect(component.discoveredServer).toBeUndefined();
|
||||||
component.discoverFirstAvailableServer();
|
component.discoverFirstAvailableServer();
|
||||||
tick();
|
tick();
|
||||||
expect(component.discoveredServer.ip).toEqual('199.111.111.1');
|
expect(component.discoveredServer.host).toEqual('199.111.111.1');
|
||||||
expect(component.discoveredServer.port).toEqual(3333);
|
expect(component.discoveredServer.port).toEqual(3333);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
let server: Server;
|
let server: Server;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
server = new Server();
|
server = new Server();
|
||||||
(server.ip = '199.111.111.1'), (server.port = 3333);
|
(server.host = '199.111.111.1'), (server.port = 3333);
|
||||||
component.discoveredServer = server;
|
component.discoveredServer = server;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,8 +155,9 @@ describe('ServerDiscoveryComponent', () => {
|
|||||||
component.accept(server);
|
component.accept(server);
|
||||||
tick();
|
tick();
|
||||||
expect(component.discoveredServer).toBeNull();
|
expect(component.discoveredServer).toBeNull();
|
||||||
expect(mockedServerService.servers[0].ip).toEqual('199.111.111.1');
|
expect(mockedServerService.servers[0].host).toEqual('199.111.111.1');
|
||||||
expect(mockedServerService.servers[0].name).toEqual('199.111.111.1');
|
expect(mockedServerService.servers[0].name).toEqual('199.111.111.1');
|
||||||
|
expect(mockedServerService.servers[0].location).toEqual('remote');
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import { ServerDatabase } from '../../../services/server.database';
|
|||||||
export class ServerDiscoveryComponent implements OnInit {
|
export class ServerDiscoveryComponent implements OnInit {
|
||||||
private defaultServers = [
|
private defaultServers = [
|
||||||
{
|
{
|
||||||
ip: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 3080
|
port: 3080
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -42,7 +42,7 @@ export class ServerDiscoveryComponent implements OnInit {
|
|||||||
).subscribe(([local, discovered]) => {
|
).subscribe(([local, discovered]) => {
|
||||||
local.forEach(added => {
|
local.forEach(added => {
|
||||||
discovered = discovered.filter(server => {
|
discovered = discovered.filter(server => {
|
||||||
return !(server.ip == added.ip && server.port == added.port);
|
return !(server.host == added.host && server.port == added.port);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (discovered.length > 0) {
|
if (discovered.length > 0) {
|
||||||
@ -56,7 +56,7 @@ export class ServerDiscoveryComponent implements OnInit {
|
|||||||
|
|
||||||
this.defaultServers.forEach(testServer => {
|
this.defaultServers.forEach(testServer => {
|
||||||
queries.push(
|
queries.push(
|
||||||
this.isServerAvailable(testServer.ip, testServer.port).catch(err => {
|
this.isServerAvailable(testServer.host, testServer.port).catch(err => {
|
||||||
return Observable.of(null);
|
return Observable.of(null);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -72,7 +72,7 @@ export class ServerDiscoveryComponent implements OnInit {
|
|||||||
|
|
||||||
isServerAvailable(ip: string, port: number): Observable<Server> {
|
isServerAvailable(ip: string, port: number): Observable<Server> {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = ip;
|
server.host = ip;
|
||||||
server.port = port;
|
server.port = port;
|
||||||
return this.versionService.get(server).flatMap((version: Version) => Observable.of(server));
|
return this.versionService.get(server).flatMap((version: Version) => Observable.of(server));
|
||||||
}
|
}
|
||||||
@ -83,9 +83,11 @@ export class ServerDiscoveryComponent implements OnInit {
|
|||||||
|
|
||||||
accept(server: Server) {
|
accept(server: Server) {
|
||||||
if (server.name == null) {
|
if (server.name == null) {
|
||||||
server.name = server.ip;
|
server.name = server.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.location = 'remote';
|
||||||
|
|
||||||
this.serverService.create(server).then((created: Server) => {
|
this.serverService.create(server).then((created: Server) => {
|
||||||
this.serverDatabase.addServer(created);
|
this.serverDatabase.addServer(created);
|
||||||
this.discoveredServer = null;
|
this.discoveredServer = null;
|
||||||
|
@ -17,9 +17,14 @@
|
|||||||
>
|
>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="location">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Location </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row"> {{ row.location }} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="ip">
|
<ng-container matColumnDef="ip">
|
||||||
<mat-header-cell *matHeaderCellDef> IP </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef> Host </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row"> {{ row.ip }} </mat-cell>
|
<mat-cell *matCellDef="let row"> {{ row.host }} </mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="port">
|
<ng-container matColumnDef="port">
|
||||||
@ -30,6 +35,14 @@
|
|||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row" style="text-align: right">
|
<mat-cell *matCellDef="let row" style="text-align: right">
|
||||||
|
<button mat-icon-button (click)="startServer(row)" *ngIf="row.location === 'local' && getServerStatus(row) === 'stopped'">
|
||||||
|
<mat-icon aria-label="Start server">play_arrow</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-icon-button (click)="stopServer(row)" *ngIf="row.location === 'local' && getServerStatus(row) === 'running'">
|
||||||
|
<mat-icon aria-label="Stop server">stop</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button mat-icon-button (click)="deleteServer(row)">
|
<button mat-icon-button (click)="deleteServer(row)">
|
||||||
<mat-icon aria-label="Remove server">delete</mat-icon>
|
<mat-icon aria-label="Remove server">delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,35 +1,70 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { DataSource } from '@angular/cdk/collections';
|
import { DataSource } from '@angular/cdk/collections';
|
||||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||||
|
|
||||||
import { Observable, merge } from 'rxjs';
|
import { Observable, merge, Subscription } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Server } from '../../models/server';
|
import { Server } from '../../models/server';
|
||||||
import { ServerService } from '../../services/server.service';
|
import { ServerService } from '../../services/server.service';
|
||||||
import { ServerDatabase } from '../../services/server.database';
|
import { ServerDatabase } from '../../services/server.database';
|
||||||
|
import { ElectronService } from 'ngx-electron';
|
||||||
|
import { ServerManagementService } from '../../services/server-management.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-server-list',
|
selector: 'app-server-list',
|
||||||
templateUrl: './servers.component.html',
|
templateUrl: './servers.component.html',
|
||||||
styleUrls: ['./servers.component.css']
|
styleUrls: ['./servers.component.css']
|
||||||
})
|
})
|
||||||
export class ServersComponent implements OnInit {
|
export class ServersComponent implements OnInit, OnDestroy {
|
||||||
dataSource: ServerDataSource;
|
dataSource: ServerDataSource;
|
||||||
displayedColumns = ['id', 'name', 'ip', 'port', 'actions'];
|
displayedColumns = ['id', 'name', 'location', 'ip', 'port', 'actions'];
|
||||||
|
serverStatusSubscription: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private serverDatabase: ServerDatabase
|
private serverDatabase: ServerDatabase,
|
||||||
|
private serverManagement: ServerManagementService,
|
||||||
|
private changeDetector: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
const runningServersNames = this.serverManagement.getRunningServers();
|
||||||
|
|
||||||
this.serverService.findAll().then((servers: Server[]) => {
|
this.serverService.findAll().then((servers: Server[]) => {
|
||||||
|
servers.forEach((server) => {
|
||||||
|
const serverIndex = runningServersNames.findIndex((serverName) => server.name === serverName);
|
||||||
|
if(serverIndex >= 0) {
|
||||||
|
server.status = 'running';
|
||||||
|
}
|
||||||
|
});
|
||||||
this.serverDatabase.addServers(servers);
|
this.serverDatabase.addServers(servers);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataSource = new ServerDataSource(this.serverDatabase);
|
this.dataSource = new ServerDataSource(this.serverDatabase);
|
||||||
|
|
||||||
|
this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => {
|
||||||
|
const server = this.serverDatabase.find(serverStatus.serverName);
|
||||||
|
if(!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(serverStatus.status === 'stopped') {
|
||||||
|
server.status = 'stopped';
|
||||||
|
}
|
||||||
|
if(serverStatus.status === 'errored') {
|
||||||
|
server.status = 'stopped';
|
||||||
|
}
|
||||||
|
if(serverStatus.status === 'started') {
|
||||||
|
server.status = 'running';
|
||||||
|
}
|
||||||
|
this.serverDatabase.update(server);
|
||||||
|
this.changeDetector.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.serverStatusSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
createModal() {
|
createModal() {
|
||||||
@ -46,11 +81,28 @@ export class ServersComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getServerStatus(server: Server) {
|
||||||
|
if(server.location === 'local') {
|
||||||
|
if(server.status === undefined) {
|
||||||
|
return 'stopped';
|
||||||
|
}
|
||||||
|
return server.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deleteServer(server: Server) {
|
deleteServer(server: Server) {
|
||||||
this.serverService.delete(server).then(() => {
|
this.serverService.delete(server).then(() => {
|
||||||
this.serverDatabase.remove(server);
|
this.serverDatabase.remove(server);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startServer(server: Server) {
|
||||||
|
await this.serverManagement.start(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopServer(server: Server) {
|
||||||
|
await this.serverManagement.stop(server);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -61,14 +113,49 @@ export class AddServerDialogComponent implements OnInit {
|
|||||||
server: Server = new Server();
|
server: Server = new Server();
|
||||||
|
|
||||||
authorizations = [{ key: 'none', name: 'No authorization' }, { key: 'basic', name: 'Basic authorization' }];
|
authorizations = [{ key: 'none', name: 'No authorization' }, { key: 'basic', name: 'Basic authorization' }];
|
||||||
|
locations = [];
|
||||||
|
|
||||||
constructor(public dialogRef: MatDialogRef<AddServerDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<AddServerDialogComponent>,
|
||||||
|
private electronService: ElectronService,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getLocations() {
|
||||||
|
let locations = [];
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
locations.push({ key: 'local', name: 'Local' });
|
||||||
|
}
|
||||||
|
locations.push({ key: 'remote', name: 'Remote' });
|
||||||
|
return locations
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultLocation() {
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
return 'remote';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultLocalServerPath() {
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
return this.electronService.remote.require('./local-server.js').getLocalServerPath();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.locations = this.getLocations();
|
||||||
this.server.authorization = 'none';
|
this.server.authorization = 'none';
|
||||||
|
this.server.location = this.getDefaultLocation();
|
||||||
|
this.server.path = this.getDefaultLocalServerPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddClick(): void {
|
onAddClick(): void {
|
||||||
|
// clear path if not local server
|
||||||
|
if(this.server.location !== 'local') {
|
||||||
|
this.server.path = null;
|
||||||
|
}
|
||||||
this.dialogRef.close(this.server);
|
this.dialogRef.close(this.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,19 +6,27 @@ import { MatIconModule, MatMenuModule, MatToolbarModule, MatProgressSpinnerModul
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { ProgressComponent } from '../../common/progress/progress.component';
|
import { ProgressComponent } from '../../common/progress/progress.component';
|
||||||
import { ProgressService } from '../../common/progress/progress.service';
|
import { ProgressService } from '../../common/progress/progress.service';
|
||||||
|
import { ServerManagementService, ServerStateEvent } from '../../services/server-management.service';
|
||||||
|
import { ToasterService } from '../../services/toaster.service';
|
||||||
|
import { MockedToasterService } from '../../services/toaster.service.spec';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
class ElectronServiceMock {
|
class ElectronServiceMock {
|
||||||
public isElectronApp: boolean;
|
public isElectronApp: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('DefaultLayoutComponent', () => {
|
fdescribe('DefaultLayoutComponent', () => {
|
||||||
let component: DefaultLayoutComponent;
|
let component: DefaultLayoutComponent;
|
||||||
let fixture: ComponentFixture<DefaultLayoutComponent>;
|
let fixture: ComponentFixture<DefaultLayoutComponent>;
|
||||||
let electronServiceMock: ElectronServiceMock;
|
let electronServiceMock: ElectronServiceMock;
|
||||||
|
let serverManagementService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
electronServiceMock = new ElectronServiceMock();
|
electronServiceMock = new ElectronServiceMock();
|
||||||
|
serverManagementService = {
|
||||||
|
serverStatusChanged: new Subject<ServerStateEvent>()
|
||||||
|
};
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [DefaultLayoutComponent, ProgressComponent],
|
declarations: [DefaultLayoutComponent, ProgressComponent],
|
||||||
@ -28,6 +36,14 @@ describe('DefaultLayoutComponent', () => {
|
|||||||
provide: ElectronService,
|
provide: ElectronService,
|
||||||
useValue: electronServiceMock
|
useValue: electronServiceMock
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ServerManagementService,
|
||||||
|
useValue: serverManagementService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToasterService,
|
||||||
|
useClass: MockedToasterService
|
||||||
|
},
|
||||||
ProgressService
|
ProgressService
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@ -54,4 +70,44 @@ describe('DefaultLayoutComponent', () => {
|
|||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(component.isInstalledSoftwareAvailable).toBeFalsy();
|
expect(component.isInstalledSoftwareAvailable).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show error when server management service throw event', () => {
|
||||||
|
const toaster: MockedToasterService = TestBed.get(ToasterService);
|
||||||
|
serverManagementService.serverStatusChanged.next({
|
||||||
|
status: 'errored',
|
||||||
|
message: 'Message'
|
||||||
|
});
|
||||||
|
expect(toaster.errors).toEqual(['Message']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show error when server management service throw event', () => {
|
||||||
|
component.ngOnDestroy();
|
||||||
|
const toaster: MockedToasterService = TestBed.get(ToasterService);
|
||||||
|
serverManagementService.serverStatusChanged.next({
|
||||||
|
status: 'errored',
|
||||||
|
message: 'Message'
|
||||||
|
});
|
||||||
|
expect(toaster.errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('auto stopping servers', () => {
|
||||||
|
let event;
|
||||||
|
beforeEach(() => {
|
||||||
|
event = new Event('onbeforeunload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close window with no action when not in electron', async () => {
|
||||||
|
component.shouldStopServersOnClosing = false;
|
||||||
|
const isClosed = await component.onBeforeUnload(event);
|
||||||
|
expect(isClosed).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop all servers and close window', () => {
|
||||||
|
component.shouldStopServersOnClosing = true;
|
||||||
|
const isClosed = component.onBeforeUnload(event);
|
||||||
|
expect(isClosed).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { ElectronService } from 'ngx-electron';
|
import { ElectronService } from 'ngx-electron';
|
||||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
import { Component, OnInit, ViewEncapsulation, OnDestroy, HostListener } from '@angular/core';
|
||||||
|
import { ServerManagementService } from '../../services/server-management.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { ToasterService } from '../../services/toaster.service';
|
||||||
|
import { ProgressService } from '../../common/progress/progress.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-default-layout',
|
selector: 'app-default-layout',
|
||||||
@ -7,15 +11,53 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
|||||||
templateUrl: './default-layout.component.html',
|
templateUrl: './default-layout.component.html',
|
||||||
styleUrls: ['./default-layout.component.css']
|
styleUrls: ['./default-layout.component.css']
|
||||||
})
|
})
|
||||||
export class DefaultLayoutComponent implements OnInit {
|
export class DefaultLayoutComponent implements OnInit, OnDestroy {
|
||||||
public isInstalledSoftwareAvailable = false;
|
public isInstalledSoftwareAvailable = false;
|
||||||
|
|
||||||
|
serverStatusSubscription: Subscription;
|
||||||
|
shouldStopServersOnClosing = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private electronService: ElectronService
|
private electronService: ElectronService,
|
||||||
|
private serverManagement: ServerManagementService,
|
||||||
|
private toasterService: ToasterService,
|
||||||
|
private progressService: ProgressService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.isInstalledSoftwareAvailable = this.electronService.isElectronApp;
|
this.isInstalledSoftwareAvailable = this.electronService.isElectronApp;
|
||||||
|
|
||||||
|
// attach to notification stream when any of running local servers experienced issues
|
||||||
|
this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => {
|
||||||
|
if(serverStatus.status === 'errored') {
|
||||||
|
this.toasterService.error(serverStatus.message);
|
||||||
|
}
|
||||||
|
if(serverStatus.status === 'stderr') {
|
||||||
|
this.toasterService.error(serverStatus.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// stop servers only when in Electron
|
||||||
|
this.shouldStopServersOnClosing = this.electronService.isElectronApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
async onBeforeUnload($event) {
|
||||||
|
if(!this.shouldStopServersOnClosing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.returnValue = false;
|
||||||
|
this.progressService.activate();
|
||||||
|
await this.serverManagement.stopAll();
|
||||||
|
this.shouldStopServersOnClosing = false;
|
||||||
|
this.progressService.deactivate();
|
||||||
|
window.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.serverStatusSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
export type ServerAuthorization = 'basic' | 'none';
|
export type ServerAuthorization = 'basic' | 'none';
|
||||||
|
export type ServerLocation = 'local' | 'remote';
|
||||||
|
export type ServerStatus = 'stopped' | 'starting' | 'running';
|
||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
location: ServerLocation;
|
||||||
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
path: string;
|
||||||
authorization: ServerAuthorization;
|
authorization: ServerAuthorization;
|
||||||
login: string;
|
login: string;
|
||||||
password: string;
|
password: string;
|
||||||
is_local: boolean;
|
is_local: boolean;
|
||||||
|
status: ServerStatus;
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ describe('HttpServer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should make local call when ip and port is not defined', () => {
|
it('should make local call when ip and port is not defined', () => {
|
||||||
server.ip = null;
|
server.host = null;
|
||||||
server.port = null;
|
server.port = null;
|
||||||
|
|
||||||
service
|
service
|
||||||
|
@ -164,8 +164,8 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getOptionsForServer<T extends HeadersOptions>(server: Server, url: string, options: T) {
|
private getOptionsForServer<T extends HeadersOptions>(server: Server, url: string, options: T) {
|
||||||
if (server.ip && server.port) {
|
if (server.host && server.port) {
|
||||||
url = `http://${server.ip}:${server.port}/v2${url}`;
|
url = `http://${server.host}:${server.port}/v2${url}`;
|
||||||
} else {
|
} else {
|
||||||
url = `/v2${url}`;
|
url = `/v2${url}`;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notificationsPath(server: Server, project_id: string): string {
|
notificationsPath(server: Server, project_id: string): string {
|
||||||
return `ws://${server.ip}:${server.port}/v2/projects/${project_id}/notifications/ws`;
|
return `ws://${server.host}:${server.port}/v2/projects/${project_id}/notifications/ws`;
|
||||||
}
|
}
|
||||||
|
|
||||||
isReadOnly(project: Project) {
|
isReadOnly(project: Project) {
|
||||||
|
93
src/app/services/server-management.service.spec.ts
Normal file
93
src/app/services/server-management.service.spec.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ServerManagementService } from './server-management.service';
|
||||||
|
import { ElectronService } from 'ngx-electron';
|
||||||
|
import { Server } from '../models/server';
|
||||||
|
|
||||||
|
describe('ServerManagementService', () => {
|
||||||
|
let electronService;
|
||||||
|
let callbacks
|
||||||
|
let removed;
|
||||||
|
let server;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callbacks = [];
|
||||||
|
removed = [];
|
||||||
|
server = undefined;
|
||||||
|
electronService = {
|
||||||
|
isElectronApp: true,
|
||||||
|
ipcRenderer: {
|
||||||
|
on: (channel, callback) => {
|
||||||
|
callbacks.push({
|
||||||
|
channel: channel,
|
||||||
|
callback: callback
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeAllListeners: (name) => {
|
||||||
|
removed.push(name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
require: (file) => {
|
||||||
|
return {
|
||||||
|
startLocalServer: (serv) => {
|
||||||
|
server = serv;
|
||||||
|
},
|
||||||
|
stopLocalServer: (serv) => {
|
||||||
|
server = serv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: ElectronService, useValue: electronService},
|
||||||
|
ServerManagementService
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: ServerManagementService = TestBed.get(ServerManagementService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attach when running as electron app', () => {
|
||||||
|
TestBed.get(ServerManagementService);
|
||||||
|
expect(callbacks.length).toEqual(1);
|
||||||
|
expect(callbacks[0].channel).toEqual('local-server-status-events');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not attach when running as not electron app', () => {
|
||||||
|
electronService.isElectronApp = false;
|
||||||
|
TestBed.get(ServerManagementService);
|
||||||
|
expect(callbacks.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deattach when running as electron app', () => {
|
||||||
|
const service: ServerManagementService = TestBed.get(ServerManagementService);
|
||||||
|
service.ngOnDestroy();
|
||||||
|
expect(removed).toEqual(['local-server-status-events']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not deattach when running as not electron app', () => {
|
||||||
|
electronService.isElectronApp = false;
|
||||||
|
const service: ServerManagementService = TestBed.get(ServerManagementService);
|
||||||
|
service.ngOnDestroy();
|
||||||
|
expect(removed).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start local server', async () => {
|
||||||
|
const service: ServerManagementService = TestBed.get(ServerManagementService);
|
||||||
|
await service.start({ name: 'test'} as Server);
|
||||||
|
expect(server).toEqual({ name: 'test'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop local server', async () => {
|
||||||
|
const service: ServerManagementService = TestBed.get(ServerManagementService);
|
||||||
|
await service.stop({ name: 'test2'} as Server);
|
||||||
|
expect(server).toEqual({ name: 'test2'});
|
||||||
|
});
|
||||||
|
});
|
55
src/app/services/server-management.service.ts
Normal file
55
src/app/services/server-management.service.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
|
import { Server } from '../models/server';
|
||||||
|
import { ElectronService } from 'ngx-electron';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface ServerStateEvent {
|
||||||
|
serverName: string;
|
||||||
|
status: "started" | "errored" | "stopped" | "stderr";
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerManagementService implements OnDestroy {
|
||||||
|
|
||||||
|
serverStatusChanged = new Subject<ServerStateEvent>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private electronService: ElectronService
|
||||||
|
) {
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
this.electronService.ipcRenderer.on(this.statusChannel, (event, data) => {
|
||||||
|
this.serverStatusChanged.next(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get statusChannel() {
|
||||||
|
return 'local-server-status-events';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(server: Server) {
|
||||||
|
return await this.electronService.remote.require('./local-server.js').startLocalServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(server: Server) {
|
||||||
|
return await this.electronService.remote.require('./local-server.js').stopLocalServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopAll() {
|
||||||
|
return await this.electronService.remote.require('./local-server.js').stopAllLocalServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRunningServers() {
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
return this.electronService.remote.require('./local-server.js').getRunningServers();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if(this.electronService.isElectronApp) {
|
||||||
|
this.electronService.ipcRenderer.removeAllListeners(this.statusChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,4 +29,20 @@ export class ServerDatabase {
|
|||||||
this.dataChange.next(this.data.slice());
|
this.dataChange.next(this.data.slice());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public find(serverName: string) {
|
||||||
|
return this.data.find((server) => server.name === serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findIndex(serverName: string) {
|
||||||
|
return this.data.findIndex((server) => server.name === serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(server: Server) {
|
||||||
|
const index = this.findIndex(server.name);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.data[index] = server;
|
||||||
|
this.dataChange.next(this.data.slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ describe('ServerService', () => {
|
|||||||
|
|
||||||
const expectedServer = new Server();
|
const expectedServer = new Server();
|
||||||
expectedServer.name = 'local';
|
expectedServer.name = 'local';
|
||||||
expectedServer.ip = 'hostname';
|
expectedServer.host = 'hostname';
|
||||||
expectedServer.port = 9999;
|
expectedServer.port = 9999;
|
||||||
expectedServer.is_local = true;
|
expectedServer.is_local = true;
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ describe('ServerService', () => {
|
|||||||
it('should update local server when found', done => {
|
it('should update local server when found', done => {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.name = 'local';
|
server.name = 'local';
|
||||||
server.ip = 'hostname';
|
server.host = 'hostname';
|
||||||
server.port = 9999;
|
server.port = 9999;
|
||||||
server.is_local = true;
|
server.is_local = true;
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ describe('ServerService', () => {
|
|||||||
spyOn(service, 'update').and.returnValue(Promise.resolve(new Server()));
|
spyOn(service, 'update').and.returnValue(Promise.resolve(new Server()));
|
||||||
|
|
||||||
service.getLocalServer('hostname-2', 11111).then(() => {
|
service.getLocalServer('hostname-2', 11111).then(() => {
|
||||||
server.ip = 'hostname-2';
|
server.host = 'hostname-2';
|
||||||
server.port = 11111;
|
server.port = 11111;
|
||||||
|
|
||||||
expect(service.update).toHaveBeenCalledWith(server);
|
expect(service.update).toHaveBeenCalledWith(server);
|
||||||
|
@ -55,12 +55,12 @@ export class ServerService {
|
|||||||
return this.onReady(() => this.indexedDbService.get().delete(this.tablename, server.id));
|
return this.onReady(() => this.indexedDbService.get().delete(this.tablename, server.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLocalServer(ip: string, port: number) {
|
public getLocalServer(host: string, port: number) {
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
this.findAll().then((servers: Server[]) => {
|
this.findAll().then((servers: Server[]) => {
|
||||||
const local = servers.find(server => server.is_local);
|
const local = servers.find(server => server.is_local);
|
||||||
if (local) {
|
if (local) {
|
||||||
local.ip = ip;
|
local.host = host;
|
||||||
local.port = port;
|
local.port = port;
|
||||||
this.update(local).then(updated => {
|
this.update(local).then(updated => {
|
||||||
resolve(updated);
|
resolve(updated);
|
||||||
@ -68,7 +68,7 @@ export class ServerService {
|
|||||||
} else {
|
} else {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.name = 'local';
|
server.name = 'local';
|
||||||
server.ip = ip;
|
server.host = host;
|
||||||
server.port = port;
|
server.port = port;
|
||||||
server.is_local = true;
|
server.is_local = true;
|
||||||
this.create(server).then(created => {
|
this.create(server).then(created => {
|
||||||
|
@ -29,7 +29,7 @@ describe('TemplateService', () => {
|
|||||||
|
|
||||||
it('should ask for the list from server', () => {
|
it('should ask for the list from server', () => {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = '127.0.0.1';
|
server.host = '127.0.0.1';
|
||||||
server.port = 3080;
|
server.port = 3080;
|
||||||
server.authorization = 'none';
|
server.authorization = 'none';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { Server } from '../models/server';
|
|||||||
|
|
||||||
export function getTestServer(): Server {
|
export function getTestServer(): Server {
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
server.ip = '127.0.0.1';
|
server.host = '127.0.0.1';
|
||||||
server.port = 3080;
|
server.port = 3080;
|
||||||
server.authorization = 'none';
|
server.authorization = 'none';
|
||||||
return server;
|
return server;
|
||||||
|
@ -2,6 +2,7 @@ import { TestBed, inject } from '@angular/core/testing';
|
|||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
import { ToasterService } from './toaster.service';
|
import { ToasterService } from './toaster.service';
|
||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
|
||||||
export class MockedToasterService {
|
export class MockedToasterService {
|
||||||
public errors: string[];
|
public errors: string[];
|
||||||
@ -40,7 +41,9 @@ class MockedSnackBar {
|
|||||||
describe('ToasterService', () => {
|
describe('ToasterService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [ToasterService, { provide: MatSnackBar, useClass: MockedSnackBar }]
|
providers: [
|
||||||
|
ToasterService,
|
||||||
|
{ provide: MatSnackBar, useClass: MockedSnackBar }]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -15,13 +15,19 @@ export class ToasterService {
|
|||||||
MatSnackBarHorizontalPosition: 'center',
|
MatSnackBarHorizontalPosition: 'center',
|
||||||
MatSnackBarVerticalPosition: 'bottom'
|
MatSnackBarVerticalPosition: 'bottom'
|
||||||
};
|
};
|
||||||
constructor(private snackbar: MatSnackBar) {}
|
constructor(
|
||||||
|
private snackbar: MatSnackBar,
|
||||||
|
private zone: NgZone) {}
|
||||||
|
|
||||||
public error(message: string) {
|
public error(message: string) {
|
||||||
|
this.zone.run(() => {
|
||||||
this.snackbar.open(message, 'Close', this.snackBarConfigForError);
|
this.snackbar.open(message, 'Close', this.snackBarConfigForError);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public success(message: string) {
|
public success(message: string) {
|
||||||
|
this.zone.run(() => {
|
||||||
this.snackbar.open(message, 'Close', this.snackBarConfigForSuccess);
|
this.snackbar.open(message, 'Close', this.snackBarConfigForSuccess);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user