diff --git a/local-server.js b/local-server.js
new file mode 100644
index 00000000..2f01fdcc
--- /dev/null
+++ b/local-server.js
@@ -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();
+}
\ No newline at end of file
diff --git a/main.js b/main.js
index 74f42f20..d111a956 100644
--- a/main.js
+++ b/main.js
@@ -1,18 +1,13 @@
const electron = require('electron');
-const fs = require('fs');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require('path');
const url = require('url');
const yargs = require('yargs');
-const { ipcMain } = require('electron')
-
// 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.
let mainWindow;
-let serverProc = null;
-let isWin = /^win/.test(process.platform);
let isDev = false;
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 () {
// Create the browser window.
mainWindow = new BrowserWindow({
@@ -107,11 +59,17 @@ function createWindow () {
}
// Emitted when the window is closed.
- mainWindow.on('closed', function () {
+ mainWindow.on('closed',async function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// 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
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
- createWindow()
+ createWindow();
}
});
diff --git a/package.json b/package.json
index 2b4533c1..27bd66c4 100644
--- a/package.json
+++ b/package.json
@@ -61,14 +61,14 @@
"material-design-icons": "^3.0.1",
"ng2-file-upload": "^1.3.0",
"ngx-electron": "^2.0.0",
+ "node-fetch": "^2.3.0",
"notosans-fontface": "^1.1.0",
"raven-js": "^3.27.0",
"rxjs": "^6.3.3",
"rxjs-compat": "^6.3.3",
"typeface-roboto": "^0.0.54",
"yargs": "^12.0.5",
- "zone.js": "^0.8.26",
- "node-fetch": "^2.3.0"
+ "zone.js": "^0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.11.4",
@@ -97,6 +97,7 @@
"prettier": "^1.15.2",
"protractor": "~5.4.2",
"replace": "^1.0.1",
+ "tree-kill": "^1.2.1",
"ts-mockito": "^2.3.1",
"ts-node": "~7.0.1",
"tslint": "~5.12.0",
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index fdab8b00..ecca4863 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -95,6 +95,7 @@ import { TextEditorDialogComponent } from './components/project-map/drawings-edi
import { InstalledSoftwareService } from './services/installed-software.service';
import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service';
import { PlatformService } from './services/platform.service';
+import { ServerManagementService } from './services/server-management.service';
if (environment.production) {
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
@@ -197,7 +198,8 @@ if (environment.production) {
ToolsService,
InstalledSoftwareService,
ExternalSoftwareDefinitionService,
- PlatformService
+ PlatformService,
+ ServerManagementService
],
entryComponents: [
AddServerDialogComponent,
diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts
index 8dffbcf4..075d0150 100644
--- a/src/app/components/project-map/project-map.component.ts
+++ b/src/app/components/project-map/project-map.component.ts
@@ -158,7 +158,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.subscriptions.push(
this.nodesDataSource.changes.subscribe((nodes: 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;
diff --git a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts
index 22ee843e..3ed7d6fd 100644
--- a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts
+++ b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts
@@ -87,7 +87,7 @@ describe('AddBlankProjectDialogComponent', () => {
}).compileComponents();
server = new Server();
- server.ip = 'localhost';
+ server.host = 'localhost';
server.port = 80;
}));
diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts
index 528c0549..bef5f1b9 100644
--- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts
+++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.spec.ts
@@ -80,7 +80,7 @@ describe('ImportProjectDialogComponent', () => {
}).compileComponents();
server = new Server();
- server.ip = 'localhost';
+ server.host = 'localhost';
server.port = 80;
}));
diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts
index f8165314..59ad09ee 100644
--- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts
+++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts
@@ -138,6 +138,6 @@ export class ImportProjectDialogComponent implements OnInit {
prepareUploadPath(): string {
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}`;
}
}
diff --git a/src/app/components/servers/add-server-dialog.html b/src/app/components/servers/add-server-dialog.html
index e4ee5378..489531be 100644
--- a/src/app/components/servers/add-server-dialog.html
+++ b/src/app/components/servers/add-server-dialog.html
@@ -1,7 +1,18 @@
Add server
-
+
+
+
+ {{ location.name }}
+
+
+
+
+
+
+
+
diff --git a/src/app/components/servers/server-discovery/server-discovery.component.html b/src/app/components/servers/server-discovery/server-discovery.component.html
index 95343b8f..22a35723 100644
--- a/src/app/components/servers/server-discovery/server-discovery.component.html
+++ b/src/app/components/servers/server-discovery/server-discovery.component.html
@@ -1,6 +1,6 @@
- We've discovered GNS3 server on {{ discoveredServer.ip }}:{{ discoveredServer.port }}{{ discoveredServer.host }}:{{ discoveredServer.port }}, would you like to add to the list?
diff --git a/src/app/components/servers/server-discovery/server-discovery.component.spec.ts b/src/app/components/servers/server-discovery/server-discovery.component.spec.ts
index 4a9fdf43..0564bcfc 100644
--- a/src/app/components/servers/server-discovery/server-discovery.component.spec.ts
+++ b/src/app/components/servers/server-discovery/server-discovery.component.spec.ts
@@ -55,12 +55,12 @@ describe('ServerDiscoveryComponent', () => {
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(Observable.of(version));
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);
});
const server = new Server();
- server.ip = '127.0.0.1';
+ server.host = '127.0.0.1';
server.port = 3080;
expect(getVersionSpy).toHaveBeenCalledWith(server);
@@ -68,7 +68,7 @@ describe('ServerDiscoveryComponent', () => {
it('should throw error once server is not available', () => {
const server = new Server();
- server.ip = '127.0.0.1';
+ server.host = '127.0.0.1';
server.port = 3080;
const getVersionSpy = spyOn(mockedVersionService, 'get').and.returnValue(
@@ -96,13 +96,13 @@ describe('ServerDiscoveryComponent', () => {
spyOn(component, 'isServerAvailable').and.callFake((ip, port) => {
const server = new Server();
- server.ip = ip;
+ server.host = ip;
server.port = port;
return Observable.of(server);
});
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.length).toEqual(1);
@@ -117,7 +117,7 @@ describe('ServerDiscoveryComponent', () => {
beforeEach(function() {
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(() => {
return Observable.of([server]);
@@ -128,7 +128,7 @@ describe('ServerDiscoveryComponent', () => {
expect(component.discoveredServer).toBeUndefined();
component.discoverFirstAvailableServer();
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);
}));
@@ -146,7 +146,7 @@ describe('ServerDiscoveryComponent', () => {
let server: Server;
beforeEach(() => {
server = new Server();
- (server.ip = '199.111.111.1'), (server.port = 3333);
+ (server.host = '199.111.111.1'), (server.port = 3333);
component.discoveredServer = server;
});
@@ -155,8 +155,9 @@ describe('ServerDiscoveryComponent', () => {
component.accept(server);
tick();
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].location).toEqual('remote');
}));
});
diff --git a/src/app/components/servers/server-discovery/server-discovery.component.ts b/src/app/components/servers/server-discovery/server-discovery.component.ts
index 9e5965a3..da682fca 100644
--- a/src/app/components/servers/server-discovery/server-discovery.component.ts
+++ b/src/app/components/servers/server-discovery/server-discovery.component.ts
@@ -18,7 +18,7 @@ import { ServerDatabase } from '../../../services/server.database';
export class ServerDiscoveryComponent implements OnInit {
private defaultServers = [
{
- ip: '127.0.0.1',
+ host: '127.0.0.1',
port: 3080
}
];
@@ -42,7 +42,7 @@ export class ServerDiscoveryComponent implements OnInit {
).subscribe(([local, discovered]) => {
local.forEach(added => {
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) {
@@ -56,7 +56,7 @@ export class ServerDiscoveryComponent implements OnInit {
this.defaultServers.forEach(testServer => {
queries.push(
- this.isServerAvailable(testServer.ip, testServer.port).catch(err => {
+ this.isServerAvailable(testServer.host, testServer.port).catch(err => {
return Observable.of(null);
})
);
@@ -72,7 +72,7 @@ export class ServerDiscoveryComponent implements OnInit {
isServerAvailable(ip: string, port: number): Observable {
const server = new Server();
- server.ip = ip;
+ server.host = ip;
server.port = port;
return this.versionService.get(server).flatMap((version: Version) => Observable.of(server));
}
@@ -83,9 +83,11 @@ export class ServerDiscoveryComponent implements OnInit {
accept(server: Server) {
if (server.name == null) {
- server.name = server.ip;
+ server.name = server.host;
}
+ server.location = 'remote';
+
this.serverService.create(server).then((created: Server) => {
this.serverDatabase.addServer(created);
this.discoveredServer = null;
diff --git a/src/app/components/servers/servers.component.html b/src/app/components/servers/servers.component.html
index 2a6d868e..8f4ea9f9 100644
--- a/src/app/components/servers/servers.component.html
+++ b/src/app/components/servers/servers.component.html
@@ -17,9 +17,14 @@
>
+
+ Location
+ {{ row.location }}
+
+
- IP
- {{ row.ip }}
+ Host
+ {{ row.host }}
@@ -30,6 +35,14 @@
Actions
+
+
+
+
diff --git a/src/app/components/servers/servers.component.ts b/src/app/components/servers/servers.component.ts
index 3bce6ae0..23d295ad 100644
--- a/src/app/components/servers/servers.component.ts
+++ b/src/app/components/servers/servers.component.ts
@@ -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 { 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 { Server } from '../../models/server';
import { ServerService } from '../../services/server.service';
import { ServerDatabase } from '../../services/server.database';
+import { ElectronService } from 'ngx-electron';
+import { ServerManagementService } from '../../services/server-management.service';
@Component({
selector: 'app-server-list',
templateUrl: './servers.component.html',
styleUrls: ['./servers.component.css']
})
-export class ServersComponent implements OnInit {
+export class ServersComponent implements OnInit, OnDestroy {
dataSource: ServerDataSource;
- displayedColumns = ['id', 'name', 'ip', 'port', 'actions'];
+ displayedColumns = ['id', 'name', 'location', 'ip', 'port', 'actions'];
+ serverStatusSubscription: Subscription;
constructor(
private dialog: MatDialog,
private serverService: ServerService,
- private serverDatabase: ServerDatabase
+ private serverDatabase: ServerDatabase,
+ private serverManagement: ServerManagementService,
+ private changeDetector: ChangeDetectorRef
) {}
ngOnInit() {
+ const runningServersNames = this.serverManagement.getRunningServers();
+
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.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() {
@@ -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) {
this.serverService.delete(server).then(() => {
this.serverDatabase.remove(server);
});
}
+
+ async startServer(server: Server) {
+ await this.serverManagement.start(server);
+ }
+
+ async stopServer(server: Server) {
+ await this.serverManagement.stop(server);
+ }
}
@Component({
@@ -61,14 +113,49 @@ export class AddServerDialogComponent implements OnInit {
server: Server = new Server();
authorizations = [{ key: 'none', name: 'No authorization' }, { key: 'basic', name: 'Basic authorization' }];
+ locations = [];
- constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) {}
+ constructor(
+ public dialogRef: MatDialogRef,
+ 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() {
+ this.locations = this.getLocations();
this.server.authorization = 'none';
+ this.server.location = this.getDefaultLocation();
+ this.server.path = this.getDefaultLocalServerPath();
}
onAddClick(): void {
+ // clear path if not local server
+ if(this.server.location !== 'local') {
+ this.server.path = null;
+ }
this.dialogRef.close(this.server);
}
diff --git a/src/app/layouts/default-layout/default-layout.component.spec.ts b/src/app/layouts/default-layout/default-layout.component.spec.ts
index 47f00002..db56d793 100644
--- a/src/app/layouts/default-layout/default-layout.component.spec.ts
+++ b/src/app/layouts/default-layout/default-layout.component.spec.ts
@@ -6,19 +6,27 @@ import { MatIconModule, MatMenuModule, MatToolbarModule, MatProgressSpinnerModul
import { RouterTestingModule } from '@angular/router/testing';
import { ProgressComponent } from '../../common/progress/progress.component';
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 {
public isElectronApp: boolean;
}
-describe('DefaultLayoutComponent', () => {
+fdescribe('DefaultLayoutComponent', () => {
let component: DefaultLayoutComponent;
let fixture: ComponentFixture;
let electronServiceMock: ElectronServiceMock;
+ let serverManagementService;
beforeEach(async(() => {
electronServiceMock = new ElectronServiceMock();
+ serverManagementService = {
+ serverStatusChanged: new Subject()
+ };
TestBed.configureTestingModule({
declarations: [DefaultLayoutComponent, ProgressComponent],
@@ -28,6 +36,14 @@ describe('DefaultLayoutComponent', () => {
provide: ElectronService,
useValue: electronServiceMock
},
+ {
+ provide: ServerManagementService,
+ useValue: serverManagementService
+ },
+ {
+ provide: ToasterService,
+ useClass: MockedToasterService
+ },
ProgressService
]
}).compileComponents();
@@ -54,4 +70,44 @@ describe('DefaultLayoutComponent', () => {
component.ngOnInit();
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();
+ });
+ });
+
+
});
diff --git a/src/app/layouts/default-layout/default-layout.component.ts b/src/app/layouts/default-layout/default-layout.component.ts
index 162f83c6..32edf9b7 100644
--- a/src/app/layouts/default-layout/default-layout.component.ts
+++ b/src/app/layouts/default-layout/default-layout.component.ts
@@ -1,5 +1,9 @@
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({
selector: 'app-default-layout',
@@ -7,15 +11,53 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
templateUrl: './default-layout.component.html',
styleUrls: ['./default-layout.component.css']
})
-export class DefaultLayoutComponent implements OnInit {
+export class DefaultLayoutComponent implements OnInit, OnDestroy {
public isInstalledSoftwareAvailable = false;
+
+ serverStatusSubscription: Subscription;
+ shouldStopServersOnClosing = true;
constructor(
- private electronService: ElectronService
+ private electronService: ElectronService,
+ private serverManagement: ServerManagementService,
+ private toasterService: ToasterService,
+ private progressService: ProgressService
) {}
ngOnInit() {
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();
}
}
diff --git a/src/app/models/server.ts b/src/app/models/server.ts
index d3ef061b..2ad4ccd1 100644
--- a/src/app/models/server.ts
+++ b/src/app/models/server.ts
@@ -1,12 +1,17 @@
export type ServerAuthorization = 'basic' | 'none';
+export type ServerLocation = 'local' | 'remote';
+export type ServerStatus = 'stopped' | 'starting' | 'running';
export class Server {
id: number;
name: string;
- ip: string;
+ location: ServerLocation;
+ host: string;
port: number;
+ path: string;
authorization: ServerAuthorization;
login: string;
password: string;
is_local: boolean;
+ status: ServerStatus;
}
diff --git a/src/app/services/http-server.service.spec.ts b/src/app/services/http-server.service.spec.ts
index 9eaccc3f..52f8d0b4 100644
--- a/src/app/services/http-server.service.spec.ts
+++ b/src/app/services/http-server.service.spec.ts
@@ -218,7 +218,7 @@ describe('HttpServer', () => {
});
it('should make local call when ip and port is not defined', () => {
- server.ip = null;
+ server.host = null;
server.port = null;
service
diff --git a/src/app/services/http-server.service.ts b/src/app/services/http-server.service.ts
index 9a04024b..a32df1e8 100644
--- a/src/app/services/http-server.service.ts
+++ b/src/app/services/http-server.service.ts
@@ -164,8 +164,8 @@ export class HttpServer {
}
private getOptionsForServer(server: Server, url: string, options: T) {
- if (server.ip && server.port) {
- url = `http://${server.ip}:${server.port}/v2${url}`;
+ if (server.host && server.port) {
+ url = `http://${server.host}:${server.port}/v2${url}`;
} else {
url = `/v2${url}`;
}
diff --git a/src/app/services/project.service.ts b/src/app/services/project.service.ts
index 0724b726..4ca87f98 100644
--- a/src/app/services/project.service.ts
+++ b/src/app/services/project.service.ts
@@ -50,7 +50,7 @@ export class ProjectService {
}
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) {
diff --git a/src/app/services/server-management.service.spec.ts b/src/app/services/server-management.service.spec.ts
new file mode 100644
index 00000000..68036d60
--- /dev/null
+++ b/src/app/services/server-management.service.spec.ts
@@ -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'});
+ });
+});
diff --git a/src/app/services/server-management.service.ts b/src/app/services/server-management.service.ts
new file mode 100644
index 00000000..5dbecd87
--- /dev/null
+++ b/src/app/services/server-management.service.ts
@@ -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();
+
+ 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);
+ }
+ }
+}
diff --git a/src/app/services/server.database.ts b/src/app/services/server.database.ts
index 4d8af007..cd3abaea 100644
--- a/src/app/services/server.database.ts
+++ b/src/app/services/server.database.ts
@@ -29,4 +29,20 @@ export class ServerDatabase {
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());
+ }
+ }
}
diff --git a/src/app/services/server.service.spec.ts b/src/app/services/server.service.spec.ts
index 28e4bd56..2b19f976 100644
--- a/src/app/services/server.service.spec.ts
+++ b/src/app/services/server.service.spec.ts
@@ -149,7 +149,7 @@ describe('ServerService', () => {
const expectedServer = new Server();
expectedServer.name = 'local';
- expectedServer.ip = 'hostname';
+ expectedServer.host = 'hostname';
expectedServer.port = 9999;
expectedServer.is_local = true;
@@ -162,7 +162,7 @@ describe('ServerService', () => {
it('should update local server when found', done => {
const server = new Server();
server.name = 'local';
- server.ip = 'hostname';
+ server.host = 'hostname';
server.port = 9999;
server.is_local = true;
@@ -170,7 +170,7 @@ describe('ServerService', () => {
spyOn(service, 'update').and.returnValue(Promise.resolve(new Server()));
service.getLocalServer('hostname-2', 11111).then(() => {
- server.ip = 'hostname-2';
+ server.host = 'hostname-2';
server.port = 11111;
expect(service.update).toHaveBeenCalledWith(server);
diff --git a/src/app/services/server.service.ts b/src/app/services/server.service.ts
index e1ac7e2e..2aed4eeb 100644
--- a/src/app/services/server.service.ts
+++ b/src/app/services/server.service.ts
@@ -55,12 +55,12 @@ export class ServerService {
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) => {
this.findAll().then((servers: Server[]) => {
const local = servers.find(server => server.is_local);
if (local) {
- local.ip = ip;
+ local.host = host;
local.port = port;
this.update(local).then(updated => {
resolve(updated);
@@ -68,7 +68,7 @@ export class ServerService {
} else {
const server = new Server();
server.name = 'local';
- server.ip = ip;
+ server.host = host;
server.port = port;
server.is_local = true;
this.create(server).then(created => {
diff --git a/src/app/services/template.service.spec.ts b/src/app/services/template.service.spec.ts
index ac767d33..af1357ff 100644
--- a/src/app/services/template.service.spec.ts
+++ b/src/app/services/template.service.spec.ts
@@ -29,7 +29,7 @@ describe('TemplateService', () => {
it('should ask for the list from server', () => {
const server = new Server();
- server.ip = '127.0.0.1';
+ server.host = '127.0.0.1';
server.port = 3080;
server.authorization = 'none';
diff --git a/src/app/services/testing.ts b/src/app/services/testing.ts
index 14b4e03f..8fb0792c 100644
--- a/src/app/services/testing.ts
+++ b/src/app/services/testing.ts
@@ -2,7 +2,7 @@ import { Server } from '../models/server';
export function getTestServer(): Server {
const server = new Server();
- server.ip = '127.0.0.1';
+ server.host = '127.0.0.1';
server.port = 3080;
server.authorization = 'none';
return server;
diff --git a/src/app/services/toaster.service.spec.ts b/src/app/services/toaster.service.spec.ts
index df5edbb7..f88476ed 100644
--- a/src/app/services/toaster.service.spec.ts
+++ b/src/app/services/toaster.service.spec.ts
@@ -2,6 +2,7 @@ import { TestBed, inject } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material';
import { ToasterService } from './toaster.service';
+import { NgZone } from '@angular/core';
export class MockedToasterService {
public errors: string[];
@@ -40,7 +41,9 @@ class MockedSnackBar {
describe('ToasterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [ToasterService, { provide: MatSnackBar, useClass: MockedSnackBar }]
+ providers: [
+ ToasterService,
+ { provide: MatSnackBar, useClass: MockedSnackBar }]
});
});
diff --git a/src/app/services/toaster.service.ts b/src/app/services/toaster.service.ts
index 24d26ae0..0c579428 100644
--- a/src/app/services/toaster.service.ts
+++ b/src/app/services/toaster.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
import { MatSnackBar } from '@angular/material';
@Injectable()
@@ -15,13 +15,19 @@ export class ToasterService {
MatSnackBarHorizontalPosition: 'center',
MatSnackBarVerticalPosition: 'bottom'
};
- constructor(private snackbar: MatSnackBar) {}
+ constructor(
+ private snackbar: MatSnackBar,
+ private zone: NgZone) {}
public error(message: string) {
- this.snackbar.open(message, 'Close', this.snackBarConfigForError);
+ this.zone.run(() => {
+ this.snackbar.open(message, 'Close', this.snackBarConfigForError);
+ });
}
public success(message: string) {
- this.snackbar.open(message, 'Close', this.snackBarConfigForSuccess);
+ this.zone.run(() => {
+ this.snackbar.open(message, 'Close', this.snackBarConfigForSuccess);
+ });
}
}