From 0c2fff6ac1d6e5fe03eea8785ab0f08c28d76133 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 5 Mar 2019 14:26:09 +0100 Subject: [PATCH 1/6] Basic OS Console executor --- console-executor.js | 33 ++++++++++++++ electron-builder.yml | 1 + src/app/app.module.ts | 4 +- .../console-device-action.component.html | 4 ++ .../console-device-action.component.spec.ts | 25 +++++++++++ .../console-device-action.component.ts | 43 +++++++++++++++++++ .../context-menu/context-menu.component.html | 6 +++ .../context-menu/context-menu.component.ts | 6 ++- src/app/services/server.service.ts | 4 ++ 9 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 console-executor.js create mode 100644 src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.html create mode 100644 src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts create mode 100644 src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts diff --git a/console-executor.js b/console-executor.js new file mode 100644 index 00000000..964f43da --- /dev/null +++ b/console-executor.js @@ -0,0 +1,33 @@ +const { spawn } = require('child_process'); + +exports.openConsole = async (consoleRequest) => { + const genericConsoleCommand = 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"'; + const command = prepareCommand(genericConsoleCommand, consoleRequest); + console.log(`Starting console with command: '${command}'`); + + let consoleProcess = spawn(command, [], { + shell :true + }); + + consoleProcess.stdout.on('data', (data) => { + console.log(`Console is producing: ${data.toString()}`); + }); +} + + +function prepareCommand(consoleCommand, consoleRequest) { + const mapping = { + h: consoleRequest.host, + p: consoleRequest.port, + d: consoleRequest.name, + i: consoleRequest.project_id, + n: consoleRequest.node_id, + c: consoleRequest.server_url + }; + + for(var key in mapping) { + const regExp = new RegExp(`%${key}`, 'g'); + consoleCommand = consoleCommand.replace(regExp, mapping[key]); + } + return consoleCommand; +} \ No newline at end of file diff --git a/electron-builder.yml b/electron-builder.yml index 3c465a61..fdaee391 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -15,6 +15,7 @@ files: - sentry.js - installed-software.js - local-server.js + - console-executor.js - package.json extraFiles: diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 223999cc..586395d3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -168,6 +168,7 @@ import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapsho import { DateFilter } from './filters/dateFilter.pipe'; import { NameFilter } from './filters/nameFilter.pipe'; import { CustomAdaptersComponent } from './components/preferences/common/custom-adapters/custom-adapters.component'; +import { ConsoleDeviceActionComponent } from './components/project-map/context-menu/actions/console-device-action/console-device-action.component'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -273,7 +274,8 @@ if (environment.production) { DateFilter, NameFilter, ListOfSnapshotsComponent, - CustomAdaptersComponent + CustomAdaptersComponent, + ConsoleDeviceActionComponent ], imports: [ BrowserModule, diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.html b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.html new file mode 100644 index 00000000..4ee59fbf --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts new file mode 100644 index 00000000..f254ff09 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsoleDeviceActionComponent } from './console-device-action.component'; + +describe('ConsoleDeviceActionComponent', () => { + let component: ConsoleDeviceActionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ConsoleDeviceActionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConsoleDeviceActionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts new file mode 100644 index 00000000..46bd67b7 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { ElectronService } from 'ngx-electron'; +import { Project } from '../../../../../models/project'; +import { ServerService } from '../../../../../services/server.service'; + +@Component({ + selector: 'app-console-device-action', + templateUrl: './console-device-action.component.html' +}) +export class ConsoleDeviceActionComponent implements OnInit { + @Input() server: Server; + @Input() project: Project; + @Input() nodes: Node[]; + + constructor( + private electronService: ElectronService, + private serverService: ServerService + ) { } + + ngOnInit() { + } + + async console() { + for(var node of this.nodes) { + const consoleRequest = { + type: node.console_type, + host: node.console_host, + port: node.console, + name: node.name, + project_id: this.project.project_id, + node_id: node.node_id, + server_url: this.serverService.getServerUrl(this.server) + }; + await this.openConsole(consoleRequest); + } + } + + async openConsole(request) { + return await this.electronService.remote.require('./console-executor.js').openConsole(request); + } +} diff --git a/src/app/components/project-map/context-menu/context-menu.component.html b/src/app/components/project-map/context-menu/context-menu.component.html index 0060c9f1..af129f3c 100644 --- a/src/app/components/project-map/context-menu/context-menu.component.html +++ b/src/app/components/project-map/context-menu/context-menu.component.html @@ -3,6 +3,12 @@ + this.indexedDbService.get().delete(this.tablename, server.id)); } + public getServerUrl(server: Server) { + return `http://${server.host}:${server.port}/`; + } + public getLocalServer(host: string, port: number) { const promise = new Promise((resolve, reject) => { this.findAll().then((servers: Server[]) => { From 267c88032f67652ddea4336569e9088568fb51d5 Mon Sep 17 00:00:00 2001 From: ziajka Date: Tue, 5 Mar 2019 15:12:47 +0100 Subject: [PATCH 2/6] Init console settings --- src/app/app-routing.module.ts | 2 ++ src/app/app.module.ts | 4 ++- .../settings/console/console.component.html | 26 +++++++++++++++++++ .../settings/console/console.component.scss | 0 .../console/console.component.spec.ts | 25 ++++++++++++++++++ .../settings/console/console.component.ts | 21 +++++++++++++++ .../settings/settings.component.html | 15 +++++++++++ .../settings/settings.component.scss | 4 +++ 8 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/app/components/settings/console/console.component.html create mode 100644 src/app/components/settings/console/console.component.scss create mode 100644 src/app/components/settings/console/console.component.spec.ts create mode 100644 src/app/components/settings/console/console.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index c5ebac59..d452aa73 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -51,6 +51,7 @@ import { CopyIosTemplateComponent } from './components/preferences/dynamips/copy import { CopyDockerTemplateComponent } from './components/preferences/docker/copy-docker-template/copy-docker-template.component'; import { CopyIouTemplateComponent } from './components/preferences/ios-on-unix/copy-iou-template/copy-iou-template.component'; import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapshots/list-of-snapshots.component'; +import { ConsoleComponent } from './components/settings/console/console.component'; const routes: Routes = [ { @@ -62,6 +63,7 @@ const routes: Routes = [ { path: 'local', component: LocalServerComponent }, { path: 'server/:server_id/projects', component: ProjectsComponent }, { path: 'settings', component: SettingsComponent }, + { path: 'settings/console', component: ConsoleComponent }, { path: 'installed-software', component: InstalledSoftwareComponent }, { path: 'server/:server_id/project/:project_id/snapshots', component: ListOfSnapshotsComponent }, { path: 'server/:server_id/preferences', component: PreferencesComponent }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 586395d3..0ba2aadf 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -169,6 +169,7 @@ import { DateFilter } from './filters/dateFilter.pipe'; import { NameFilter } from './filters/nameFilter.pipe'; import { CustomAdaptersComponent } from './components/preferences/common/custom-adapters/custom-adapters.component'; import { ConsoleDeviceActionComponent } from './components/project-map/context-menu/actions/console-device-action/console-device-action.component'; +import { ConsoleComponent } from './components/settings/console/console.component'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -275,7 +276,8 @@ if (environment.production) { NameFilter, ListOfSnapshotsComponent, CustomAdaptersComponent, - ConsoleDeviceActionComponent + ConsoleDeviceActionComponent, + ConsoleComponent ], imports: [ BrowserModule, diff --git a/src/app/components/settings/console/console.component.html b/src/app/components/settings/console/console.component.html new file mode 100644 index 00000000..530e7305 --- /dev/null +++ b/src/app/components/settings/console/console.component.html @@ -0,0 +1,26 @@ +
+
+
+

Console settings

+
+
+
+ +
+ + + + + + +
+
+
+ +
+
+
+
diff --git a/src/app/components/settings/console/console.component.scss b/src/app/components/settings/console/console.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/settings/console/console.component.spec.ts b/src/app/components/settings/console/console.component.spec.ts new file mode 100644 index 00000000..e8ebf2a1 --- /dev/null +++ b/src/app/components/settings/console/console.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsoleComponent } from './console.component'; + +describe('ConsoleComponent', () => { + let component: ConsoleComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ConsoleComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConsoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/settings/console/console.component.ts b/src/app/components/settings/console/console.component.ts new file mode 100644 index 00000000..4fb1e09b --- /dev/null +++ b/src/app/components/settings/console/console.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-console', + templateUrl: './console.component.html', + styleUrls: ['./console.component.scss'] +}) +export class ConsoleComponent implements OnInit { + + consoleForm = new FormGroup({ + 'runner': new FormControl('', [ Validators.required ]), + 'command': new FormControl(''), + }); + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index ef4d5caf..d0e61074 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -27,6 +27,21 @@ > + + + + Console settings + Customize console settings + + +
+ + + mode_edit + +
+ +
diff --git a/src/app/components/settings/settings.component.scss b/src/app/components/settings/settings.component.scss index e69de29b..869dd968 100644 --- a/src/app/components/settings/settings.component.scss +++ b/src/app/components/settings/settings.component.scss @@ -0,0 +1,4 @@ + +.example-full-width { + width: 100%; +} \ No newline at end of file From c5b783c7229e26cef891b4b3ccd411fa3c281cce Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 10 Apr 2019 13:14:02 +0200 Subject: [PATCH 3/6] Download PuTTY from external URL, Ref: #368 --- scripts/build.py | 78 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/scripts/build.py b/scripts/build.py index c67e38a9..6608ae8e 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -40,6 +40,7 @@ SOURCE_DESTINATION = os.path.join(WORKING_DIR, 'source') BINARIES_EXTENSION = platform.system() == "Windows" and ".exe" or "" DEPENDENCIES = { 'ubridge': { + 'type': 'github', 'releases': 'https://api.github.com/repos/GNS3/ubridge/releases', 'version': 'LATEST', 'files': { @@ -50,6 +51,7 @@ DEPENDENCIES = { } }, 'vpcs': { + 'type': 'github', 'releases': 'https://api.github.com/repos/GNS3/vpcs/releases', 'version': '0.6.1', 'files': { @@ -60,6 +62,7 @@ DEPENDENCIES = { } }, 'dynamips': { + 'type': 'github', 'releases': 'https://api.github.com/repos/GNS3/dynamips/releases', 'version': '0.2.17', 'files': { @@ -69,6 +72,16 @@ DEPENDENCIES = { 'nvram_export.exe' ] } + }, + 'putty': { + 'type': 'http', + 'url': 'https://the.earth.li/~sgtatham/putty/{version}/w64/putty.exe', + 'version': '0.71', + 'files': { + 'windows': [ + 'putty.exe', + ] + } } } @@ -113,31 +126,54 @@ def prepare(): os.makedirs(WORKING_DIR, exist_ok=True) +def download_from_github(name, definition, output_directory): + response = requests.get(definition['releases']) + response.raise_for_status() + releases = response.json() + + if definition['version'] == 'LATEST': + release = releases[0] + else: + release = list(filter(lambda x: x['tag_name'] == "v{}".format(definition['version']), releases))[0] + + dependency_dir = os.path.join(output_directory, name) + os.makedirs(dependency_dir, exist_ok=True) + + files = [] + if platform.system() == "Windows": + files = definition['files']['windows'] + + for filename in files: + dependency_file = os.path.join(dependency_dir, filename) + dependency_url = list(filter(lambda x: x['name'] == filename, release['assets']))[0]['browser_download_url'] + download(dependency_url, dependency_file) + print('Downloaded {} to {}'.format(filename, dependency_file)) + + +def download_from_http(name, definition, output_directory): + url = definition['url'].format(version=definition['version']) + + dependency_dir = os.path.join(output_directory, name) + os.makedirs(dependency_dir, exist_ok=True) + + files = [] + if platform.system() == "Windows": + files = definition['files']['windows'] + + for filename in files: + dependency_file = os.path.join(dependency_dir, filename) + download(url, dependency_file) + print('Downloaded {} to {}'.format(filename, dependency_file)) + + def download_dependencies_command(arguments): output_directory = os.path.join(os.getcwd(), arguments.b) for name, definition in DEPENDENCIES.items(): - response = requests.get(definition['releases']) - response.raise_for_status() - releases = response.json() - - if definition['version'] == 'LATEST': - release = releases[0] - else: - release = list(filter(lambda x: x['tag_name'] == "v{}".format(definition['version']), releases))[0] - - dependency_dir = os.path.join(output_directory, name) - os.makedirs(dependency_dir, exist_ok=True) - - files = [] - if platform.system() == "Windows": - files = definition['files']['windows'] - - for filename in files: - dependency_file = os.path.join(dependency_dir, filename) - dependency_url = list(filter(lambda x: x['name'] == filename, release['assets']))[0]['browser_download_url'] - download(dependency_url, dependency_file) - print('Downloaded {} to {}'.format(filename, dependency_file)) + if definition['type'] == 'github': + download_from_github(name, definition, output_directory) + if definition['type'] == 'http': + download_from_http(name, definition, output_directory) From f6b50526f4baa40e4ef9467da702c3e999164d57 Mon Sep 17 00:00:00 2001 From: ziajka Date: Fri, 12 Apr 2019 12:16:46 +0200 Subject: [PATCH 4/6] Basic PuTTY support, Ref. #368 --- console-executor.js | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/console-executor.js b/console-executor.js index 964f43da..c8364fd9 100644 --- a/console-executor.js +++ b/console-executor.js @@ -1,8 +1,32 @@ const { spawn } = require('child_process'); +const { app } = require('electron'); +const path = require('path'); + +async function setPATHEnv() { + const puttyLookup = [ + path.join(__dirname, 'dist', 'putty'), + path.join(path.dirname(app.getPath('exe')), 'dist', 'putty') + ]; + + // prevent adding duplicates + let extra = [ + ...puttyLookup, + ].filter((dir) => { + return process.env.PATH.indexOf(dir) < 0; + }); + extra.push(process.env.PATH); + process.env.PATH = extra.join(";"); +} exports.openConsole = async (consoleRequest) => { - const genericConsoleCommand = 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"'; + // const genericConsoleCommand = 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"'; + const genericConsoleCommand = 'putty.exe -telnet %h %p -loghost "%d"'; + const command = prepareCommand(genericConsoleCommand, consoleRequest); + + console.log(`Setting up PATH`); + await setPATHEnv(); + console.log(`Starting console with command: '${command}'`); let consoleProcess = spawn(command, [], { @@ -10,7 +34,15 @@ exports.openConsole = async (consoleRequest) => { }); consoleProcess.stdout.on('data', (data) => { - console.log(`Console is producing: ${data.toString()}`); + console.log(`Console stdout is producing: ${data.toString()}`); + }); + + consoleProcess.stderr.on('data', (data) => { + console.log(`Console stderr is producing: ${data.toString()}`); + }); + + consoleProcess.on('close', (code) => { + console.log(`child process exited with code ${code}`); }); } From b6ed486ce0ae3e5b91ee858881efe659de0cf6e0 Mon Sep 17 00:00:00 2001 From: ziajka Date: Wed, 17 Apr 2019 11:16:35 +0200 Subject: [PATCH 5/6] Console setting edition --- src/app/app.module.ts | 6 ++++- .../console-device-action.component.ts | 25 +++++++++++++++++- .../settings/console/console.component.html | 20 +++++++------- .../settings/console/console.component.scss | 3 +++ .../settings/console/console.component.ts | 23 ++++++++++++++-- .../settings/settings.component.html | 6 ++--- .../settings/settings.component.scss | 4 --- .../components/settings/settings.component.ts | 8 +++++- src/app/services/settings.service.ts | 4 ++- .../services/settings/console.service.spec.ts | 12 +++++++++ src/app/services/settings/console.service.ts | 24 +++++++++++++++++ .../settings/default-console.service.spec.ts | 12 +++++++++ .../settings/default-console.service.ts | 26 +++++++++++++++++++ src/styles.css | 4 +++ 14 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 src/app/services/settings/console.service.spec.ts create mode 100644 src/app/services/settings/console.service.ts create mode 100644 src/app/services/settings/default-console.service.spec.ts create mode 100644 src/app/services/settings/default-console.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b8f48dd8..a970b723 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -180,6 +180,8 @@ import { StartCaptureDialogComponent } from './components/project-map/packet-cap import { SuspendLinkActionComponent } from './components/project-map/context-menu/actions/suspend-link/suspend-link-action.component'; import { ResumeLinkActionComponent } from './components/project-map/context-menu/actions/resume-link-action/resume-link-action.component'; import { StopCaptureActionComponent } from './components/project-map/context-menu/actions/stop-capture/stop-capture-action.component'; +import { ConsoleService } from './services/settings/console.service'; +import { DefaultConsoleService } from './services/settings/default-console.service'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -365,7 +367,9 @@ if (environment.production) { IouService, IouConfigurationService, RecentlyOpenedProjectService, - ServerManagementService + ServerManagementService, + ConsoleService, + DefaultConsoleService ], entryComponents: [ AddServerDialogComponent, diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts index 46bd67b7..64f0140a 100644 --- a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts @@ -4,6 +4,8 @@ import { Server } from '../../../../../models/server'; import { ElectronService } from 'ngx-electron'; import { Project } from '../../../../../models/project'; import { ServerService } from '../../../../../services/server.service'; +import { SettingsService } from '../../../../../services/settings.service'; +import { ToasterService } from '../../../../../services/toaster.service'; @Component({ selector: 'app-console-device-action', @@ -16,15 +18,36 @@ export class ConsoleDeviceActionComponent implements OnInit { constructor( private electronService: ElectronService, - private serverService: ServerService + private serverService: ServerService, + private settingsService: SettingsService, + private toasterService: ToasterService ) { } ngOnInit() { } async console() { + const consoleCommand = this.settingsService.get('console_command'); + + if(consoleCommand === undefined) { + this.toasterService.error('Console command is not defined. Please change it in the Settings.'); + return; + } + + const startedNodes = this.nodes.filter(node => node.status === 'started'); + + if(startedNodes.length === 0) { + this.toasterService.error('Device needs to be started in order to console to it.'); + return; + } + for(var node of this.nodes) { + if(node.status !== 'started') { + continue; + } + const consoleRequest = { + command: consoleCommand, type: node.console_type, host: node.console_host, port: node.console, diff --git a/src/app/components/settings/console/console.component.html b/src/app/components/settings/console/console.component.html index 530e7305..67c2316f 100644 --- a/src/app/components/settings/console/console.component.html +++ b/src/app/components/settings/console/console.component.html @@ -7,20 +7,22 @@
- - - - - + + +
The following variables are replaced by GNS3:
+ %h: console IP or hostname
+ %p: console port
+ %s: path of the serial connection
+ %d: title of the console
+ %i: Project UUID
+ %c: server URL (http://user:password@server:port) +
-
+
diff --git a/src/app/components/settings/console/console.component.scss b/src/app/components/settings/console/console.component.scss index e69de29b..6955ed73 100644 --- a/src/app/components/settings/console/console.component.scss +++ b/src/app/components/settings/console/console.component.scss @@ -0,0 +1,3 @@ +.help { + font-size: 14px; +} \ No newline at end of file diff --git a/src/app/components/settings/console/console.component.ts b/src/app/components/settings/console/console.component.ts index 4fb1e09b..f043bc5f 100644 --- a/src/app/components/settings/console/console.component.ts +++ b/src/app/components/settings/console/console.component.ts @@ -1,5 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ConsoleService } from '../../../services/settings/console.service'; +import { ToasterService } from '../../../services/toaster.service'; @Component({ selector: 'app-console', @@ -9,13 +12,29 @@ import { FormGroup, FormControl, Validators } from '@angular/forms'; export class ConsoleComponent implements OnInit { consoleForm = new FormGroup({ - 'runner': new FormControl('', [ Validators.required ]), 'command': new FormControl(''), }); - constructor() { } + constructor( + private router: Router, + private consoleService: ConsoleService, + private toasterService: ToasterService + ) { } ngOnInit() { + const commandControl = this.consoleForm.get('command'); + commandControl.setValue(this.consoleService.command); + } + + goBack() { + this.router.navigate(['/settings']); + } + + save() { + const formValue = this.consoleForm.value; + this.consoleService.command = formValue.command; + this.toasterService.success("Console command has been updated."); + this.goBack(); } } diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index 3ce06211..dbdbb0f7 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -28,15 +28,15 @@ --> - + Console settings Customize console settings
- - + + mode_edit
diff --git a/src/app/components/settings/settings.component.scss b/src/app/components/settings/settings.component.scss index 869dd968..e69de29b 100644 --- a/src/app/components/settings/settings.component.scss +++ b/src/app/components/settings/settings.component.scss @@ -1,4 +0,0 @@ - -.example-full-width { - width: 100%; -} \ No newline at end of file diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 638799d7..96f08cbd 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { SettingsService } from '../../services/settings.service'; import { ToasterService } from '../../services/toaster.service'; +import { ConsoleService } from '../../services/settings/console.service'; @Component({ selector: 'app-settings', @@ -9,11 +10,16 @@ import { ToasterService } from '../../services/toaster.service'; }) export class SettingsComponent implements OnInit { settings = { ...SettingsService.DEFAULTS }; + consoleCommand: string; - constructor(private settingsService: SettingsService, private toaster: ToasterService) {} + constructor( + private settingsService: SettingsService, + private toaster: ToasterService, + private consoleService: ConsoleService) {} ngOnInit() { this.settings = this.settingsService.getAll(); + this.consoleCommand = this.consoleService.command; } save() { diff --git a/src/app/services/settings.service.ts b/src/app/services/settings.service.ts index 7934f857..2fc48050 100644 --- a/src/app/services/settings.service.ts +++ b/src/app/services/settings.service.ts @@ -6,6 +6,7 @@ export interface Settings { crash_reports: boolean; experimental_features: boolean; angular_map: boolean; + console_command: string; } @Injectable() @@ -13,7 +14,8 @@ export class SettingsService { static DEFAULTS: Settings = { crash_reports: true, experimental_features: false, - angular_map: false + angular_map: false, + console_command: undefined }; private settingsSubject: BehaviorSubject; diff --git a/src/app/services/settings/console.service.spec.ts b/src/app/services/settings/console.service.spec.ts new file mode 100644 index 00000000..30a37977 --- /dev/null +++ b/src/app/services/settings/console.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { ConsoleService } from './console.service'; + +describe('ConsoleService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: ConsoleService = TestBed.get(ConsoleService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/settings/console.service.ts b/src/app/services/settings/console.service.ts new file mode 100644 index 00000000..0f8a04a2 --- /dev/null +++ b/src/app/services/settings/console.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { DefaultConsoleService } from './default-console.service'; +import { SettingsService } from '../settings.service'; + +@Injectable() +export class ConsoleService { + + constructor( + private defaultConsoleService: DefaultConsoleService, + private settingsService: SettingsService + ) { } + + get command(): string { + const command = this.settingsService.get('console_command'); + if(command === undefined) { + return this.defaultConsoleService.get(); + } + return command; + } + + set command(command: string) { + this.settingsService.set('console_command', command); + } +} diff --git a/src/app/services/settings/default-console.service.spec.ts b/src/app/services/settings/default-console.service.spec.ts new file mode 100644 index 00000000..167e0f02 --- /dev/null +++ b/src/app/services/settings/default-console.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { DefaultConsoleService } from './default-console.service'; + +describe('DefaultConsoleService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: DefaultConsoleService = TestBed.get(DefaultConsoleService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/settings/default-console.service.ts b/src/app/services/settings/default-console.service.ts new file mode 100644 index 00000000..0120a68b --- /dev/null +++ b/src/app/services/settings/default-console.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { ElectronService } from 'ngx-electron'; + +@Injectable() +export class DefaultConsoleService { + + constructor( + private electronService: ElectronService + ) { } + + get() { + if(!this.electronService.isElectronApp) { + return undefined; + } + + if(this.electronService.isLinux) { + return 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"' + } + + if(this.electronService.isWindows) { + return 'putty.exe -telnet %h %p -loghost "%d"' + } + + return undefined; + } +} diff --git a/src/styles.css b/src/styles.css index fc243b2c..a10f8c16 100644 --- a/src/styles.css +++ b/src/styles.css @@ -26,6 +26,10 @@ a.table-link { } } +.full-width-field { + width: 100%; +} + app-root { width: 100%; } From 16eeb8638a901beca721cbde348ca0572dada265 Mon Sep 17 00:00:00 2001 From: ziajka Date: Thu, 25 Apr 2019 10:13:13 +0200 Subject: [PATCH 6/6] Console command tests --- .../helpers/drawings-factory.spec.ts | 0 .../console-device-action.component.spec.ts | 101 ++++++++++++++++++ .../console-device-action.component.ts | 4 +- .../context-menu/context-menu.component.html | 1 - .../context-menu.component.spec.ts | 12 ++- .../console/console.component.spec.ts | 47 ++++++++ .../settings/settings.component.spec.ts | 20 +++- src/app/services/server.service.spec.ts | 4 + src/app/services/settings.service.spec.ts | 16 ++- .../services/settings/console.service.spec.ts | 25 ++++- .../settings/default-console.service.spec.ts | 39 ++++++- 11 files changed, 253 insertions(+), 16 deletions(-) delete mode 100644 src/app/cartography/helpers/drawings-factory.spec.ts diff --git a/src/app/cartography/helpers/drawings-factory.spec.ts b/src/app/cartography/helpers/drawings-factory.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts index f254ff09..4d2dd290 100644 --- a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts @@ -1,13 +1,57 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatIconModule } from '@angular/material'; +import { ElectronService } from 'ngx-electron'; import { ConsoleDeviceActionComponent } from './console-device-action.component'; +import { ServerService } from '../../../../../services/server.service'; +import { MockedServerService } from '../../../../../services/server.service.spec'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MockedToasterService } from '../../../../../services/toaster.service.spec'; +import { SettingsService } from '../../../../../services/settings.service'; +import { MockedSettingsService } from '../../../../../services/settings.service.spec'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; + describe('ConsoleDeviceActionComponent', () => { let component: ConsoleDeviceActionComponent; let fixture: ComponentFixture; + let electronService; + let server: Server; + let mockedSettingsService: MockedSettingsService; + let mockedServerService: MockedServerService; + let mockedToaster: MockedToasterService + + beforeEach(() => { + electronService = { + isElectronApp: true, + remote: { + require: (file) => { + return { + openConsole() {} + } + } + } + }; + + mockedSettingsService = new MockedSettingsService(); + mockedServerService = new MockedServerService(); + mockedToaster = new MockedToasterService(); + + server = { host: 'localhost', 'port': 222} as Server; + }) beforeEach(async(() => { TestBed.configureTestingModule({ + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: ServerService, useValue: mockedServerService }, + { provide: SettingsService, useValue: mockedSettingsService }, + { provide: ToasterService, useValue: mockedToaster } + ], + imports: [ + MatIconModule + ], declarations: [ ConsoleDeviceActionComponent ] }) .compileComponents(); @@ -22,4 +66,61 @@ describe('ConsoleDeviceActionComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('console to nodes', () => { + let nodes: Node[]; + + beforeEach(() => { + nodes = [{ + status: 'started', + console_type: 'telnet', + console_host: 'host', + console: 999, + name: 'Node 1', + project_id: '1111', + node_id: '2222', + } as Node]; + + component.nodes = nodes; + component.server = server; + + mockedSettingsService.set('console_command', 'command'); + spyOn(component, 'openConsole'); + }); + + it('should console to device', async () => { + await component.console(); + + expect(component.openConsole).toHaveBeenCalledWith({ + command: 'command', + type: 'telnet', + host: 'host', + port: 999, + name: 'Node 1', + project_id: '1111', + node_id: '2222', + server_url: 'localhost:222' + }); + }); + + it('should show message when command is not defined', async () => { + mockedSettingsService.set('console_command', undefined); + await component.console(); + expect(component.openConsole).not.toHaveBeenCalled(); + }); + + it('should show message when there is no started nodes', async () => { + nodes[0]['status'] = 'stopped'; + await component.console(); + expect(component.openConsole).not.toHaveBeenCalled(); + }); + + it('should only start running nodes', async () => { + nodes.push({ + status: 'stopped' + } as Node); + await component.console(); + expect(component.openConsole).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts index 64f0140a..2cf30188 100644 --- a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, Input } from '@angular/core'; import { Node } from '../../../../../cartography/models/node'; import { Server } from '../../../../../models/server'; import { ElectronService } from 'ngx-electron'; -import { Project } from '../../../../../models/project'; import { ServerService } from '../../../../../services/server.service'; import { SettingsService } from '../../../../../services/settings.service'; import { ToasterService } from '../../../../../services/toaster.service'; @@ -13,7 +12,6 @@ import { ToasterService } from '../../../../../services/toaster.service'; }) export class ConsoleDeviceActionComponent implements OnInit { @Input() server: Server; - @Input() project: Project; @Input() nodes: Node[]; constructor( @@ -52,7 +50,7 @@ export class ConsoleDeviceActionComponent implements OnInit { host: node.console_host, port: node.console, name: node.name, - project_id: this.project.project_id, + project_id: node.project_id, node_id: node.node_id, server_url: this.serverService.getServerUrl(this.server) }; diff --git a/src/app/components/project-map/context-menu/context-menu.component.html b/src/app/components/project-map/context-menu/context-menu.component.html index 47ddcf68..0d80158f 100644 --- a/src/app/components/project-map/context-menu/context-menu.component.html +++ b/src/app/components/project-map/context-menu/context-menu.component.html @@ -5,7 +5,6 @@ diff --git a/src/app/components/project-map/context-menu/context-menu.component.spec.ts b/src/app/components/project-map/context-menu/context-menu.component.spec.ts index 2fdfae66..ab01063f 100644 --- a/src/app/components/project-map/context-menu/context-menu.component.spec.ts +++ b/src/app/components/project-map/context-menu/context-menu.component.spec.ts @@ -9,17 +9,23 @@ import { Drawing } from '../../../cartography/models/drawing'; import { RectElement } from '../../../cartography/models/drawings/rect-element'; import { TextElement } from '../../../cartography/models/drawings/text-element'; import { Server } from '../../../models/server'; +import { ElectronService } from 'ngx-electron'; describe('ContextMenuComponent', () => { let component: ContextMenuComponent; let fixture: ComponentFixture; beforeEach(async(() => { + const electronMock = { + isElectronApp: true + }; + TestBed.configureTestingModule({ imports: [MatMenuModule, BrowserModule], providers: [ { provide: ChangeDetectorRef }, - { provide: ProjectService, useClass: MockedProjectService } + { provide: ProjectService, useClass: MockedProjectService }, + { provide: ElectronService, useValue: electronMock} ], declarations: [ContextMenuComponent], schemas: [NO_ERRORS_SCHEMA] @@ -37,6 +43,10 @@ describe('ContextMenuComponent', () => { expect(component).toBeTruthy(); }); + it('should define property if running in electron ', () => { + expect(component.isElectronApp).toBeTruthy(); + }); + it('should reset capabilities while opening menu for node', () => { component.contextMenu = { openMenu() {} } as MatMenuTrigger; var spy = spyOn(component, 'resetCapabilities'); diff --git a/src/app/components/settings/console/console.component.spec.ts b/src/app/components/settings/console/console.component.spec.ts index e8ebf2a1..b79cf69e 100644 --- a/src/app/components/settings/console/console.component.spec.ts +++ b/src/app/components/settings/console/console.component.spec.ts @@ -1,13 +1,40 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ConsoleComponent } from './console.component'; +import { MatFormFieldModule, MatCardModule, MatInputModule } from '@angular/material'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ConsoleService } from '../../../services/settings/console.service'; +import { ToasterService } from '../../../services/toaster.service'; +import { MockedToasterService } from '../../../services/toaster.service.spec'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; + describe('ConsoleComponent', () => { let component: ConsoleComponent; let fixture: ComponentFixture; + let consoleService; + let router; + let toaster: MockedToasterService; beforeEach(async(() => { + consoleService = { + command: 'command' + }; + + router = { + navigate: jasmine.createSpy('navigate') + }; + + toaster = new MockedToasterService(); + TestBed.configureTestingModule({ + providers: [ + { provide: ConsoleService, useValue: consoleService }, + { provide: ToasterService, useValue: toaster }, + { provide: Router, useValue: router} + ], + imports: [ FormsModule, ReactiveFormsModule, MatFormFieldModule, MatCardModule, MatInputModule, NoopAnimationsModule ], declarations: [ ConsoleComponent ] }) .compileComponents(); @@ -22,4 +49,24 @@ describe('ConsoleComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set default command', () => { + component.ngOnInit(); + expect(component.consoleForm.value.command).toEqual('command'); + }) + + it('should go back', () => { + component.goBack(); + expect(router.navigate).toHaveBeenCalledWith(['/settings']); + }); + + + it('should update console command', () => { + component.consoleForm.get('command').setValue('newCommand'); + spyOn(component, 'goBack'); + component.save(); + expect(toaster.success.length).toEqual(1); + expect(component.goBack).toHaveBeenCalled(); + }); + }); diff --git a/src/app/components/settings/settings.component.spec.ts b/src/app/components/settings/settings.component.spec.ts index 3b0c7f31..ce8b4b3b 100644 --- a/src/app/components/settings/settings.component.spec.ts +++ b/src/app/components/settings/settings.component.spec.ts @@ -1,5 +1,5 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatCheckboxModule, MatExpansionModule } from '@angular/material'; +import { MatCheckboxModule, MatExpansionModule, MatIconModule, MatFormFieldModule, MatInputModule } from '@angular/material'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -9,16 +9,26 @@ import { SettingsComponent } from './settings.component'; import { SettingsService } from '../../services/settings.service'; import { ToasterService } from '../../services/toaster.service'; import { MockedToasterService } from '../../services/toaster.service.spec'; +import { ConsoleService } from '../../services/settings/console.service'; describe('SettingsComponent', () => { let component: SettingsComponent; let fixture: ComponentFixture; let settingsService: SettingsService; + let consoleService; beforeEach(async(() => { + consoleService = { + command: 'command' + }; + TestBed.configureTestingModule({ - imports: [MatExpansionModule, MatCheckboxModule, FormsModule, PersistenceModule, BrowserAnimationsModule], - providers: [SettingsService, { provide: ToasterService, useClass: MockedToasterService }], + imports: [MatExpansionModule, MatCheckboxModule, FormsModule, PersistenceModule, BrowserAnimationsModule, MatIconModule, MatFormFieldModule, MatInputModule], + providers: [ + SettingsService, + { provide: ToasterService, useClass: MockedToasterService }, + { provide: ConsoleService, useValue: consoleService} + ], declarations: [SettingsComponent] }).compileComponents(); @@ -39,7 +49,8 @@ describe('SettingsComponent', () => { const settings = { crash_reports: true, experimental_features: true, - angular_map: false + angular_map: false, + console_command: '' }; const getAll = spyOn(settingsService, 'getAll').and.returnValue(settings); const setAll = spyOn(settingsService, 'setAll'); @@ -50,4 +61,5 @@ describe('SettingsComponent', () => { component.save(); expect(setAll).toHaveBeenCalledWith(settings); }); + }); diff --git a/src/app/services/server.service.spec.ts b/src/app/services/server.service.spec.ts index a1f0c437..64d98b18 100644 --- a/src/app/services/server.service.spec.ts +++ b/src/app/services/server.service.spec.ts @@ -35,6 +35,10 @@ export class MockedServerService { resolve(this.servers); }); } + + public getServerUrl(server: Server) { + return `${server.host}:${server.port}`; + } } describe('ServerService', () => { diff --git a/src/app/services/settings.service.spec.ts b/src/app/services/settings.service.spec.ts index b16bfc24..ec8f9ad7 100644 --- a/src/app/services/settings.service.spec.ts +++ b/src/app/services/settings.service.spec.ts @@ -4,10 +4,20 @@ import { PersistenceService, StorageType } from 'angular-persistence'; import { Settings, SettingsService } from './settings.service'; export class MockedSettingsService { + settings = {}; + isExperimentalEnabled() { return true; } getAll() {} + + get(key: string) { + return this.settings[key]; + } + + set(key: string, value: any) { + this.settings[key] = value; + } } describe('SettingsService', () => { @@ -50,7 +60,8 @@ describe('SettingsService', () => { expect(service.getAll()).toEqual({ crash_reports: true, experimental_features: false, - angular_map: false + angular_map: false, + console_command: undefined }); })); @@ -63,7 +74,8 @@ describe('SettingsService', () => { expect(service.getAll()).toEqual({ crash_reports: false, experimental_features: false, - angular_map: false + angular_map: false, + console_command: undefined }); })); diff --git a/src/app/services/settings/console.service.spec.ts b/src/app/services/settings/console.service.spec.ts index 30a37977..1cbbd288 100644 --- a/src/app/services/settings/console.service.spec.ts +++ b/src/app/services/settings/console.service.spec.ts @@ -1,12 +1,33 @@ import { TestBed } from '@angular/core/testing'; import { ConsoleService } from './console.service'; +import { MockedSettingsService } from '../settings.service.spec'; +import { SettingsService } from '../settings.service'; +import { DefaultConsoleService } from './default-console.service'; describe('ConsoleService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + let service: ConsoleService; + let settings: MockedSettingsService; + + beforeEach(() => { + let defaultConsoleService = { + get: () => 'default' + }; + settings = new MockedSettingsService(); + service = new ConsoleService(defaultConsoleService as any, settings as any); + }); it('should be created', () => { - const service: ConsoleService = TestBed.get(ConsoleService); expect(service).toBeTruthy(); }); + + it('should get command from settings if defined', () => { + settings.set('console_command', 'from_settings'); + expect(service.command).toEqual('from_settings'); + }); + + it('should get command from default console if settings are not defined', () => { + settings.set('console_command', undefined); + expect(service.command).toEqual('default'); + }); }); diff --git a/src/app/services/settings/default-console.service.spec.ts b/src/app/services/settings/default-console.service.spec.ts index 167e0f02..df89f144 100644 --- a/src/app/services/settings/default-console.service.spec.ts +++ b/src/app/services/settings/default-console.service.spec.ts @@ -1,12 +1,45 @@ -import { TestBed } from '@angular/core/testing'; import { DefaultConsoleService } from './default-console.service'; describe('DefaultConsoleService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + let electronService; + let service: DefaultConsoleService; + beforeEach(() => { + electronService = { + isElectronApp: false, + isWindows: false, + isLinux: false + }; + }); + + beforeEach(() => { + service = new DefaultConsoleService(electronService); + }); it('should be created', () => { - const service: DefaultConsoleService = TestBed.get(DefaultConsoleService); expect(service).toBeTruthy(); }); + + it('should return undefined when not running in electron', () => { + electronService.isElectronApp = false; + expect(service.get()).toBeUndefined(); + }); + + it('should return console for windows', () => { + electronService.isElectronApp = true; + electronService.isWindows = true; + expect(service.get()).toEqual('putty.exe -telnet %h %p -loghost "%d"'); + }) + + it('should return console for linux', () => { + electronService.isElectronApp = true; + electronService.isLinux = true; + expect(service.get()).toEqual('xfce4-terminal --tab -T "%d" -e "telnet %h %p"'); + }) + + it('should return undefined for other platforms', () => { + electronService.isElectronApp = true; + expect(service.get()).toBeUndefined(); + }) + });