Skip to content

Commit

Permalink
feat(repeater): restore remote network diagnostic and scripts (#437)
Browse files Browse the repository at this point in the history
closes #436
  • Loading branch information
derevnjuk committed Aug 18, 2023
1 parent ea42521 commit a4da5f5
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 228 deletions.
14 changes: 13 additions & 1 deletion src/Config/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ import { ConfigReader } from './ConfigReader';
import { DefaultConfigReader } from './DefaultConfigReader';
import { CliInfo } from './CliInfo';
import { CliBuilder } from './CliBuilder';
import { RepeaterServer, DefaultRepeaterServer } from '../Repeater';
import {
RepeaterServer,
DefaultRepeaterServer,
RepeaterCommandHub,
DefaultRepeaterCommandHub
} from '../Repeater';
import { container, Lifecycle } from 'tsyringe';

container
Expand Down Expand Up @@ -107,6 +112,13 @@ container
},
{ lifecycle: Lifecycle.Singleton }
)
.register(
RepeaterCommandHub,
{
useClass: DefaultRepeaterCommandHub
},
{ lifecycle: Lifecycle.Singleton }
)
.register(
Tokens,
{
Expand Down
6 changes: 1 addition & 5 deletions src/Handlers/Events/NetworkTest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Event } from '../../Bus';

export enum NetworkTestType {
PING = 'ping',
TRACEROUTE = 'traceroute'
}
import { NetworkTestType } from '../../Repeater';

export class NetworkTest implements Event {
constructor(
Expand Down
4 changes: 2 additions & 2 deletions src/Handlers/Events/RegisterScripts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Event } from '../../Bus/Event';
import { Event } from '../../Bus';

export class RegisterScripts implements Event {
constructor(public readonly script: string | Record<string, string>) {}
constructor(public readonly script?: string | Record<string, string>) {}
}
109 changes: 26 additions & 83 deletions src/Handlers/NetworkTestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,37 @@
import { bind, Handler } from '../Bus';
import { NetworkTest } from './Events';
import { NetworkTestResult } from '../Integrations';
import { ReadlinePlatform } from '../Wizard';
import { Helpers, logger } from '../Utils';
import { injectable } from 'tsyringe';
import { EOL } from 'os';
import { URL } from 'url';
import { RepeaterCommandHub } from '../Repeater';
import { inject, injectable } from 'tsyringe';

type NetworkTestResult =
| {
output: string;
}
| {
error: string;
};

@injectable()
@bind(NetworkTest)
export class NetworkTestHandler
implements Handler<NetworkTest, NetworkTestResult>
{
public async handle(config: NetworkTest): Promise<NetworkTestResult> {
const output = await this.getOutput(config);

return { output, repeaterId: config.repeaterId };
}

private async getOutput(config: NetworkTest): Promise<string> {
return new Promise((resolve, reject) => {
const args = ['configure', `--${config.type}`];

logger.debug('Launching "Network Diagnostic" process with cmd: %j', args);

const child = Helpers.spawn({
include: args,
exclude: ['repeater']
});

child.unref();

const stdout: string[] = [];

child.stdout.on('data', (data: Buffer) => {
const chunk = data.toString();
const lines: string[] = chunk
.split('\n')
.filter((line: string) => line.length > 0);

stdout.push(...lines);
constructor(
@inject(RepeaterCommandHub) private readonly commandHub: RepeaterCommandHub
) {}

const [first, ...rest]: string[] = [].concat(config.input);

if (chunk.indexOf(ReadlinePlatform.URLS_QUESTION) > -1) {
child.stdin.write(`${[first, ...rest].join(',')}${EOL}`);
}

if (chunk.indexOf(ReadlinePlatform.HOST_OR_IP_QUESTION) > -1) {
child.stdin.write(`${new URL(first).hostname}${EOL}`);
}

if (chunk.indexOf(ReadlinePlatform.COMPELED_MESSAGE) > -1) {
child.stdin.end();
}
});

child.once('error', (err: Error) => {
logger.warn(
`Failed to start "Network Diagnostic" due to %s`,
err.message
);
reject(err);
});

child.on('close', (code: number) => {
if (code !== 0 || stdout.length === 0) {
const msg = `"Network Diagnostic" did not start successfully. Process exited with code ${code}`;

logger.warn(msg);

return reject(new Error(msg));
}

resolve(this.processOutput(stdout));
});
});
}

// this is workaround of \x1B[1G control code that retype string in console
private processOutput(input: string[]): string {
return input
.filter(
(element, index, arr) =>
!(
element.endsWith('\u001b[1G') ||
(!!arr[index + 1] && arr[index + 1] === '\u001b[1G')
)
)
.filter((x) => !x.startsWith(ReadlinePlatform.URLS_QUESTION))
.join('\n');
public async handle(config: NetworkTest): Promise<NetworkTestResult> {
try {
const output = await this.commandHub.testNetwork(
config.type,
config.input
);

return { output };
} catch (e) {
return {
error: typeof e === 'string' ? e : (e as Error).message
};
}
}
}
16 changes: 4 additions & 12 deletions src/Handlers/RegisterScriptsHandler.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import { bind, Handler } from '../Bus';
import { VirtualScripts, VirtualScriptType } from '../Scripts';
import { RegisterScripts } from './Events';
import { RepeaterCommandHub } from '../Repeater';
import { inject, injectable } from 'tsyringe';

@injectable()
@bind(RegisterScripts)
export class RegisterScriptsHandler implements Handler<RegisterScripts> {
constructor(
@inject(VirtualScripts) private readonly virtualScripts: VirtualScripts
@inject(RepeaterCommandHub) private readonly commandHub: RepeaterCommandHub
) {}

// eslint-disable-next-line @typescript-eslint/require-await
public async handle(event: RegisterScripts): Promise<void> {
this.virtualScripts.clear(VirtualScriptType.REMOTE);

const { script } = event;

if (typeof script === 'string') {
this.virtualScripts.set('*', VirtualScriptType.REMOTE, script);
} else if (script) {
Object.entries(script).map(([wildcard, code]: [string, string]) =>
this.virtualScripts.set(wildcard, VirtualScriptType.REMOTE, code)
);
if (event.script) {
this.commandHub.compileScripts(event.script);
}
}
}
24 changes: 8 additions & 16 deletions src/Handlers/SendRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import { bind, Handler } from '../Bus';
import { Request, RequestExecutor, Response } from '../RequestExecutor';
import { Request } from '../RequestExecutor';
import { ExecuteScript, ForwardResponse } from './Events';
import { injectable, injectAll } from 'tsyringe';
import { RepeaterCommandHub } from '../Repeater';
import { inject, injectable } from 'tsyringe';

@injectable()
@bind(ExecuteScript)
export class SendRequestHandler
implements Handler<ExecuteScript, ForwardResponse>
{
constructor(
@injectAll(RequestExecutor)
private readonly requestExecutors: RequestExecutor[]
@inject(RepeaterCommandHub)
private readonly commandHub: RepeaterCommandHub
) {}

public async handle(event: ExecuteScript): Promise<ForwardResponse> {
const { protocol } = event;

const requestExecutor = this.requestExecutors.find(
(x) => x.protocol === protocol
);

if (!requestExecutor) {
throw new Error(`Unsupported protocol "${protocol}"`);
}

const response: Response = await requestExecutor.execute(
const response = await this.commandHub.sendRequest(
new Request({ ...event, correlationIdRegex: event.correlation_id_regex })
);

const { statusCode, message, errorCode, body, headers } = response;
const { statusCode, message, errorCode, body, headers, protocol } =
response;

return new ForwardResponse(
protocol,
Expand Down
4 changes: 0 additions & 4 deletions src/Integrations/NetworkTestResult.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/Integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export * from './IntegrationClient';
export * from './IntegrationType';
export * from './Project';
export * from './IntegrationOptions';
export * from './NetworkTestResult';
129 changes: 129 additions & 0 deletions src/Repeater/DefaultRepeaterCommandHub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { RepeaterCommandHub } from './RepeaterCommandHub';
import { NetworkTestType } from './NetworkTestType';
import { VirtualScripts, VirtualScriptType } from '../Scripts';
import { Helpers, logger } from '../Utils';
import { ReadlinePlatform } from '../Wizard';
import { Request, RequestExecutor, Response } from '../RequestExecutor';
import { inject, injectable, injectAll } from 'tsyringe';
import { EOL } from 'os';
import { URL } from 'url';

@injectable()
export class DefaultRepeaterCommandHub implements RepeaterCommandHub {
constructor(
@inject(VirtualScripts) private readonly virtualScripts: VirtualScripts,
@injectAll(RequestExecutor)
private readonly requestExecutors: RequestExecutor[]
) {}

public compileScripts(script: string | Record<string, string>): void {
this.virtualScripts.clear(VirtualScriptType.REMOTE);

if (this.virtualScripts.size) {
logger.warn(
'Error Loading Script: Cannot accept scripts from the cloud when a local script is already loaded'
);

return;
}

if (typeof script === 'string') {
this.virtualScripts.set('*', VirtualScriptType.REMOTE, script);
} else {
Object.entries(script).map(([wildcard, code]: [string, string]) =>
this.virtualScripts.set(wildcard, VirtualScriptType.REMOTE, code)
);
}
}

public sendRequest(request: Request): Promise<Response> {
const { protocol } = request;

const requestExecutor = this.requestExecutors.find(
(x) => x.protocol === protocol
);

if (!requestExecutor) {
throw new Error(`Unsupported protocol "${protocol}"`);
}

return requestExecutor.execute(request);
}

public testNetwork(
type: NetworkTestType,
input: string | string[]
): Promise<string> {
return new Promise((resolve, reject) => {
const args = ['configure', `--${type}`];

logger.debug('Launching "Network Diagnostic" process with cmd: %j', args);

const child = Helpers.spawn({
include: args,
exclude: ['repeater']
});

child.unref();

const stdout: string[] = [];

child.stdout.on('data', (data: Buffer) => {
const chunk = data.toString();
const lines: string[] = chunk
.split('\n')
.filter((line: string) => line.length > 0);

stdout.push(...lines);

const [first, ...rest]: string[] = [].concat(input);

if (chunk.indexOf(ReadlinePlatform.URLS_QUESTION) > -1) {
child.stdin.write(`${[first, ...rest].join(',')}${EOL}`);
}

if (chunk.indexOf(ReadlinePlatform.HOST_OR_IP_QUESTION) > -1) {
child.stdin.write(`${new URL(first).hostname}${EOL}`);
}

if (chunk.indexOf(ReadlinePlatform.COMPELED_MESSAGE) > -1) {
child.stdin.end();
}
});

child.once('error', (err: Error) => {
logger.warn(
`Failed to start "Network Diagnostic" due to %s`,
err.message
);
reject(err);
});

child.on('close', (code: number) => {
if (code !== 0 || stdout.length === 0) {
const msg = `"Network Diagnostic" did not start successfully. Process exited with code ${code}`;

logger.warn(msg);

return reject(new Error(msg));
}

resolve(this.processOutput(stdout));
});
});
}

// this is workaround of \x1B[1G control code that retype string in console
private processOutput(input: string[]): string {
return input
.filter(
(element, index, arr) =>
!(
element.endsWith('\u001b[1G') ||
(!!arr[index + 1] && arr[index + 1] === '\u001b[1G')
)
)
.filter((x) => !x.startsWith(ReadlinePlatform.URLS_QUESTION))
.join('\n');
}
}
Loading

0 comments on commit a4da5f5

Please sign in to comment.