BREAKING CHANGE(update the API): update
This commit is contained in:
parent
a63ae38437
commit
2ca593297c
@ -1,32 +0,0 @@
|
||||
# smartshell
|
||||
|
||||
shell actions designed as promises
|
||||
|
||||
## Availabililty
|
||||
|
||||
[![npm](https://pushrocks.gitlab.io/assets/repo-button-npm.svg)](https://www.npmjs.com/package/smartshell)
|
||||
[![git](https://pushrocks.gitlab.io/assets/repo-button-git.svg)](https://GitLab.com/pushrocks/smartshell)
|
||||
[![git](https://pushrocks.gitlab.io/assets/repo-button-mirror.svg)](https://github.com/pushrocks/smartshell)
|
||||
[![docs](https://pushrocks.gitlab.io/assets/repo-button-docs.svg)](https://pushrocks.gitlab.io/smartshell/)
|
||||
|
||||
## Status for master
|
||||
|
||||
[![build status](https://GitLab.com/pushrocks/smartshell/badges/master/build.svg)](https://GitLab.com/pushrocks/smartshell/commits/master)
|
||||
[![coverage report](https://GitLab.com/pushrocks/smartshell/badges/master/coverage.svg)](https://GitLab.com/pushrocks/smartshell/commits/master)
|
||||
[![npm downloads per month](https://img.shields.io/npm/dm/smartshell.svg)](https://www.npmjs.com/package/smartshell)
|
||||
[![Dependency Status](https://david-dm.org/pushrocks/smartshell.svg)](https://david-dm.org/pushrocks/smartshell)
|
||||
[![bitHound Dependencies](https://www.bithound.io/github/pushrocks/smartshell/badges/dependencies.svg)](https://www.bithound.io/github/pushrocks/smartshell/master/dependencies/npm)
|
||||
[![bitHound Code](https://www.bithound.io/github/pushrocks/smartshell/badges/code.svg)](https://www.bithound.io/github/pushrocks/smartshell)
|
||||
[![TypeScript](https://img.shields.io/badge/TypeScript-2.x-blue.svg)](https://nodejs.org/dist/latest-v6.x/docs/api/)
|
||||
[![node](https://img.shields.io/badge/node->=%206.x.x-blue.svg)](https://nodejs.org/dist/latest-v6.x/docs/api/)
|
||||
[![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)
|
||||
|
||||
## Usage
|
||||
|
||||
Use TypeScript for best in class instellisense.
|
||||
|
||||
For further information read the linked docs at the top of this README.
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
|
||||
[![repo-footer](https://pushrocks.gitlab.io/assets/repo-footer.svg)](https://push.rocks)
|
28
test/test.ts
28
test/test.ts
@ -1,23 +1,31 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
|
||||
import * as smartshell from '../ts/index';
|
||||
import * as smartshell from '../ts';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
|
||||
let testSmartshell: smartshell.Smartshell;
|
||||
|
||||
tap.test('smartshell should create a Smartshell instance', async () => {
|
||||
testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: []
|
||||
});
|
||||
expect(testSmartshell).to.be.instanceof(smartshell.Smartshell);
|
||||
});
|
||||
|
||||
tap.test('smartshell should run async', async () => {
|
||||
let execResult = await smartshell.exec('npm -v');
|
||||
let execResult = await testSmartshell.exec('npm -v');
|
||||
expect(execResult.stdout).to.match(/[0-9\.]*/);
|
||||
});
|
||||
|
||||
tap.test('smartshell should run async and silent', async () => {
|
||||
let execResult = await smartshell.execSilent('npm -v');
|
||||
let execResult = await testSmartshell.execSilent('npm -v');
|
||||
expect(execResult.stdout).to.match(/[0-9\.]*/);
|
||||
});
|
||||
|
||||
tap.test('smartshell should stream a shell execution', async () => {
|
||||
let done = smartpromise.defer();
|
||||
let execStreamingResponse = smartshell.execStreaming('npm -v');
|
||||
let execStreamingResponse = await testSmartshell.execStreaming('npm -v');
|
||||
execStreamingResponse.childProcess.stdout.on('data', data => {
|
||||
done.resolve(data);
|
||||
});
|
||||
@ -27,17 +35,7 @@ tap.test('smartshell should stream a shell execution', async () => {
|
||||
});
|
||||
|
||||
tap.test('it should execute and wait for a line in the output', async () => {
|
||||
await smartshell.execAndWaitForLine('echo "5.0.4"', /5.0.4/);
|
||||
});
|
||||
|
||||
// Smartshell class
|
||||
|
||||
tap.test('smartshell should create a Smartshell instance', async () => {
|
||||
testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: []
|
||||
});
|
||||
expect(testSmartshell).to.be.instanceof(smartshell.Smartshell);
|
||||
await testSmartshell.execAndWaitForLine('echo "5.0.4"', /5.0.4/);
|
||||
});
|
||||
|
||||
tap.test('smartshell should run async', async () => {
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './smartshell.wrap';
|
||||
export * from './smartshell.classes.smartshell';
|
||||
|
50
ts/smartshell.classes.shellenv.ts
Normal file
50
ts/smartshell.classes.shellenv.ts
Normal file
@ -0,0 +1,50 @@
|
||||
export type TExecutor = 'sh' | 'bash';
|
||||
|
||||
export interface IShellEnvContructorOptions {
|
||||
executor: TExecutor;
|
||||
sourceFilePaths: string[];
|
||||
}
|
||||
|
||||
export class ShellEnv {
|
||||
executor: TExecutor;
|
||||
sourceFileArray: string[] = [];
|
||||
|
||||
/**
|
||||
* constructor for the shellenv
|
||||
*/
|
||||
constructor(optionsArg: IShellEnvContructorOptions) {
|
||||
this.executor = optionsArg.executor;
|
||||
for (let sourceFilePath of optionsArg.sourceFilePaths) {
|
||||
this.sourceFileArray.push(sourceFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* add files that are going to be sourced when running a command
|
||||
* @param sourceFilePathsArray
|
||||
*/
|
||||
addSourceFiles(sourceFilePathsArray: string[]) {
|
||||
for (let sourceFilePath of sourceFilePathsArray) {
|
||||
this.sourceFileArray.push(sourceFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* cleans the source files array
|
||||
*/
|
||||
cleanSourceFiles() {
|
||||
this.sourceFileArray = [];
|
||||
}
|
||||
|
||||
createEnvExecString(commandArg): string {
|
||||
if (this.executor === 'bash') {
|
||||
let sourceString = '';
|
||||
for (let sourceFilePath of this.sourceFileArray) {
|
||||
sourceString = sourceString + `source ${sourceFilePath} && `;
|
||||
}
|
||||
return `bash -c '${sourceString} ${commandArg}'`;
|
||||
} else {
|
||||
return commandArg;
|
||||
}
|
||||
}
|
||||
}
|
44
ts/smartshell.classes.shelllog.ts
Normal file
44
ts/smartshell.classes.shelllog.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import * as plugins from './smartshell.plugins';
|
||||
|
||||
/**
|
||||
* a log handler for spawned logs
|
||||
* making sure the process doesn't run out of memory
|
||||
*/
|
||||
export class ShellLog {
|
||||
logStore = Buffer.from('');
|
||||
|
||||
/**
|
||||
* log data to console
|
||||
* @param dataArg
|
||||
*/
|
||||
logToConsole(dataArg: string | Buffer): void {
|
||||
// make sure we have the data as string
|
||||
const dataString: string = (() => {
|
||||
if (Buffer.isBuffer(dataArg)) {
|
||||
return dataArg.toString();
|
||||
}
|
||||
return dataArg;
|
||||
})();
|
||||
console.log(dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* add data to Buffer for later consumption
|
||||
* @param dataArg
|
||||
*/
|
||||
addToBuffer(dataArg: string | Buffer): void {
|
||||
// make sure we have the data as Buffer
|
||||
const dataBuffer: Buffer = (() => {
|
||||
if (!Buffer.isBuffer(dataArg)) {
|
||||
return Buffer.from(dataArg);
|
||||
}
|
||||
return dataArg;
|
||||
})();
|
||||
this.logStore = Buffer.concat([this.logStore,dataBuffer]);
|
||||
}
|
||||
|
||||
logAndAdd(dataArg: string | Buffer): void {
|
||||
this.logToConsole(dataArg);
|
||||
this.addToBuffer(dataArg);
|
||||
}
|
||||
}
|
@ -1,64 +1,177 @@
|
||||
// -- imports --
|
||||
import * as plugins from './smartshell.plugins';
|
||||
import * as smartshellWrap from './smartshell.wrap';
|
||||
import { ShellEnv, IShellEnvContructorOptions, TExecutor, } from './smartshell.classes.shellenv';
|
||||
import { ShellLog } from './smartshell.classes.shelllog';
|
||||
|
||||
export type TExecutor = 'sh' | 'bash';
|
||||
import * as cp from "child_process";
|
||||
import { Deferred } from "@pushrocks/smartpromise";
|
||||
|
||||
export interface ISmartshellContructorOptions {
|
||||
executor: TExecutor;
|
||||
sourceFilePaths: string[];
|
||||
// -- interfaces --
|
||||
/**
|
||||
* interface for ExecResult
|
||||
*/
|
||||
export interface IExecResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* interface for streaming ExecResult
|
||||
*/
|
||||
export interface IExecResultStreaming {
|
||||
childProcess: cp.ChildProcess;
|
||||
finalPromise: Promise<IExecResult>;
|
||||
}
|
||||
|
||||
// -- SmartShell --
|
||||
export class Smartshell {
|
||||
executor: TExecutor;
|
||||
sourceFileArray: string[] = [];
|
||||
constructor(optionsArg: ISmartshellContructorOptions) {
|
||||
this.executor = optionsArg.executor;
|
||||
for (let sourceFilePath of optionsArg.sourceFilePaths) {
|
||||
this.sourceFileArray.push(sourceFilePath);
|
||||
}
|
||||
}
|
||||
shellEnv: ShellEnv;
|
||||
|
||||
addSourceFiles(sourceFilePathsArray: string[]) {
|
||||
for (let sourceFilePath of sourceFilePathsArray) {
|
||||
this.sourceFileArray.push(sourceFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
cleanSourceFiles() {
|
||||
this.sourceFileArray = [];
|
||||
constructor(optionsArg: IShellEnvContructorOptions) {
|
||||
this.shellEnv = new ShellEnv(optionsArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* executes silently and returns IExecResult
|
||||
* @param commandArg
|
||||
* imports path into the shell from env if available and returns it with
|
||||
*/
|
||||
async execSilent(commandArg: string) {
|
||||
let execCommand = this.createExecString(commandArg);
|
||||
return await smartshellWrap.execSilent(execCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* executes and returns IExecResult
|
||||
* @param commandArg
|
||||
*/
|
||||
async exec(commandArg: string) {
|
||||
let execCommand = this.createExecString(commandArg);
|
||||
return await smartshellWrap.exec(execCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates the final sourcing string
|
||||
* @param commandArg
|
||||
*/
|
||||
private createExecString(commandArg): string {
|
||||
if (this.executor === 'bash') {
|
||||
let sourceString = '';
|
||||
for (let sourceFilePath of this.sourceFileArray) {
|
||||
sourceString = sourceString + `source ${sourceFilePath} && `;
|
||||
}
|
||||
return `bash -c '${sourceString} ${commandArg}'`;
|
||||
private _importEnvVarPath (stringArg): string {
|
||||
if (process.env.SMARTSHELL_PATH) {
|
||||
let commandResult = `PATH=${process.env.SMARTSHELL_PATH} && ${stringArg}`;
|
||||
// console.log(commandResult)
|
||||
return commandResult;
|
||||
} else {
|
||||
return commandArg;
|
||||
return stringArg;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a given command async
|
||||
* @param commandStringArg
|
||||
*/
|
||||
private async _exec (
|
||||
commandStringArg: string,
|
||||
silentArg: boolean = false,
|
||||
strictArg = false,
|
||||
streamingArg = false
|
||||
): Promise<IExecResult | IExecResultStreaming> {
|
||||
// flow control promises
|
||||
const done = plugins.smartpromise.defer<IExecResult | IExecResultStreaming>();
|
||||
const childProcessEnded = plugins.smartpromise.defer<IExecResult>();
|
||||
// build commandToExecute
|
||||
let commandToExecute = commandStringArg
|
||||
commandToExecute = this.shellEnv.createEnvExecString(commandStringArg);
|
||||
commandToExecute = this._importEnvVarPath(commandToExecute);
|
||||
const spawnlogInstance = new ShellLog();
|
||||
const execChildProcess = cp.spawn(commandToExecute, [], {
|
||||
shell: true,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
execChildProcess.stdout.on("data", data => {
|
||||
if (!silentArg) {
|
||||
spawnlogInstance.logToConsole(data);
|
||||
}
|
||||
spawnlogInstance.addToBuffer(data);
|
||||
});
|
||||
execChildProcess.stderr.on("data", data => {
|
||||
if (!silentArg) {
|
||||
spawnlogInstance.logToConsole(data);
|
||||
}
|
||||
spawnlogInstance.addToBuffer(data);
|
||||
});
|
||||
|
||||
if(streamingArg) {
|
||||
done.resolve({
|
||||
childProcess: execChildProcess,
|
||||
finalPromise: childProcessEnded.promise
|
||||
});
|
||||
}
|
||||
|
||||
execChildProcess.on("exit", (code, signal) => {
|
||||
if(strictArg && code === 1) {
|
||||
done.reject();
|
||||
}
|
||||
|
||||
const execResult = {
|
||||
exitCode: code,
|
||||
stdout: spawnlogInstance.logStore.toString()
|
||||
}
|
||||
|
||||
if(!streamingArg) {
|
||||
done.resolve(execResult);
|
||||
}
|
||||
childProcessEnded.resolve(execResult);
|
||||
});
|
||||
|
||||
const result = await done.promise;
|
||||
return result;
|
||||
};
|
||||
|
||||
async exec (
|
||||
commandStringArg: string
|
||||
): Promise<IExecResult> {
|
||||
return (await this._exec(commandStringArg, false)) as IExecResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a given command async and silent
|
||||
* @param commandStringArg
|
||||
*/
|
||||
async execSilent (
|
||||
commandStringArg: string
|
||||
): Promise<IExecResult> {
|
||||
return (await this._exec(commandStringArg, true)) as IExecResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a command async and strict, meaning it rejects the promise if something happens
|
||||
*/
|
||||
async execStrict (
|
||||
commandStringArg: string
|
||||
): Promise<IExecResult> {
|
||||
return (await this._exec(commandStringArg, true, true)) as IExecResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a command and allows you to stream output
|
||||
*/
|
||||
async execStreaming (
|
||||
commandStringArg: string,
|
||||
silentArg: boolean = false
|
||||
): Promise<IExecResultStreaming> {
|
||||
return (await this._exec(commandStringArg, silentArg, false, true)) as IExecResultStreaming;
|
||||
};
|
||||
|
||||
async execStreamingSilent (commandStringArg: string) {
|
||||
return (await this.execStreaming(commandStringArg, true)) as IExecResultStreaming;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a command and returns promise that will be fullfilled once an putput line matches RegexArg
|
||||
* @param commandStringArg
|
||||
* @param regexArg
|
||||
*/
|
||||
async execAndWaitForLine (
|
||||
commandStringArg: string,
|
||||
regexArg: RegExp,
|
||||
silentArg: boolean = false
|
||||
) {
|
||||
let done = plugins.smartpromise.defer();
|
||||
let execStreamingResult = await this.execStreaming(commandStringArg, silentArg);
|
||||
execStreamingResult.childProcess.stdout.on("data", (stdOutChunk: string) => {
|
||||
if (regexArg.test(stdOutChunk)) {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
return done.promise;
|
||||
};
|
||||
|
||||
async execAndWaitForLineSilent (
|
||||
commandStringArg: string,
|
||||
regexArg: RegExp
|
||||
) {
|
||||
this.execAndWaitForLine(commandStringArg, regexArg, true);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as plugins from "./smartshell.plugins";
|
||||
import { ShellLog } from "./smartshell.classes.shelllog";
|
||||
|
||||
// interfaces
|
||||
import * as cp from "child_process";
|
||||
@ -20,174 +21,7 @@ export interface IExecResultStreaming {
|
||||
finalPromise: Promise<IExecResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* imports path into the shell from env if available and returns it with
|
||||
*/
|
||||
let importEnvVarPath = (stringArg): string => {
|
||||
if (process.env.SMARTSHELL_PATH) {
|
||||
let commandResult = `PATH=${process.env.SMARTSHELL_PATH} && ${stringArg}`;
|
||||
// console.log(commandResult)
|
||||
return commandResult;
|
||||
} else {
|
||||
return stringArg;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a given command async
|
||||
* @param commandStringArg
|
||||
*/
|
||||
export let exec = (
|
||||
commandStringArg: string,
|
||||
silentArg: boolean = false,
|
||||
strictArg = false
|
||||
): Promise<IExecResult> => {
|
||||
let done = plugins.smartpromise.defer<IExecResult>();
|
||||
const commandToExecute = importEnvVarPath(commandStringArg);
|
||||
try {
|
||||
const execChildProcess = cp.exec(commandToExecute, {
|
||||
timeout: null,
|
||||
maxBuffer: 1000000,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
let logStore = "";
|
||||
|
||||
execChildProcess.stdout.on("data", (data: string) => {
|
||||
if (!silentArg) {
|
||||
console.log(data);
|
||||
}
|
||||
logStore += data;
|
||||
});
|
||||
execChildProcess.stderr.on("data", data => {
|
||||
if (!silentArg) {
|
||||
console.log(data);
|
||||
}
|
||||
logStore += data;
|
||||
});
|
||||
execChildProcess.on("exit", (code, signal) => {
|
||||
done.resolve({
|
||||
exitCode: code,
|
||||
stdout: logStore
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e;
|
||||
}
|
||||
|
||||
/*plugins.shelljs.exec(importEnvVarPath(commandStringArg), { async: true, silent: silentArg }, (code, stdout, stderr) => {
|
||||
if (
|
||||
stderr
|
||||
&& (stderr !== '')
|
||||
&& (!silentArg || strictArg)
|
||||
&& (process.env.DEBUG === 'true')
|
||||
) {
|
||||
console.log('StdErr found.')
|
||||
console.log(stderr)
|
||||
}
|
||||
if (strictArg) {
|
||||
done.reject(new Error(stderr))
|
||||
return
|
||||
}
|
||||
done.resolve({
|
||||
exitCode: code,
|
||||
stdout: stdout
|
||||
})
|
||||
})*/
|
||||
return done.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a given command async and silent
|
||||
* @param commandStringArg
|
||||
*/
|
||||
export let execSilent = async (
|
||||
commandStringArg: string
|
||||
): Promise<IExecResult> => {
|
||||
return await exec(commandStringArg, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* executes strict, meaning it rejects the promise if something happens
|
||||
*/
|
||||
export let execStrict = async (
|
||||
commandStringArg: string
|
||||
): Promise<IExecResult> => {
|
||||
return await exec(commandStringArg, true, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a command and allws you to stream output
|
||||
*/
|
||||
export let execStreaming = (
|
||||
commandStringArg: string,
|
||||
silentArg: boolean = false
|
||||
) => {
|
||||
let childProcessEnded = plugins.smartpromise.defer<IExecResult>();
|
||||
const commandToExecute = importEnvVarPath(commandStringArg);
|
||||
let execChildProcess = cp.exec(commandToExecute, {
|
||||
timeout: null,
|
||||
maxBuffer: 1000000,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
let logStore = "";
|
||||
|
||||
execChildProcess.stdout.on("data", (data: string) => {
|
||||
if (!silentArg) {
|
||||
console.log(data);
|
||||
}
|
||||
logStore += data;
|
||||
});
|
||||
execChildProcess.stderr.on("data", data => {
|
||||
if (!silentArg) {
|
||||
console.log(data);
|
||||
}
|
||||
logStore += data;
|
||||
});
|
||||
execChildProcess.on("exit", (code, signal) => {
|
||||
childProcessEnded.resolve({
|
||||
exitCode: code,
|
||||
stdout: logStore
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
childProcess: execChildProcess,
|
||||
finalPromise: childProcessEnded.promise
|
||||
};
|
||||
};
|
||||
|
||||
export let execStreamingSilent = (commandStringArg: string) => {
|
||||
return execStreaming(commandStringArg, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a command and returns promise that will be fullfilled once an putput line matches RegexArg
|
||||
* @param commandStringArg
|
||||
* @param regexArg
|
||||
*/
|
||||
export let execAndWaitForLine = (
|
||||
commandStringArg: string,
|
||||
regexArg: RegExp,
|
||||
silentArg: boolean = false
|
||||
) => {
|
||||
let done = plugins.smartpromise.defer();
|
||||
let execStreamingResult = execStreaming(commandStringArg, silentArg);
|
||||
execStreamingResult.childProcess.stdout.on("data", (stdOutChunk: string) => {
|
||||
if (regexArg.test(stdOutChunk)) {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
return done.promise;
|
||||
};
|
||||
|
||||
export let execAndWaitForLineSilent = (
|
||||
commandStringArg: string,
|
||||
regexArg: RegExp
|
||||
) => {
|
||||
execAndWaitForLine(commandStringArg, regexArg, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* get a path
|
||||
@ -202,3 +36,24 @@ export let which = (cmd: string): Promise<string> => {
|
||||
});
|
||||
return done.promise;
|
||||
};
|
||||
|
||||
|
||||
/* /////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* executes silently and returns IExecResult
|
||||
* @param commandArg
|
||||
|
||||
async execSilent(commandArg: string) {
|
||||
let execCommand = this..createExecString(commandArg);
|
||||
return await smartshellWrap.execSilent(execCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* executes and returns IExecResult
|
||||
* @param commandArg
|
||||
|
||||
async exec(commandArg: string) {
|
||||
let execCommand = this.createExecString(commandArg);
|
||||
return await smartshellWrap.exec(execCommand);
|
||||
} */
|
Loading…
Reference in New Issue
Block a user