@vpalmisano/webrtcperf 4.3.2 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.min.js +1 -1
- package/build/src/app.js +16 -5
- package/build/src/app.js.map +1 -1
- package/build/src/config.d.ts +1 -0
- package/build/src/config.js +7 -0
- package/build/src/config.js.map +1 -1
- package/build/src/docker.d.ts +1 -0
- package/build/src/docker.js +123 -0
- package/build/src/docker.js.map +1 -0
- package/build/src/session.d.ts +5 -1
- package/build/src/session.js +73 -23
- package/build/src/session.js.map +1 -1
- package/build/src/utils.d.ts +5 -1
- package/build/src/utils.js +22 -14
- package/build/src/utils.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -25
- package/src/app.ts +17 -7
- package/src/config.ts +7 -0
- package/src/docker.ts +131 -0
- package/src/session.ts +66 -15
- package/src/utils.ts +22 -13
package/src/docker.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import Docker from 'dockerode'
|
|
3
|
+
import { logger, resolvePackagePath } from './utils'
|
|
4
|
+
import { loadConfig } from './config'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
|
|
7
|
+
const log = logger('webrtcperf:docker')
|
|
8
|
+
|
|
9
|
+
export async function runWithDocker(argv: string[]) {
|
|
10
|
+
const docker = new Docker()
|
|
11
|
+
const configPath = argv.filter(s => s !== '--docker')[0]
|
|
12
|
+
if (!configPath) throw new Error('No configuration file specified')
|
|
13
|
+
const configName = path.basename(configPath)
|
|
14
|
+
const config = (await loadConfig(configPath))[0]
|
|
15
|
+
|
|
16
|
+
const startTimestamp = Date.now()
|
|
17
|
+
const dataDir = path.resolve(path.dirname(configPath), 'logs', `${startTimestamp}`)
|
|
18
|
+
await fs.promises.mkdir(dataDir, { recursive: true })
|
|
19
|
+
|
|
20
|
+
const binds: string[] = [
|
|
21
|
+
`${path.resolve(configPath)}:/config/${configName}:ro`,
|
|
22
|
+
'/dev/shm:/dev/shm',
|
|
23
|
+
`${dataDir}:/data`,
|
|
24
|
+
'/tmp/webrtcperf-cache:/root/.webrtcperf',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
if (config.scriptPath) {
|
|
28
|
+
const scriptName = path.basename(config.scriptPath)
|
|
29
|
+
binds.push(`${path.resolve(config.scriptPath)}:/scripts/${scriptName}:ro`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.env.DEBUG_SRC) {
|
|
33
|
+
binds.push(`${resolvePackagePath('app.min.js')}:/app/app.min.js:ro`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const portBindings: Docker.PortMap = {}
|
|
37
|
+
const exposedPorts: { [portAndProtocol: string]: object } = {}
|
|
38
|
+
if (config.debuggingPort) {
|
|
39
|
+
for (let i = 0; i < config.sessions; i++) {
|
|
40
|
+
const port = `${config.debuggingPort + i}/tcp`
|
|
41
|
+
portBindings[port] = [{ HostPort: `${config.debuggingPort + i}` }]
|
|
42
|
+
exposedPorts[port] = {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const env = [
|
|
47
|
+
`DEBUG_LEVEL=${process.env.DEBUG_LEVEL || 'info'}`,
|
|
48
|
+
'SHOW_PAGE_LOG=false',
|
|
49
|
+
'SHOW_STATS=false',
|
|
50
|
+
'SERVER_PORT=5000',
|
|
51
|
+
'SERVER_USE_HTTPS=true',
|
|
52
|
+
'SERVER_DATA=/data',
|
|
53
|
+
`START_TIMESTAMP=${startTimestamp}`,
|
|
54
|
+
`STATS_PATH=/data/stats.csv`,
|
|
55
|
+
`PAGE_LOG_PATH=/data/page.log`,
|
|
56
|
+
`DETAILED_STATS_PATH=/data/detailed-stats.csv`,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if (config.scriptPath) {
|
|
60
|
+
const scriptName = path.basename(config.scriptPath)
|
|
61
|
+
env.push(`SCRIPT_PATH=/scripts/${scriptName}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.debuggingPort) {
|
|
65
|
+
env.push(`DEBUGGING_PORT=${config.debuggingPort}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.prometheusPushgateway.startsWith('http://localhost')) {
|
|
69
|
+
env.push('PROMETHEUS_PUSHGATEWAY=http://pushgateway:9091')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const containerConfig: Docker.ContainerCreateOptions = {
|
|
73
|
+
Image: 'ghcr.io/vpalmisano/webrtcperf:devel',
|
|
74
|
+
name: 'webrtcperf',
|
|
75
|
+
Cmd: [`/config/${configName}`],
|
|
76
|
+
HostConfig: {
|
|
77
|
+
Binds: binds,
|
|
78
|
+
PortBindings: portBindings,
|
|
79
|
+
CapAdd: ['NET_ADMIN'],
|
|
80
|
+
NetworkMode: config.prometheusPushgateway.startsWith('http://localhost') ? 'prometheus-stack_default' : 'bridge',
|
|
81
|
+
},
|
|
82
|
+
Env: env,
|
|
83
|
+
AttachStdin: true,
|
|
84
|
+
AttachStdout: true,
|
|
85
|
+
AttachStderr: true,
|
|
86
|
+
Tty: true,
|
|
87
|
+
OpenStdin: true,
|
|
88
|
+
StdinOnce: true,
|
|
89
|
+
ExposedPorts: exposedPorts,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (!process.env.DEBUG_SRC) {
|
|
94
|
+
log.info('Pulling latest webrtcperf image...')
|
|
95
|
+
await docker.pull('ghcr.io/vpalmisano/webrtcperf:devel')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const existingContainer = await docker.getContainer('webrtcperf')
|
|
100
|
+
await existingContainer.remove({ force: true })
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
// Container doesn't exist, continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const container = await docker.createContainer(containerConfig)
|
|
107
|
+
await container.start()
|
|
108
|
+
|
|
109
|
+
const stream = await container.attach({
|
|
110
|
+
stream: true,
|
|
111
|
+
stdin: true,
|
|
112
|
+
stdout: true,
|
|
113
|
+
stderr: true,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
process.stdin.pipe(stream)
|
|
117
|
+
stream.pipe(process.stdout)
|
|
118
|
+
|
|
119
|
+
await new Promise(resolve => {
|
|
120
|
+
container.wait((err: Error, data: { StatusCode: number }) => {
|
|
121
|
+
if (err) log.error('Error waiting for container:', data, err.stack)
|
|
122
|
+
resolve(data)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await container.remove()
|
|
127
|
+
} catch (error) {
|
|
128
|
+
log.error('Docker operation failed:', error)
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getSessionThrottleValues, throttleLauncher } from '@vpalmisano/throttler'
|
|
1
|
+
import { getSessionThrottleValues, throttleLauncher, throttleNotifier } from '@vpalmisano/throttler'
|
|
2
2
|
import assert from 'assert'
|
|
3
3
|
import axios from 'axios'
|
|
4
4
|
import EventEmitter from 'events'
|
|
@@ -30,8 +30,8 @@ import { gunzipSync } from 'zlib'
|
|
|
30
30
|
import { RtcStats, rtcStatKey, updateRtcStats } from './rtcstats'
|
|
31
31
|
import { FastStats } from './stats'
|
|
32
32
|
import {
|
|
33
|
-
PeerConnectionExternal,
|
|
34
|
-
PeerConnectionExternalMethod,
|
|
33
|
+
/* PeerConnectionExternal,
|
|
34
|
+
PeerConnectionExternalMethod, */
|
|
35
35
|
checkChromeExecutable,
|
|
36
36
|
downloadUrl,
|
|
37
37
|
enabledForSession,
|
|
@@ -163,6 +163,7 @@ export interface SessionParams {
|
|
|
163
163
|
userAgent: string
|
|
164
164
|
id: number
|
|
165
165
|
throttleIndex: number
|
|
166
|
+
useBrowserThrottling: boolean
|
|
166
167
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
168
|
evaluateAfter?: any[]
|
|
168
169
|
exposedFunctions?: string
|
|
@@ -279,6 +280,8 @@ export class Session extends EventEmitter {
|
|
|
279
280
|
readonly id: number
|
|
280
281
|
/** The throttle configuration index assigned to the session. */
|
|
281
282
|
readonly throttleIndex: number
|
|
283
|
+
/** If true, the network will be throttled using the browser internal throttling mechanism. */
|
|
284
|
+
readonly useBrowserThrottling: boolean
|
|
282
285
|
/** The test page url. */
|
|
283
286
|
readonly url: string
|
|
284
287
|
/** The url query. */
|
|
@@ -378,6 +381,7 @@ export class Session extends EventEmitter {
|
|
|
378
381
|
userAgent,
|
|
379
382
|
id,
|
|
380
383
|
throttleIndex,
|
|
384
|
+
useBrowserThrottling,
|
|
381
385
|
evaluateAfter,
|
|
382
386
|
exposedFunctions,
|
|
383
387
|
scriptParams,
|
|
@@ -475,6 +479,7 @@ export class Session extends EventEmitter {
|
|
|
475
479
|
this.emulateCpuThrottling = emulateCpuThrottling
|
|
476
480
|
|
|
477
481
|
this.throttleIndex = throttleIndex
|
|
482
|
+
this.useBrowserThrottling = useBrowserThrottling
|
|
478
483
|
this.evaluateAfter = evaluateAfter || []
|
|
479
484
|
this.exposedFunctions = exposedFunctions || {}
|
|
480
485
|
if (scriptParams) {
|
|
@@ -676,7 +681,7 @@ export class Session extends EventEmitter {
|
|
|
676
681
|
deviceScaleFactor: this.deviceScaleFactor,
|
|
677
682
|
isMobile: false,
|
|
678
683
|
hasTouch: false,
|
|
679
|
-
isLandscape:
|
|
684
|
+
isLandscape: true,
|
|
680
685
|
},
|
|
681
686
|
})
|
|
682
687
|
} catch (err) {
|
|
@@ -803,7 +808,7 @@ try {
|
|
|
803
808
|
} catch (err) {
|
|
804
809
|
console.error('[webrtcperf] Error parsing scriptParams:', err);
|
|
805
810
|
webrtcperf.params = {};
|
|
806
|
-
}
|
|
811
|
+
};
|
|
807
812
|
`
|
|
808
813
|
|
|
809
814
|
if (this.serverPort) {
|
|
@@ -892,6 +897,9 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
892
897
|
|
|
893
898
|
const page = await this.getNewPage(tabIndex)
|
|
894
899
|
|
|
900
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
901
|
+
const pageCDPSession = (page as any)._client() as CDPSession
|
|
902
|
+
|
|
895
903
|
await page.setBypassCSP(true)
|
|
896
904
|
|
|
897
905
|
if (this.userAgent) {
|
|
@@ -910,23 +918,29 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
910
918
|
if (this.localStorage) {
|
|
911
919
|
log.debug('Using localStorage:', this.localStorage)
|
|
912
920
|
Object.entries(this.localStorage).map(([key, value]) => {
|
|
913
|
-
cmd += `localStorage.setItem('${key}',
|
|
921
|
+
cmd += `window.localStorage.setItem('${key}', ${JSON.stringify(value)});\n`
|
|
914
922
|
})
|
|
915
923
|
}
|
|
916
924
|
if (this.sessionStorage) {
|
|
917
925
|
log.debug('Using sessionStorage:', this.sessionStorage)
|
|
918
926
|
Object.entries(this.sessionStorage).map(([key, value]) => {
|
|
919
|
-
cmd += `sessionStorage.setItem('${key}',
|
|
927
|
+
cmd += `window.sessionStorage.setItem('${key}', ${JSON.stringify(value)});\n`
|
|
920
928
|
})
|
|
921
929
|
}
|
|
930
|
+
cmd += `
|
|
931
|
+
Object.defineProperty(window.screen, 'width', { value: ${this.windowWidth}, writable: false });
|
|
932
|
+
Object.defineProperty(window.screen, 'height', { value: ${this.windowHeight}, writable: false });
|
|
933
|
+
Object.defineProperty(window.screen, 'availWidth', { value: ${this.windowWidth}, writable: false });
|
|
934
|
+
Object.defineProperty(window.screen, 'availHeight', { value: ${this.windowHeight}, writable: false });
|
|
935
|
+
Object.defineProperty(window.screen.orientation, 'type', { value: 'landscape-primary', writable: false });
|
|
936
|
+
`
|
|
922
937
|
log.debug('init command:', cmd)
|
|
923
938
|
await page.evaluateOnNewDocument(cmd)
|
|
924
939
|
|
|
925
940
|
// Clear cookies.
|
|
926
941
|
if (this.clearCookies) {
|
|
927
942
|
try {
|
|
928
|
-
|
|
929
|
-
await client.send('Network.clearBrowserCookies')
|
|
943
|
+
await pageCDPSession.send('Network.clearBrowserCookies')
|
|
930
944
|
} catch (err) {
|
|
931
945
|
log.error(`clearCookies error: ${(err as Error).stack}`)
|
|
932
946
|
}
|
|
@@ -970,8 +984,6 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
970
984
|
// Enable request interception.
|
|
971
985
|
let setRequestInterceptionState = true
|
|
972
986
|
|
|
973
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
974
|
-
const pageCDPSession = (page as any)._client() as CDPSession
|
|
975
987
|
await pageCDPSession.send('Network.setBypassServiceWorker', {
|
|
976
988
|
bypass: true,
|
|
977
989
|
})
|
|
@@ -1174,7 +1186,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1174
1186
|
}
|
|
1175
1187
|
|
|
1176
1188
|
// PeerConnectionExternal
|
|
1177
|
-
await page.exposeFunction(
|
|
1189
|
+
/* await page.exposeFunction(
|
|
1178
1190
|
'createPeerConnectionExternal',
|
|
1179
1191
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1180
1192
|
async (options: any) => {
|
|
@@ -1192,7 +1204,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1192
1204
|
return pc[name](arg)
|
|
1193
1205
|
}
|
|
1194
1206
|
},
|
|
1195
|
-
)
|
|
1207
|
+
)*/
|
|
1196
1208
|
|
|
1197
1209
|
// Simulate keypress
|
|
1198
1210
|
await page.exposeFunction('keypressText', async (selector: string, text: string, delay = 20) => {
|
|
@@ -1305,7 +1317,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1305
1317
|
if (this.showPageLog || saveFile) {
|
|
1306
1318
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1307
1319
|
page.on('pageerror', async (error: any) => {
|
|
1308
|
-
const text = `pageerror: ${error
|
|
1320
|
+
const text = `pageerror: ${error?.message?.message || error?.message || error} - ${error?.message?.stack || error?.stack}`
|
|
1309
1321
|
await this.onPageMessage(index, 'error', text, saveFile)
|
|
1310
1322
|
})
|
|
1311
1323
|
|
|
@@ -1420,12 +1432,25 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1420
1432
|
resourcesStats.wsRecvBytes += event.response.payloadData.length
|
|
1421
1433
|
})
|
|
1422
1434
|
|
|
1423
|
-
//
|
|
1435
|
+
// Hardware concurrency.
|
|
1424
1436
|
if (this.hardwareConcurrency) {
|
|
1425
1437
|
const plugin = NavigatorHardwareConcurrency({ hardwareConcurrency: this.hardwareConcurrency })
|
|
1426
1438
|
await plugin.onPageCreated(page)
|
|
1427
1439
|
}
|
|
1428
1440
|
|
|
1441
|
+
// Network throttling.
|
|
1442
|
+
if (this.throttleIndex > -1 && (process.platform !== 'linux' || this.useBrowserThrottling)) {
|
|
1443
|
+
log.debug(`Using internal network throttling`)
|
|
1444
|
+
await pageCDPSession.send('Network.emulateNetworkConditions', {
|
|
1445
|
+
offline: false,
|
|
1446
|
+
uploadThroughput: 100000000 / 8,
|
|
1447
|
+
downloadThroughput: 100000000 / 8,
|
|
1448
|
+
latency: 0,
|
|
1449
|
+
packetLoss: 0,
|
|
1450
|
+
packetQueueLength: 0,
|
|
1451
|
+
})
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1429
1454
|
// Load page script.
|
|
1430
1455
|
{
|
|
1431
1456
|
const filePath = resolvePackagePath('node_modules/@vpalmisano/webrtcperf-js/dist/webrtcperf.js')
|
|
@@ -1485,6 +1510,13 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1485
1510
|
// add to pages map
|
|
1486
1511
|
this.pages.set(index, page)
|
|
1487
1512
|
|
|
1513
|
+
if (this.throttleIndex > -1 && (process.platform !== 'linux' || this.useBrowserThrottling)) {
|
|
1514
|
+
await this.applyNetworkThrottling(pageCDPSession)
|
|
1515
|
+
throttleNotifier.on('change', async () => {
|
|
1516
|
+
await this.applyNetworkThrottling(pageCDPSession)
|
|
1517
|
+
})
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1488
1520
|
log.debug(`Page ${index + 1} "${url}" loaded in ${(Date.now() - pageLoadTime) / 1000}s`)
|
|
1489
1521
|
|
|
1490
1522
|
for (let i = 0; i < this.evaluateAfter.length; i++) {
|
|
@@ -1496,6 +1528,25 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
|
|
|
1496
1528
|
}
|
|
1497
1529
|
}
|
|
1498
1530
|
|
|
1531
|
+
private async applyNetworkThrottling(pageCDPSession: CDPSession) {
|
|
1532
|
+
const throttleUpValues = getSessionThrottleValues(this.throttleIndex, 'up')
|
|
1533
|
+
const throttleDownValues = getSessionThrottleValues(this.throttleIndex, 'down')
|
|
1534
|
+
const params = {
|
|
1535
|
+
offline: false,
|
|
1536
|
+
uploadThroughput: throttleUpValues.rate || -1,
|
|
1537
|
+
downloadThroughput: throttleDownValues.rate || -1,
|
|
1538
|
+
latency: Math.max(throttleUpValues.delay || 0, throttleDownValues.delay || 0),
|
|
1539
|
+
packetLoss: Math.max(throttleUpValues.loss || 0, throttleDownValues.loss || 0),
|
|
1540
|
+
packetQueueLength: Math.max(throttleUpValues.queue || 0, throttleDownValues.queue || 0),
|
|
1541
|
+
}
|
|
1542
|
+
log.debug(`Apply internal network throttling: ${JSON.stringify(params)}`)
|
|
1543
|
+
await pageCDPSession.send('Network.emulateNetworkConditions', {
|
|
1544
|
+
...params,
|
|
1545
|
+
uploadThroughput: params.uploadThroughput !== -1 ? params.uploadThroughput / 8 : -1,
|
|
1546
|
+
downloadThroughput: params.downloadThroughput !== -1 ? params.downloadThroughput / 8 : -1,
|
|
1547
|
+
})
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1499
1550
|
private async getNewPage(tabIndex: number): Promise<Page> {
|
|
1500
1551
|
log.debug(`getNewPage ${tabIndex}`)
|
|
1501
1552
|
assert(this.context, 'NoBrowserContextCreated')
|
package/src/utils.ts
CHANGED
|
@@ -651,28 +651,37 @@ export async function runShellCommand(
|
|
|
651
651
|
cmd: string,
|
|
652
652
|
verbose = false,
|
|
653
653
|
maxBuffer = 1024 * 1024,
|
|
654
|
+
{ provideStdin = false, returnStdout = true, returnStderr = true } = {},
|
|
654
655
|
): Promise<{ stdout: string; stderr: string }> {
|
|
655
656
|
if (verbose) log.debug(`runShellCommand cmd: ${cmd}`)
|
|
656
657
|
return new Promise((resolve, reject) => {
|
|
657
658
|
const p = spawn(cmd, {
|
|
658
659
|
shell: true,
|
|
659
|
-
stdio: [
|
|
660
|
+
stdio: [
|
|
661
|
+
provideStdin ? 'inherit' : 'ignore',
|
|
662
|
+
returnStdout ? 'pipe' : 'inherit',
|
|
663
|
+
returnStderr ? 'pipe' : 'inherit',
|
|
664
|
+
],
|
|
660
665
|
detached: true,
|
|
661
666
|
})
|
|
662
667
|
let stdout = ''
|
|
663
668
|
let stderr = ''
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
669
|
+
if (returnStdout) {
|
|
670
|
+
p.stdout?.on('data', data => {
|
|
671
|
+
if (maxBuffer && stdout.length > maxBuffer) {
|
|
672
|
+
stdout = stdout.slice(data.length)
|
|
673
|
+
}
|
|
674
|
+
stdout += data
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
if (returnStderr) {
|
|
678
|
+
p.stderr?.on('data', data => {
|
|
679
|
+
if (maxBuffer && stderr.length > maxBuffer) {
|
|
680
|
+
stderr = stderr.slice(data.length)
|
|
681
|
+
}
|
|
682
|
+
stderr += data
|
|
683
|
+
})
|
|
684
|
+
}
|
|
676
685
|
p.once('error', err => reject(err))
|
|
677
686
|
p.once('close', code => {
|
|
678
687
|
if (code !== 0) {
|