Communication betwen angular and local-server.js

This commit is contained in:
ziajka 2019-02-12 15:11:50 +01:00
parent b00604cc39
commit af90fe46d0
9 changed files with 322 additions and 51 deletions

View File

@ -38,11 +38,13 @@ exports.getLocalServerPath = async () => {
} }
exports.startLocalServer = async (server) => { exports.startLocalServer = async (server) => {
return await run(server); return await run(server, {
logStdout: true
});
} }
exports.stopLocalServer = async (server) => { exports.stopLocalServer = async (server) => {
return await stop(server); return await stop(server.name);
} }
function getServerArguments(server, overrides) { function getServerArguments(server, overrides) {
@ -54,56 +56,99 @@ function getChannelForServer(server) {
return `local-server-run-${server.name}`; return `local-server-run-${server.name}`;
} }
function notifyStatus(status) {
ipcMain.emit('local-server-status-events', status);
}
async function stopAll() { async function stopAll() {
for(var serverName in runningServers) { for(var serverName in runningServers) {
let result, error = await stop(serverName); let result, error = await stop(serverName);
} }
console.log(`Stopped all servers`); console.log(`Stopped all servers`);
} }
async function stop(serverName) { async function stop(serverName) {
const runningServer = runningServers[serverName]; let pid = undefined;
const pid = runningServer.process.pid;
console.log(`Stopping '${serverName}' with PID='${pid}'`);
const stopped = new Promise((resolve, reject) => { const runningServer = runningServers[serverName];
kill(pid, (error) => {
if(error) { if(runningServer !== undefined && runningServer.process) {
console.error(`Error occured during stopping '${serverName}' with PID='${pid}'`); pid = runningServer.process.pid;
reject(error); }
}
else { console.log(`Stopping '${serverName}' with PID='${pid}'`);
console.log(`Stopped '${serverName}' with PID='${pid}'`);
resolve(`Stopped '${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) { async function run(server, options) {
if(!options) { if(!options) {
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}'`); serverProcess.on('exit', (code, signal) => {
notifyStatus({
let serverProcess = spawn(server.path, getServerArguments(server)); serverName: server.name,
status: 'errored',
runningServers[server.name] = { message: `Server '${server.name}' has exited with status='${code}'`
process: serverProcess
};
serverProcess.stdout.on('data', function(data) {
if(logStdout) {
console.log(data.toString());
}
}); });
});
serverProcess.on('error', (err) => {
});
} }
async function main() { async function main() {
await run({ await run({
name: 'my-local', name: 'my-local',

View File

@ -65,6 +65,11 @@ function createWindow () {
// when you should delete the corresponding element. // when you should delete the corresponding element.
mainWindow = null mainWindow = null
}); });
// forward event to renderer
electron.ipcMain.on('local-server-status-events', (event) => {
mainWindow.webContents.send('local-server-status-events', event);
});
} }
// This method will be called when Electron has finished // This method will be called when Electron has finished

View File

@ -95,6 +95,7 @@ import { TextEditorDialogComponent } from './components/project-map/drawings-edi
import { InstalledSoftwareService } from './services/installed-software.service'; import { InstalledSoftwareService } from './services/installed-software.service';
import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service'; import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service';
import { PlatformService } from './services/platform.service'; import { PlatformService } from './services/platform.service';
import { ServerManagementService } from './services/server-management.service';
if (environment.production) { if (environment.production) {
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
@ -197,7 +198,8 @@ if (environment.production) {
ToolsService, ToolsService,
InstalledSoftwareService, InstalledSoftwareService,
ExternalSoftwareDefinitionService, ExternalSoftwareDefinitionService,
PlatformService PlatformService,
ServerManagementService
], ],
entryComponents: [ entryComponents: [
AddServerDialogComponent, AddServerDialogComponent,

View File

@ -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 { DataSource } from '@angular/cdk/collections';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { Observable, merge } from 'rxjs'; import { Observable, merge, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Server } from '../../models/server'; import { Server } from '../../models/server';
import { ServerService } from '../../services/server.service'; import { ServerService } from '../../services/server.service';
import { ServerDatabase } from '../../services/server.database'; import { ServerDatabase } from '../../services/server.database';
import { ElectronService } from 'ngx-electron'; import { ElectronService } from 'ngx-electron';
import { ServerManagementService } from '../../services/server-management.service';
@Component({ @Component({
selector: 'app-server-list', selector: 'app-server-list',
templateUrl: './servers.component.html', templateUrl: './servers.component.html',
styleUrls: ['./servers.component.css'] styleUrls: ['./servers.component.css']
}) })
export class ServersComponent implements OnInit { export class ServersComponent implements OnInit, OnDestroy {
dataSource: ServerDataSource; dataSource: ServerDataSource;
displayedColumns = ['id', 'name', 'location', 'ip', 'port', 'actions']; displayedColumns = ['id', 'name', 'location', 'ip', 'port', 'actions'];
serverStatusSubscription: Subscription;
constructor( constructor(
private dialog: MatDialog, private dialog: MatDialog,
private serverService: ServerService, private serverService: ServerService,
private serverDatabase: ServerDatabase, private serverDatabase: ServerDatabase,
private electronService: ElectronService, private serverManagement: ServerManagementService,
private changeDetector: ChangeDetectorRef
) {} ) {}
ngOnInit() { ngOnInit() {
@ -32,6 +35,28 @@ export class ServersComponent implements OnInit {
}); });
this.dataSource = new ServerDataSource(this.serverDatabase); this.dataSource = new ServerDataSource(this.serverDatabase);
this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => {
const server = this.serverDatabase.find(serverStatus.serverName);
if(!server) {
return;
}
if(serverStatus.status === 'stopped') {
server.status = 'stopped';
}
if(serverStatus.status === 'errored') {
server.status = 'stopped';
}
if(serverStatus.status === 'started') {
server.status = 'running';
}
this.serverDatabase.update(server);
this.changeDetector.detectChanges();
});
}
ngOnDestroy() {
this.serverStatusSubscription.unsubscribe();
} }
createModal() { createModal() {
@ -64,15 +89,11 @@ export class ServersComponent implements OnInit {
} }
async startServer(server: Server) { async startServer(server: Server) {
await this.electronService.remote.require('./local-server.js').startLocalServer(server); await this.serverManagement.start(server);
server.status = 'running';
this.serverDatabase.update(server);
} }
async stopServer(server: Server) { async stopServer(server: Server) {
await this.electronService.remote.require('./local-server.js').stopLocalServer(server); await this.serverManagement.stop(server);
server.status = 'stopped';
this.serverDatabase.update(server);
} }
} }

View File

@ -6,6 +6,10 @@ import { MatIconModule, MatMenuModule, MatToolbarModule, MatProgressSpinnerModul
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ProgressComponent } from '../../common/progress/progress.component'; import { ProgressComponent } from '../../common/progress/progress.component';
import { ProgressService } from '../../common/progress/progress.service'; import { ProgressService } from '../../common/progress/progress.service';
import { ServerManagementService, ServerStateEvent } from '../../services/server-management.service';
import { ToasterService } from '../../services/toaster.service';
import { MockedToasterService } from '../../services/toaster.service.spec';
import { Subject } from 'rxjs';
class ElectronServiceMock { class ElectronServiceMock {
@ -16,9 +20,13 @@ describe('DefaultLayoutComponent', () => {
let component: DefaultLayoutComponent; let component: DefaultLayoutComponent;
let fixture: ComponentFixture<DefaultLayoutComponent>; let fixture: ComponentFixture<DefaultLayoutComponent>;
let electronServiceMock: ElectronServiceMock; let electronServiceMock: ElectronServiceMock;
let serverManagementService;
beforeEach(async(() => { beforeEach(async(() => {
electronServiceMock = new ElectronServiceMock(); electronServiceMock = new ElectronServiceMock();
serverManagementService = {
serverStatusChanged: new Subject<ServerStateEvent>()
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DefaultLayoutComponent, ProgressComponent], declarations: [DefaultLayoutComponent, ProgressComponent],
@ -28,6 +36,14 @@ describe('DefaultLayoutComponent', () => {
provide: ElectronService, provide: ElectronService,
useValue: electronServiceMock useValue: electronServiceMock
}, },
{
provide: ServerManagementService,
useValue: serverManagementService
},
{
provide: ToasterService,
useClass: MockedToasterService
},
ProgressService ProgressService
] ]
}).compileComponents(); }).compileComponents();
@ -54,4 +70,23 @@ describe('DefaultLayoutComponent', () => {
component.ngOnInit(); component.ngOnInit();
expect(component.isInstalledSoftwareAvailable).toBeFalsy(); expect(component.isInstalledSoftwareAvailable).toBeFalsy();
}); });
it('should show error when server management service throw event', () => {
const toaster: MockedToasterService = TestBed.get(ToasterService);
serverManagementService.serverStatusChanged.next({
status: 'errored',
message: 'Message'
});
expect(toaster.errors).toEqual(['Message']);
});
it('should not show error when server management service throw event', () => {
component.ngOnDestroy();
const toaster: MockedToasterService = TestBed.get(ToasterService);
serverManagementService.serverStatusChanged.next({
status: 'errored',
message: 'Message'
});
expect(toaster.errors).toEqual([]);
});
}); });

View File

@ -1,5 +1,8 @@
import { ElectronService } from 'ngx-electron'; 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({ @Component({
selector: 'app-default-layout', selector: 'app-default-layout',
@ -7,15 +10,30 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
templateUrl: './default-layout.component.html', templateUrl: './default-layout.component.html',
styleUrls: ['./default-layout.component.css'] styleUrls: ['./default-layout.component.css']
}) })
export class DefaultLayoutComponent implements OnInit { export class DefaultLayoutComponent implements OnInit, OnDestroy {
public isInstalledSoftwareAvailable = false; public isInstalledSoftwareAvailable = false;
serverStatusSubscription: Subscription;
constructor( constructor(
private electronService: ElectronService private electronService: ElectronService,
private serverManagement: ServerManagementService,
private toasterService: ToasterService
) {} ) {}
ngOnInit() { ngOnInit() {
this.isInstalledSoftwareAvailable = this.electronService.isElectronApp; 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();
} }
} }

View File

@ -0,0 +1,93 @@
import { TestBed } from '@angular/core/testing';
import { ServerManagementService } from './server-management.service';
import { ElectronService } from 'ngx-electron';
import { Server } from '../models/server';
describe('ServerManagementService', () => {
let electronService;
let callbacks
let removed;
let server;
beforeEach(() => {
callbacks = [];
removed = [];
server = undefined;
electronService = {
isElectronApp: true,
ipcRenderer: {
on: (channel, callback) => {
callbacks.push({
channel: channel,
callback: callback
});
},
removeAllListeners: (name) => {
removed.push(name);
}
},
remote: {
require: (file) => {
return {
startLocalServer: (serv) => {
server = serv;
},
stopLocalServer: (serv) => {
server = serv;
}
}
}
}
};
})
beforeEach(() => TestBed.configureTestingModule({
providers: [
{ provide: ElectronService, useValue: electronService},
ServerManagementService
]
}));
it('should be created', () => {
const service: ServerManagementService = TestBed.get(ServerManagementService);
expect(service).toBeTruthy();
});
it('should attach when running as electron app', () => {
TestBed.get(ServerManagementService);
expect(callbacks.length).toEqual(1);
expect(callbacks[0].channel).toEqual('local-server-status-events');
});
it('should not attach when running as not electron app', () => {
electronService.isElectronApp = false;
TestBed.get(ServerManagementService);
expect(callbacks.length).toEqual(0);
});
it('should deattach when running as electron app', () => {
const service: ServerManagementService = TestBed.get(ServerManagementService);
service.ngOnDestroy();
expect(removed).toEqual(['local-server-status-events']);
});
it('should not deattach when running as not electron app', () => {
electronService.isElectronApp = false;
const service: ServerManagementService = TestBed.get(ServerManagementService);
service.ngOnDestroy();
expect(removed).toEqual([]);
});
it('should start local server', async () => {
const service: ServerManagementService = TestBed.get(ServerManagementService);
await service.start({ name: 'test'} as Server);
expect(server).toEqual({ name: 'test'});
});
it('should stop local server', async () => {
const service: ServerManagementService = TestBed.get(ServerManagementService);
await service.stop({ name: 'test2'} as Server);
expect(server).toEqual({ name: 'test2'});
});
});

View File

@ -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<ServerStateEvent>();
constructor(
private electronService: ElectronService
) {
if(this.electronService.isElectronApp) {
this.electronService.ipcRenderer.on(this.statusChannel, (event, data) => {
this.serverStatusChanged.next(data);
});
}
}
get statusChannel() {
return 'local-server-status-events';
}
async start(server: Server) {
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);
}
}
}

View File

@ -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) { public update(server: Server) {
const index = this.data.indexOf(server); const index = this.findIndex(server.name);
if (index >= 0) { if (index >= 0) {
this.data[index] = server; this.data[index] = server;
this.dataChange.next(this.data.slice()); this.dataChange.next(this.data.slice());