From 6c7196c0bed8bc32b0e811435dd5a077d0129314 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 6 Feb 2019 09:12:14 +0100 Subject: [PATCH 01/13] Initial local server management --- local-server.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 local-server.js diff --git a/local-server.js b/local-server.js new file mode 100644 index 00000000..d4e79948 --- /dev/null +++ b/local-server.js @@ -0,0 +1,39 @@ +const { spawn } = require('child_process'); + +servers = [ + { + name: 'my-local', + path: 'c:\\Program Files\\GNS3\\gns3server.EXE' + } +] + + +async function run(server) { + console.log(`Running '${server.path}'`); + const process = spawn(server.path); + + process.on('exit', () => { + console.log(`Process has exited`); + }); + + process.on('close', (code) => { + console.log(`child process exited with code ${code}`); + }); + + process.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + process.stderr.on('data', (data) => { + console.log(`stderr: ${data}`); + }); + +} + +async function main() { + await run(servers[0]); +} + +if (require.main === module) { + main(); +} \ No newline at end of file From 06bda9fc7994d16c4a4a2408e05b5ba3c26f9ce8 Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 7 Feb 2019 13:35:07 +0100 Subject: [PATCH 02/13] Starting and stopping gns3server in local-server.js manager --- local-server.js | 80 ++++++++++++++++++++++++++++++++++--------------- package.json | 5 ++-- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/local-server.js b/local-server.js index d4e79948..bea8a4bb 100644 --- a/local-server.js +++ b/local-server.js @@ -1,39 +1,71 @@ const { spawn } = require('child_process'); +const kill = require('tree-kill'); -servers = [ - { - name: 'my-local', - path: 'c:\\Program Files\\GNS3\\gns3server.EXE' - } -] +let runningServers = {}; +function getServerArguments(server, overrides) { + let serverArguments = []; + return serverArguments; +} + +async function stopAll() { + Object.keys(runningServers).forEach(async (serverName) => { + await stop(serverName); + }); +} + +async function stop(serverName) { + const runningServer = runningServers[serverName]; + const pid = runningServer.process.pid; + console.log(`Stopping '${serverName}' with PID='${pid}'`); + kill(pid, (error) => { + if(error) { + console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); + } + else { + console.log(`Stopped '${serverName}' with PID='${pid}'`); + } + }); +} + +async function run(server, options) { + const logStdout = options.logStdout || false; -async function run(server) { console.log(`Running '${server.path}'`); - const process = spawn(server.path); + + let serverProcess = spawn(server.path, getServerArguments(server)); - process.on('exit', () => { - console.log(`Process has exited`); + runningServers[server.name] = { + process: serverProcess + }; + + serverProcess.stdout.on('data', function(data) { + if(logStdout) { + console.log(data.toString()); + } }); - - process.on('close', (code) => { - console.log(`child process exited with code ${code}`); - }); - - process.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - process.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); - }); - } async function main() { - await run(servers[0]); + await run({ + name: 'my-local', + path: 'c:\\Program Files\\GNS3\\gns3server.EXE', + port: 3080 + }, { + logStdout: 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/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", From 7410455bdf7aed0885665238d13e56ea3de1a716 Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 7 Feb 2019 16:02:25 +0100 Subject: [PATCH 03/13] Sequential execution of stopping servers --- local-server.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/local-server.js b/local-server.js index bea8a4bb..4efeae75 100644 --- a/local-server.js +++ b/local-server.js @@ -9,23 +9,31 @@ function getServerArguments(server, overrides) { } async function stopAll() { - Object.keys(runningServers).forEach(async (serverName) => { - await stop(serverName); - }); + for(var serverName in runningServers) { + let result, error = await stop(serverName); + } + console.log(`Stopped all servers`); } async function stop(serverName) { const runningServer = runningServers[serverName]; const pid = runningServer.process.pid; console.log(`Stopping '${serverName}' with PID='${pid}'`); - kill(pid, (error) => { - if(error) { - console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); - } - else { - console.log(`Stopped '${serverName}' with PID='${pid}'`); - } + + const stopped = new Promise((resolve, reject) => { + kill(pid, (error) => { + if(error) { + console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); + reject(error); + } + else { + console.log(`Stopped '${serverName}' with PID='${pid}'`); + resolve(`Stopped '${serverName}' with PID='${pid}'`); + } + }); }); + + return stopped; } async function run(server, options) { From 494b292238fa9c59f953fb0636b5be08563b6fd7 Mon Sep 17 00:00:00 2001 From: ziajka Date: Mon, 11 Feb 2019 11:10:31 +0100 Subject: [PATCH 04/13] Discover server as remote server --- .../servers/server-discovery/server-discovery.component.spec.ts | 1 + .../servers/server-discovery/server-discovery.component.ts | 2 ++ 2 files changed, 3 insertions(+) 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..cf4c253b 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 @@ -157,6 +157,7 @@ describe('ServerDiscoveryComponent', () => { expect(component.discoveredServer).toBeNull(); expect(mockedServerService.servers[0].ip).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..6b1cf10d 100644 --- a/src/app/components/servers/server-discovery/server-discovery.component.ts +++ b/src/app/components/servers/server-discovery/server-discovery.component.ts @@ -86,6 +86,8 @@ export class ServerDiscoveryComponent implements OnInit { server.name = server.ip; } + server.location = 'remote'; + this.serverService.create(server).then((created: Server) => { this.serverDatabase.addServer(created); this.discoveredServer = null; From 42d1f20d1541ba357366580d421b98e446cfc389 Mon Sep 17 00:00:00 2001 From: ziajka Date: Mon, 11 Feb 2019 11:24:26 +0100 Subject: [PATCH 05/13] Distinction between local and remote server; Refactor IP to HOST --- .../project-map/project-map.component.ts | 2 +- ...add-blank-project-dialog.component.spec.ts | 2 +- .../import-project-dialog.component.spec.ts | 2 +- .../import-project-dialog.component.ts | 2 +- .../components/servers/add-server-dialog.html | 9 +++++- .../server-discovery.component.html | 2 +- .../server-discovery.component.spec.ts | 18 ++++++------ .../server-discovery.component.ts | 10 +++---- .../components/servers/servers.component.html | 9 ++++-- .../components/servers/servers.component.ts | 28 +++++++++++++++++-- src/app/models/server.ts | 5 +++- src/app/services/http-server.service.spec.ts | 2 +- src/app/services/http-server.service.ts | 4 +-- src/app/services/project.service.ts | 2 +- src/app/services/server.service.spec.ts | 6 ++-- src/app/services/server.service.ts | 6 ++-- src/app/services/template.service.spec.ts | 2 +- src/app/services/testing.ts | 2 +- 18 files changed, 76 insertions(+), 37 deletions(-) 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..a4999abb 100644 --- a/src/app/components/servers/add-server-dialog.html +++ b/src/app/components/servers/add-server-dialog.html @@ -1,7 +1,14 @@

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 cf4c253b..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,7 +155,7 @@ 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 6b1cf10d..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,7 +83,7 @@ export class ServerDiscoveryComponent implements OnInit { accept(server: Server) { if (server.name == null) { - server.name = server.ip; + server.name = server.host; } server.location = 'remote'; diff --git a/src/app/components/servers/servers.component.html b/src/app/components/servers/servers.component.html index 2a6d868e..806475a5 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 }} diff --git a/src/app/components/servers/servers.component.ts b/src/app/components/servers/servers.component.ts index 3bce6ae0..681ded09 100644 --- a/src/app/components/servers/servers.component.ts +++ b/src/app/components/servers/servers.component.ts @@ -8,6 +8,7 @@ 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'; @Component({ selector: 'app-server-list', @@ -16,7 +17,7 @@ import { ServerDatabase } from '../../services/server.database'; }) export class ServersComponent implements OnInit { dataSource: ServerDataSource; - displayedColumns = ['id', 'name', 'ip', 'port', 'actions']; + displayedColumns = ['id', 'name', 'location', 'ip', 'port', 'actions']; constructor( private dialog: MatDialog, @@ -61,11 +62,34 @@ 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'; + } ngOnInit() { + this.locations = this.getLocations(); this.server.authorization = 'none'; + this.server.location = this.getDefaultLocation(); } onAddClick(): void { diff --git a/src/app/models/server.ts b/src/app/models/server.ts index d3ef061b..93894147 100644 --- a/src/app/models/server.ts +++ b/src/app/models/server.ts @@ -1,10 +1,13 @@ export type ServerAuthorization = 'basic' | 'none'; +export type ServerLocation = 'local' | 'remote'; export class Server { id: number; name: string; - ip: string; + location: ServerLocation; + host: string; port: number; + path: string; authorization: ServerAuthorization; login: string; password: string; 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.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; From 2cae9de2def3c8b1b94724176ef294ef7bb128be Mon Sep 17 00:00:00 2001 From: ziajka Date: Mon, 11 Feb 2019 11:26:32 +0100 Subject: [PATCH 06/13] Communication with local-server.js --- local-server.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/local-server.js b/local-server.js index 4efeae75..d3abe038 100644 --- a/local-server.js +++ b/local-server.js @@ -8,6 +8,10 @@ function getServerArguments(server, overrides) { return serverArguments; } +function getChannelForServer(server) { + return `local-server-run-${server.name}`; +} + async function stopAll() { for(var serverName in runningServers) { let result, error = await stop(serverName); @@ -64,6 +68,14 @@ async function main() { }); } +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"); From d772a0d9df4fa284fd71009df1537e6efb04b2eb Mon Sep 17 00:00:00 2001 From: ziajka Date: Mon, 11 Feb 2019 14:53:45 +0100 Subject: [PATCH 07/13] Basic starting and stopping servers on UI --- local-server.js | 46 ++++++++++++++++++ main.js | 48 ------------------- .../components/servers/add-server-dialog.html | 4 ++ .../components/servers/servers.component.html | 8 ++++ .../components/servers/servers.component.ts | 33 ++++++++++++- src/app/models/server.ts | 2 + src/app/services/server.database.ts | 8 ++++ 7 files changed, 100 insertions(+), 49 deletions(-) diff --git a/local-server.js b/local-server.js index d3abe038..2cc5a639 100644 --- a/local-server.js +++ b/local-server.js @@ -1,8 +1,50 @@ 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); +} + +exports.stopLocalServer = async (server) => { + return await stop(server); +} + function getServerArguments(server, overrides) { let serverArguments = []; return serverArguments; @@ -41,6 +83,10 @@ async function stop(serverName) { } async function run(server, options) { + if(!options) { + options = {}; + } + const logStdout = options.logStdout || false; console.log(`Running '${server.path}'`); diff --git a/main.js b/main.js index 74f42f20..776cee3a 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({ diff --git a/src/app/components/servers/add-server-dialog.html b/src/app/components/servers/add-server-dialog.html index a4999abb..489531be 100644 --- a/src/app/components/servers/add-server-dialog.html +++ b/src/app/components/servers/add-server-dialog.html @@ -8,6 +8,10 @@ + + + + diff --git a/src/app/components/servers/servers.component.html b/src/app/components/servers/servers.component.html index 806475a5..8f4ea9f9 100644 --- a/src/app/components/servers/servers.component.html +++ b/src/app/components/servers/servers.component.html @@ -35,6 +35,14 @@ Actions + + + + diff --git a/src/app/components/servers/servers.component.ts b/src/app/components/servers/servers.component.ts index 681ded09..f88cc52c 100644 --- a/src/app/components/servers/servers.component.ts +++ b/src/app/components/servers/servers.component.ts @@ -22,7 +22,8 @@ export class ServersComponent implements OnInit { constructor( private dialog: MatDialog, private serverService: ServerService, - private serverDatabase: ServerDatabase + private serverDatabase: ServerDatabase, + private electronService: ElectronService, ) {} ngOnInit() { @@ -47,11 +48,32 @@ 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.electronService.remote.require('./local-server.js').startLocalServer(server); + server.status = 'running'; + this.serverDatabase.update(server); + } + + async stopServer(server: Server) { + await this.electronService.remote.require('./local-server.js').stopLocalServer(server); + server.status = 'stopped'; + this.serverDatabase.update(server); + } } @Component({ @@ -86,13 +108,22 @@ export class AddServerDialogComponent implements OnInit { return 'remote'; } + getDefaultLocalServerPath() { + return this.electronService.remote.require('./local-server.js').getLocalServerPath(); + } + 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/models/server.ts b/src/app/models/server.ts index 93894147..2ad4ccd1 100644 --- a/src/app/models/server.ts +++ b/src/app/models/server.ts @@ -1,5 +1,6 @@ export type ServerAuthorization = 'basic' | 'none'; export type ServerLocation = 'local' | 'remote'; +export type ServerStatus = 'stopped' | 'starting' | 'running'; export class Server { id: number; @@ -12,4 +13,5 @@ export class Server { login: string; password: string; is_local: boolean; + status: ServerStatus; } diff --git a/src/app/services/server.database.ts b/src/app/services/server.database.ts index 4d8af007..fea41c42 100644 --- a/src/app/services/server.database.ts +++ b/src/app/services/server.database.ts @@ -29,4 +29,12 @@ export class ServerDatabase { this.dataChange.next(this.data.slice()); } } + + public update(server: Server) { + const index = this.data.indexOf(server); + if (index >= 0) { + this.data[index] = server; + this.dataChange.next(this.data.slice()); + } + } } From b00604cc39efc5211c8f5b5755c30a3606476f30 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 12 Feb 2019 13:05:37 +0100 Subject: [PATCH 08/13] Fix wrong toaster position for error/success, Fixes: #291 --- src/app/services/toaster.service.spec.ts | 5 ++++- src/app/services/toaster.service.ts | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) 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); + }); } } From af90fe46d05437bb771f30485765429a4b839979 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 12 Feb 2019 15:11:50 +0100 Subject: [PATCH 09/13] Communication betwen angular and local-server.js --- local-server.js | 117 ++++++++++++------ main.js | 5 + src/app/app.module.ts | 4 +- .../components/servers/servers.component.ts | 41 ++++-- .../default-layout.component.spec.ts | 35 ++++++ .../default-layout.component.ts | 24 +++- .../server-management.service.spec.ts | 93 ++++++++++++++ src/app/services/server-management.service.ts | 44 +++++++ src/app/services/server.database.ts | 10 +- 9 files changed, 322 insertions(+), 51 deletions(-) create mode 100644 src/app/services/server-management.service.spec.ts create mode 100644 src/app/services/server-management.service.ts diff --git a/local-server.js b/local-server.js index 2cc5a639..59a3d90b 100644 --- a/local-server.js +++ b/local-server.js @@ -38,11 +38,13 @@ exports.getLocalServerPath = async () => { } exports.startLocalServer = async (server) => { - return await run(server); + return await run(server, { + logStdout: true + }); } exports.stopLocalServer = async (server) => { - return await stop(server); + return await stop(server.name); } function getServerArguments(server, overrides) { @@ -54,56 +56,99 @@ function getChannelForServer(server) { return `local-server-run-${server.name}`; } +function notifyStatus(status) { + ipcMain.emit('local-server-status-events', status); +} + async function stopAll() { - for(var serverName in runningServers) { - let result, error = await stop(serverName); - } - console.log(`Stopped all servers`); + for(var serverName in runningServers) { + let result, error = await stop(serverName); + } + console.log(`Stopped all servers`); } async function stop(serverName) { - const runningServer = runningServers[serverName]; - const pid = runningServer.process.pid; - console.log(`Stopping '${serverName}' with PID='${pid}'`); + let pid = undefined; - const stopped = new Promise((resolve, reject) => { - kill(pid, (error) => { - if(error) { - console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); - reject(error); - } - else { - console.log(`Stopped '${serverName}' with PID='${pid}'`); - resolve(`Stopped '${serverName}' with PID='${pid}'`); - } - }); + 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`); + return; + } + + kill(pid, (error) => { + if(error) { + console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); + reject(error); + } + else { + console.log(`Stopped '${serverName}' with PID='${pid}'`); + resolve(`Stopped '${serverName}' with PID='${pid}'`); + } }); + }); - return stopped; + return stopped; } async function run(server, options) { - if(!options) { - 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) { + if(logStdout) { + console.log(data.toString()); } + }); - const logStdout = options.logStdout || false; + serverProcess.stderr.on('data', function(data) { + if(logSterr) { + console.log(data.toString()); + } + }); - console.log(`Running '${server.path}'`); - - let serverProcess = spawn(server.path, getServerArguments(server)); - - runningServers[server.name] = { - process: serverProcess - }; - - serverProcess.stdout.on('data', function(data) { - if(logStdout) { - 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) => { + + }); + + } + async function main() { await run({ name: 'my-local', diff --git a/main.js b/main.js index 776cee3a..4b85890c 100644 --- a/main.js +++ b/main.js @@ -65,6 +65,11 @@ function createWindow () { // when you should delete the corresponding element. mainWindow = null }); + + // forward event to renderer + electron.ipcMain.on('local-server-status-events', (event) => { + mainWindow.webContents.send('local-server-status-events', event); + }); } // This method will be called when Electron has finished 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/servers/servers.component.ts b/src/app/components/servers/servers.component.ts index f88cc52c..47cf7e61 100644 --- a/src/app/components/servers/servers.component.ts +++ b/src/app/components/servers/servers.component.ts @@ -1,29 +1,32 @@ -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', 'location', 'ip', 'port', 'actions']; + serverStatusSubscription: Subscription; constructor( private dialog: MatDialog, private serverService: ServerService, private serverDatabase: ServerDatabase, - private electronService: ElectronService, + private serverManagement: ServerManagementService, + private changeDetector: ChangeDetectorRef ) {} ngOnInit() { @@ -32,6 +35,28 @@ export class ServersComponent implements OnInit { }); 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() { @@ -64,15 +89,11 @@ export class ServersComponent implements OnInit { } async startServer(server: Server) { - await this.electronService.remote.require('./local-server.js').startLocalServer(server); - server.status = 'running'; - this.serverDatabase.update(server); + await this.serverManagement.start(server); } async stopServer(server: Server) { - await this.electronService.remote.require('./local-server.js').stopLocalServer(server); - server.status = 'stopped'; - this.serverDatabase.update(server); + await this.serverManagement.stop(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..0fd3312d 100644 --- a/src/app/layouts/default-layout/default-layout.component.spec.ts +++ b/src/app/layouts/default-layout/default-layout.component.spec.ts @@ -6,6 +6,10 @@ 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 { @@ -16,9 +20,13 @@ describe('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,23 @@ 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([]); + }); }); diff --git a/src/app/layouts/default-layout/default-layout.component.ts b/src/app/layouts/default-layout/default-layout.component.ts index 162f83c6..bd51b85f 100644 --- a/src/app/layouts/default-layout/default-layout.component.ts +++ b/src/app/layouts/default-layout/default-layout.component.ts @@ -1,5 +1,8 @@ import { ElectronService } from 'ngx-electron'; -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core'; +import { ServerManagementService } from '../../services/server-management.service'; +import { Subscription } from 'rxjs'; +import { ToasterService } from '../../services/toaster.service'; @Component({ selector: 'app-default-layout', @@ -7,15 +10,30 @@ 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; constructor( - private electronService: ElectronService + private electronService: ElectronService, + private serverManagement: ServerManagementService, + private toasterService: ToasterService ) {} ngOnInit() { this.isInstalledSoftwareAvailable = this.electronService.isElectronApp; + + this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => { + if(serverStatus.status === 'errored') { + this.toasterService.error(serverStatus.message); + } + }); + + } + + ngOnDestroy() { + this.serverStatusSubscription.unsubscribe(); } } 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..81d0313b --- /dev/null +++ b/src/app/services/server-management.service.ts @@ -0,0 +1,44 @@ +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"; + 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) { + await this.electronService.remote.require('./local-server.js').startLocalServer(server); + } + + async stop(server: Server) { + await this.electronService.remote.require('./local-server.js').stopLocalServer(server); + } + + 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 fea41c42..cd3abaea 100644 --- a/src/app/services/server.database.ts +++ b/src/app/services/server.database.ts @@ -30,8 +30,16 @@ export class ServerDatabase { } } + 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.data.indexOf(server); + const index = this.findIndex(server.name); if (index >= 0) { this.data[index] = server; this.dataChange.next(this.data.slice()); From 67595689337da13d06c21fe008064960f49299b2 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 12 Feb 2019 15:34:18 +0100 Subject: [PATCH 10/13] Update status of server when is already running --- local-server.js | 40 +++++++++++++------ .../components/servers/servers.component.ts | 8 ++++ src/app/services/server-management.service.ts | 4 ++ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/local-server.js b/local-server.js index 59a3d90b..08b776ba 100644 --- a/local-server.js +++ b/local-server.js @@ -47,6 +47,10 @@ exports.stopLocalServer = async (server) => { return await stop(server.name); } +exports.getRunningServers = () => { + return Object.keys(runningServers); +} + function getServerArguments(server, overrides) { let serverArguments = []; return serverArguments; @@ -81,18 +85,26 @@ async function stop(serverName) { 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 { - console.log(`Stopped '${serverName}' with PID='${pid}'`); - resolve(`Stopped '${serverName}' with PID='${pid}'`); - } + 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'` + }); + } }); }); @@ -134,11 +146,13 @@ async function run(server, options) { }); serverProcess.on('exit', (code, signal) => { - notifyStatus({ - serverName: server.name, - status: 'errored', - message: `Server '${server.name}' has exited with status='${code}'` - }); + if(code > 0) { + notifyStatus({ + serverName: server.name, + status: 'errored', + message: `Server '${server.name}' has exited with status='${code}'` + }); + } }); serverProcess.on('error', (err) => { diff --git a/src/app/components/servers/servers.component.ts b/src/app/components/servers/servers.component.ts index 47cf7e61..cec9f8bb 100644 --- a/src/app/components/servers/servers.component.ts +++ b/src/app/components/servers/servers.component.ts @@ -30,7 +30,15 @@ export class ServersComponent implements OnInit, OnDestroy { ) {} 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); }); diff --git a/src/app/services/server-management.service.ts b/src/app/services/server-management.service.ts index 81d0313b..9466c299 100644 --- a/src/app/services/server-management.service.ts +++ b/src/app/services/server-management.service.ts @@ -36,6 +36,10 @@ export class ServerManagementService implements OnDestroy { await this.electronService.remote.require('./local-server.js').stopLocalServer(server); } + getRunningServers() { + return this.electronService.remote.require('./local-server.js').getRunningServers(); + } + ngOnDestroy() { if(this.electronService.isElectronApp) { this.electronService.ipcRenderer.removeAllListeners(this.statusChannel); From 16185d24614ed24bdffbfceff03b3c3ab8c7cd0c Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 19 Feb 2019 15:03:33 +0100 Subject: [PATCH 11/13] Stopping all local servers when leaving application --- local-server.js | 62 ++++++++++--------- main.js | 3 +- .../components/servers/servers.component.ts | 5 +- .../default-layout.component.spec.ts | 23 ++++++- .../default-layout.component.ts | 25 +++++++- src/app/services/server-management.service.ts | 13 +++- 6 files changed, 95 insertions(+), 36 deletions(-) diff --git a/local-server.js b/local-server.js index 08b776ba..c957d182 100644 --- a/local-server.js +++ b/local-server.js @@ -8,7 +8,6 @@ const isWin = /^win/.test(process.platform); let runningServers = {}; - exports.getLocalServerPath = async () => { const distDirectory = path.join(__dirname, 'dist'); if (!fs.existsSync(distDirectory)) { @@ -51,13 +50,17 @@ exports.getRunningServers = () => { return Object.keys(runningServers); } +exports.stopAllLocalServers = async () => { + return await stopAll(); +} + function getServerArguments(server, overrides) { - let serverArguments = []; - return serverArguments; + let serverArguments = []; + return serverArguments; } function getChannelForServer(server) { - return `local-server-run-${server.name}`; + return `local-server-run-${server.name}`; } function notifyStatus(status) { @@ -66,7 +69,7 @@ function notifyStatus(status) { async function stopAll() { for(var serverName in runningServers) { - let result, error = await stop(serverName); + let result, error = await stop(serverName); } console.log(`Stopped all servers`); } @@ -159,38 +162,41 @@ async function run(server, options) { }); - } - async function main() { - await run({ - name: 'my-local', - path: 'c:\\Program Files\\GNS3\\gns3server.EXE', - port: 3080 - }, { - logStdout: true - }); + 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 - }); + const responseChannel = getChannelForServer(); + await run(server); + event.sender.send(responseChannel, { + success: true + }); +}); + + +ipcMain.on('before-quit', async function (event) { + console.log(event); }); if (require.main === module) { - process.on('SIGINT', function() { - console.log("Caught interrupt signal"); - stopAll(); - }); + process.on('SIGINT', function() { + console.log("Caught interrupt signal"); + stopAll(); + }); - process.on('unhandledRejection', (reason, promise) => { - console.log(`UnhandledRejection occured 'reason'`); - process.exit(1); - }); + process.on('unhandledRejection', (reason, promise) => { + console.log(`UnhandledRejection occured 'reason'`); + process.exit(1); + }); - main(); + main(); } \ No newline at end of file diff --git a/main.js b/main.js index 4b85890c..e123c388 100644 --- a/main.js +++ b/main.js @@ -59,13 +59,14 @@ 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 }); + // forward event to renderer electron.ipcMain.on('local-server-status-events', (event) => { mainWindow.webContents.send('local-server-status-events', event); diff --git a/src/app/components/servers/servers.component.ts b/src/app/components/servers/servers.component.ts index cec9f8bb..23d295ad 100644 --- a/src/app/components/servers/servers.component.ts +++ b/src/app/components/servers/servers.component.ts @@ -138,7 +138,10 @@ export class AddServerDialogComponent implements OnInit { } getDefaultLocalServerPath() { - return this.electronService.remote.require('./local-server.js').getLocalServerPath(); + if(this.electronService.isElectronApp) { + return this.electronService.remote.require('./local-server.js').getLocalServerPath(); + } + return; } ngOnInit() { 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 0fd3312d..db56d793 100644 --- a/src/app/layouts/default-layout/default-layout.component.spec.ts +++ b/src/app/layouts/default-layout/default-layout.component.spec.ts @@ -16,7 +16,7 @@ class ElectronServiceMock { public isElectronApp: boolean; } -describe('DefaultLayoutComponent', () => { +fdescribe('DefaultLayoutComponent', () => { let component: DefaultLayoutComponent; let fixture: ComponentFixture; let electronServiceMock: ElectronServiceMock; @@ -89,4 +89,25 @@ describe('DefaultLayoutComponent', () => { }); 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 bd51b85f..dac16e11 100644 --- a/src/app/layouts/default-layout/default-layout.component.ts +++ b/src/app/layouts/default-layout/default-layout.component.ts @@ -1,8 +1,9 @@ import { ElectronService } from 'ngx-electron'; -import { Component, OnInit, ViewEncapsulation, OnDestroy } 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', @@ -14,22 +15,42 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy { public isInstalledSoftwareAvailable = false; serverStatusSubscription: Subscription; + shouldStopServersOnClosing = true; constructor( private electronService: ElectronService, private serverManagement: ServerManagementService, - private toasterService: ToasterService + 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); } }); + // 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() { diff --git a/src/app/services/server-management.service.ts b/src/app/services/server-management.service.ts index 9466c299..48d605a1 100644 --- a/src/app/services/server-management.service.ts +++ b/src/app/services/server-management.service.ts @@ -29,15 +29,22 @@ export class ServerManagementService implements OnDestroy { } async start(server: Server) { - await this.electronService.remote.require('./local-server.js').startLocalServer(server); + return await this.electronService.remote.require('./local-server.js').startLocalServer(server); } async stop(server: Server) { - await this.electronService.remote.require('./local-server.js').stopLocalServer(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() { - return this.electronService.remote.require('./local-server.js').getRunningServers(); + if(this.electronService.isElectronApp) { + return this.electronService.remote.require('./local-server.js').getRunningServers(); + } + return []; } ngOnDestroy() { From cdadbd914006d43885395c2eb6dd19eb50eccc40 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 20 Feb 2019 11:32:54 +0100 Subject: [PATCH 12/13] Support of stderr CRITICAL issues forwarded to app --- local-server.js | 45 ++++++++++++++----- main.js | 4 +- .../default-layout.component.ts | 3 ++ src/app/services/server-management.service.ts | 2 +- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/local-server.js b/local-server.js index c957d182..299a9171 100644 --- a/local-server.js +++ b/local-server.js @@ -67,6 +67,19 @@ 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); @@ -137,6 +150,16 @@ async function run(server, options) { }; 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()); } @@ -149,17 +172,19 @@ async function run(server, options) { }); serverProcess.on('exit', (code, signal) => { - if(code > 0) { - notifyStatus({ - serverName: server.name, - status: 'errored', - message: `Server '${server.name}' has exited with status='${code}'` - }); - } + 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}` + }); }); } @@ -183,10 +208,6 @@ ipcMain.on('local-server-run', async function (event, server) { }); -ipcMain.on('before-quit', async function (event) { - console.log(event); -}); - if (require.main === module) { process.on('SIGINT', function() { console.log("Caught interrupt signal"); diff --git a/main.js b/main.js index e123c388..d111a956 100644 --- a/main.js +++ b/main.js @@ -63,7 +63,7 @@ function createWindow () { // 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; }); @@ -94,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/src/app/layouts/default-layout/default-layout.component.ts b/src/app/layouts/default-layout/default-layout.component.ts index dac16e11..32edf9b7 100644 --- a/src/app/layouts/default-layout/default-layout.component.ts +++ b/src/app/layouts/default-layout/default-layout.component.ts @@ -32,6 +32,9 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy { if(serverStatus.status === 'errored') { this.toasterService.error(serverStatus.message); } + if(serverStatus.status === 'stderr') { + this.toasterService.error(serverStatus.message); + } }); // stop servers only when in Electron diff --git a/src/app/services/server-management.service.ts b/src/app/services/server-management.service.ts index 48d605a1..5dbecd87 100644 --- a/src/app/services/server-management.service.ts +++ b/src/app/services/server-management.service.ts @@ -5,7 +5,7 @@ import { Subject } from 'rxjs'; export interface ServerStateEvent { serverName: string; - status: "started" | "errored" | "stopped"; + status: "started" | "errored" | "stopped" | "stderr"; message: string; } From 85a5ece8aef0f34ab4c2252876ac553ce1711e50 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 20 Feb 2019 12:36:09 +0100 Subject: [PATCH 13/13] Fix UnhandledRejection handle message --- local-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-server.js b/local-server.js index 299a9171..2f01fdcc 100644 --- a/local-server.js +++ b/local-server.js @@ -215,7 +215,7 @@ if (require.main === module) { }); process.on('unhandledRejection', (reason, promise) => { - console.log(`UnhandledRejection occured 'reason'`); + console.log(`UnhandledRejection occured '${reason}'`); process.exit(1); });