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) => {
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',

View File

@ -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

View File

@ -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,

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 { 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);
}
}

View File

@ -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<DefaultLayoutComponent>;
let electronServiceMock: ElectronServiceMock;
let serverManagementService;
beforeEach(async(() => {
electronServiceMock = new ElectronServiceMock();
serverManagementService = {
serverStatusChanged: new Subject<ServerStateEvent>()
};
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([]);
});
});

View File

@ -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();
}
}

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) {
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());