From af90fe46d05437bb771f30485765429a4b839979 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 12 Feb 2019 15:11:50 +0100 Subject: [PATCH] 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());