teen_process 3.0.6 → 4.0.1

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.
Files changed (53) hide show
  1. package/build/lib/exec.d.ts +32 -107
  2. package/build/lib/exec.d.ts.map +1 -1
  3. package/build/lib/exec.js +60 -103
  4. package/build/lib/exec.js.map +1 -1
  5. package/build/lib/helpers.d.ts +1 -12
  6. package/build/lib/helpers.d.ts.map +1 -1
  7. package/build/lib/helpers.js +17 -13
  8. package/build/lib/helpers.js.map +1 -1
  9. package/build/lib/index.d.ts +4 -6
  10. package/build/lib/index.d.ts.map +1 -1
  11. package/build/lib/index.js +7 -43
  12. package/build/lib/index.js.map +1 -1
  13. package/build/lib/subprocess.d.ts +106 -61
  14. package/build/lib/subprocess.d.ts.map +1 -1
  15. package/build/lib/subprocess.js +145 -143
  16. package/build/lib/subprocess.js.map +1 -1
  17. package/build/lib/types.d.ts +79 -0
  18. package/build/lib/types.d.ts.map +1 -0
  19. package/build/lib/types.js +3 -0
  20. package/build/lib/types.js.map +1 -0
  21. package/build/test/circular-buffer-specs.d.ts +2 -0
  22. package/build/test/circular-buffer-specs.d.ts.map +1 -0
  23. package/build/test/circular-buffer-specs.js +40 -0
  24. package/build/test/circular-buffer-specs.js.map +1 -0
  25. package/build/test/exec-specs.d.ts +2 -0
  26. package/build/test/exec-specs.d.ts.map +1 -0
  27. package/build/test/exec-specs.js +167 -0
  28. package/build/test/exec-specs.js.map +1 -0
  29. package/build/test/fixtures/bigbuffer.d.ts +3 -0
  30. package/build/test/fixtures/bigbuffer.d.ts.map +1 -0
  31. package/build/test/fixtures/bigbuffer.js +13 -0
  32. package/build/test/fixtures/bigbuffer.js.map +1 -0
  33. package/build/test/helpers.d.ts +3 -0
  34. package/build/test/helpers.d.ts.map +1 -0
  35. package/build/test/helpers.js +15 -0
  36. package/build/test/helpers.js.map +1 -0
  37. package/build/test/subproc-specs.d.ts +2 -0
  38. package/build/test/subproc-specs.d.ts.map +1 -0
  39. package/build/test/subproc-specs.js +414 -0
  40. package/build/test/subproc-specs.js.map +1 -0
  41. package/build/tsconfig.tsbuildinfo +1 -0
  42. package/lib/circular-buffer.ts +1 -1
  43. package/lib/exec.ts +158 -0
  44. package/lib/helpers.ts +44 -0
  45. package/lib/index.ts +8 -0
  46. package/lib/subprocess.ts +353 -0
  47. package/lib/types.ts +95 -0
  48. package/package.json +5 -5
  49. package/index.js +0 -1
  50. package/lib/exec.js +0 -191
  51. package/lib/helpers.js +0 -38
  52. package/lib/index.js +0 -9
  53. package/lib/subprocess.js +0 -329
package/lib/exec.js DELETED
@@ -1,191 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { quote } from 'shell-quote';
3
- import B from 'bluebird';
4
- import _ from 'lodash';
5
- import { formatEnoent } from './helpers';
6
- import { CircularBuffer, MAX_BUFFER_SIZE } from './circular-buffer';
7
-
8
- /**
9
- * Spawns a process
10
- * @template {TeenProcessExecOptions} T
11
- * @param {string} cmd - Program to execute
12
- * @param {string[]} [args] - Arguments to pass to the program
13
- * @param {T} [originalOpts] - Options
14
- * @returns {Promise<BufferProp<T> extends true ? TeenProcessExecBufferResult : TeenProcessExecStringResult>}
15
- */
16
- async function exec (cmd, args = [], originalOpts = /** @type {T} */({})) {
17
- // get a quoted representation of the command for error strings
18
- const rep = quote([cmd, ...args]);
19
-
20
- // extend default options; we're basically re-implementing exec's options
21
- // for use here with spawn under the hood
22
- const opts = /** @type {T} */(_.defaults(originalOpts, {
23
- timeout: null,
24
- encoding: 'utf8',
25
- killSignal: 'SIGTERM',
26
- cwd: undefined,
27
- env: process.env,
28
- ignoreOutput: false,
29
- stdio: 'inherit',
30
- isBuffer: false,
31
- shell: undefined,
32
- logger: undefined,
33
- maxStdoutBufferSize: MAX_BUFFER_SIZE,
34
- maxStderrBufferSize: MAX_BUFFER_SIZE,
35
- }));
36
-
37
- const isBuffer = Boolean(opts.isBuffer);
38
-
39
- // this is an async function, so return a promise
40
- return await new B((resolve, reject) => {
41
- // spawn the child process with options; we don't currently expose any of
42
- // the other 'spawn' options through the API
43
- const proc = spawn(cmd, args, {cwd: opts.cwd, env: opts.env, shell: opts.shell});
44
- const stdoutBuffer = new CircularBuffer(opts.maxStdoutBufferSize);
45
- const stderrBuffer = new CircularBuffer(opts.maxStderrBufferSize);
46
- let timer = null;
47
-
48
- // if the process errors out, reject the promise
49
- proc.on('error', /** @param {NodeJS.ErrnoException} err */ async (err) => {
50
- if (err.code === 'ENOENT') {
51
- err = await formatEnoent(err, cmd, opts.cwd?.toString());
52
- }
53
- reject(err);
54
- });
55
- if (proc.stdin) {
56
- proc.stdin.on('error', /** @param {NodeJS.ErrnoException} err */(err) => {
57
- reject(new Error(`Standard input '${err.syscall}' error: ${err.stack}`));
58
- });
59
- }
60
- const handleStream = (/** @type {string} */ streamType, /** @type {CircularBuffer} */ buffer) => {
61
- if (!proc[streamType]) {
62
- return;
63
- }
64
-
65
- proc[streamType].on('error', (err) => {
66
- reject(new Error(`${_.capitalize(streamType)} '${err.syscall}' error: ${err.stack}`));
67
- });
68
-
69
- if (opts.ignoreOutput) {
70
- // https://github.com/nodejs/node/issues/4236
71
- proc[streamType].on('data', () => {});
72
- return;
73
- }
74
-
75
- // keep track of the stream if we don't want to ignore it
76
- proc[streamType].on('data', (/** @type {Buffer} */ chunk) => {
77
- buffer.add(chunk);
78
- if (opts.logger && _.isFunction(opts.logger.debug)) {
79
- opts.logger.debug(chunk.toString());
80
- }
81
- });
82
- };
83
- handleStream('stdout', stdoutBuffer);
84
- handleStream('stderr', stderrBuffer);
85
-
86
- /**
87
- * @template {boolean} U
88
- * @param {U} isBuffer
89
- * @returns {U extends true ? {stdout: Buffer, stderr: Buffer} : {stdout: string, stderr: string}}
90
- */
91
- function getStdio (isBuffer) {
92
- const stdout = isBuffer ? stdoutBuffer.value() : stdoutBuffer.value().toString(opts.encoding);
93
- const stderr = isBuffer ? stderrBuffer.value() : stderrBuffer.value().toString(opts.encoding);
94
- return /** @type {U extends true ? {stdout: Buffer, stderr: Buffer} : {stdout: string, stderr: string}} */(
95
- {stdout, stderr}
96
- );
97
- }
98
-
99
- // if the process ends, either resolve or reject the promise based on the
100
- // exit code of the process. either way, attach stdout, stderr, and code.
101
- // Also clean up the timer if it exists
102
- proc.on('close', (code) => {
103
- if (timer) {
104
- clearTimeout(timer);
105
- }
106
- const {stdout, stderr} = getStdio(isBuffer);
107
- if (code === 0) {
108
- resolve(/** @type {BufferProp<T> extends true ? TeenProcessExecBufferResult : TeenProcessExecStringResult} */({stdout, stderr, code}));
109
- } else {
110
- let err = new Error(`Command '${rep}' exited with code ${code}`);
111
- err = Object.assign(err, {stdout, stderr, code});
112
- reject(err);
113
- }
114
- });
115
-
116
- // if we set a timeout on the child process, cut into the execution and
117
- // reject if the timeout is reached. Attach the stdout/stderr we currently
118
- // have in case it's helpful in debugging
119
- if (opts.timeout) {
120
- timer = setTimeout(() => {
121
- const {stdout, stderr} = getStdio(isBuffer);
122
- let err = new Error(`Command '${rep}' timed out after ${opts.timeout}ms`);
123
- err = Object.assign(err, {stdout, stderr, code: null});
124
- reject(err);
125
- // reject and THEN kill to avoid race conditions with the handlers
126
- // above
127
- proc.kill(opts.killSignal);
128
- }, opts.timeout);
129
- }
130
- });
131
- }
132
-
133
- export { exec };
134
- export default exec;
135
-
136
- /**
137
- * Options on top of `SpawnOptions`, unique to `teen_process.`
138
- * @typedef {Object} TeenProcessProps
139
- * @property {boolean} [ignoreOutput] - Ignore & discard all output
140
- * @property {boolean} [isBuffer] - Return output as a Buffer
141
- * @property {TeenProcessLogger} [logger] - Logger to use for debugging
142
- * @property {number} [maxStdoutBufferSize] - Maximum size of `stdout` buffer
143
- * @property {number} [maxStderrBufferSize] - Maximum size of `stderr` buffer
144
- * @property {BufferEncoding} [encoding='utf8'] - Encoding to use for output
145
- */
146
-
147
- /**
148
- * A logger object understood by {@link exec teen_process.exec}.
149
- * @typedef {Object} TeenProcessLogger
150
- * @property {(...args: any[]) => void} debug
151
- */
152
-
153
- /**
154
- * Options for {@link exec teen_process.exec}.
155
- * @typedef {import('child_process').SpawnOptions & TeenProcessProps} TeenProcessExecOptions
156
- */
157
-
158
- /**
159
- * The value {@link exec teen_process.exec} resolves to when `isBuffer` is `false`
160
- * @typedef {Object} TeenProcessExecStringResult
161
- * @property {string} stdout - Stdout
162
- * @property {string} stderr - Stderr
163
- * @property {number?} code - Exit code
164
- */
165
-
166
- /**
167
- * The value {@link exec teen_process.exec} resolves to when `isBuffer` is `true`
168
- * @typedef {Object} TeenProcessExecBufferResult
169
- * @property {Buffer} stdout - Stdout
170
- * @property {Buffer} stderr - Stderr
171
- * @property {number?} code - Exit code
172
- */
173
-
174
- /**
175
- * Extra props {@link exec teen_process.exec} adds to its error objects
176
- * @typedef {Object} TeenProcessExecErrorProps
177
- * @property {string} stdout - STDOUT
178
- * @property {string} stderr - STDERR
179
- * @property {number?} code - Exit code
180
- */
181
-
182
- /**
183
- * Error thrown by {@link exec teen_process.exec}
184
- * @typedef {Error & TeenProcessExecErrorProps} TeenProcessExecError
185
- */
186
-
187
- /**
188
- * @template {{isBuffer?: boolean}} MaybeBuffer
189
- * @typedef {MaybeBuffer['isBuffer']} BufferProp
190
- * @private
191
- */
package/lib/helpers.js DELETED
@@ -1,38 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
-
4
- /**
5
- * Decorates ENOENT error received from a spawn system call
6
- * with a more descriptive message, so it could be properly handled by a user.
7
- *
8
- * @param {NodeJS.ErrnoException} error Original error instance. !!! The instance is mutated after
9
- * this helper function invocation
10
- * @param {string} cmd Original command to execute
11
- * @param {string?} [cwd] Optional path to the current working dir
12
- * @returns {Promise<NodeJS.ErrnoException>} Mutated error instance with an improved description or an
13
- * unchanged error instance
14
- */
15
- async function formatEnoent (error, cmd, cwd = null) {
16
- if (cwd) {
17
- try {
18
- const stat = await fs.stat(cwd);
19
- if (!stat.isDirectory()) {
20
- error.message = `The working directory '${cwd}' of '${cmd}' is not a valid folder path`;
21
- return error;
22
- }
23
- } catch (e) {
24
- if (e.code === 'ENOENT') {
25
- error.message = `The working directory '${cwd}' of '${cmd}' does not exist`;
26
- return error;
27
- }
28
- }
29
- }
30
-
31
- const curDir = path.resolve(cwd ?? process.cwd());
32
- const pathMsg = process.env.PATH ?? 'which is not defined for the process';
33
- error.message = `'${cmd}' executable is not found neither in the process working folder (${curDir}) ` +
34
- `nor in any folders specified in the PATH environment variable (${pathMsg})`;
35
- return error;
36
- }
37
-
38
- export { formatEnoent };
package/lib/index.js DELETED
@@ -1,9 +0,0 @@
1
- import * as cp from 'child_process';
2
- import * as spIndex from './subprocess';
3
- import * as execIndex from './exec';
4
-
5
- const { spawn } = cp;
6
- const { SubProcess } = spIndex;
7
- const { exec } = execIndex;
8
-
9
- export { exec, spawn, SubProcess };
package/lib/subprocess.js DELETED
@@ -1,329 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { EventEmitter } from 'events';
3
- import B from 'bluebird';
4
- import { quote } from 'shell-quote';
5
- import _ from 'lodash';
6
- import { formatEnoent } from './helpers';
7
- import { createInterface } from 'node:readline';
8
-
9
- /**
10
- * @template {SubProcessOptions} TSubProcessOptions
11
- */
12
- export class SubProcess extends EventEmitter {
13
- /**
14
- * @callback StartDetector
15
- * @param {TSubProcessOptions extends TIsBufferOpts ? Buffer : string} stdout
16
- * @param {TSubProcessOptions extends TIsBufferOpts ? Buffer : string} [stderr]
17
- * @returns {any}
18
- */
19
-
20
- /** @type {import('child_process').ChildProcess | null} */
21
- proc;
22
- /** @type {string[]} */
23
- args;
24
- /**
25
- * @type {string}
26
- */
27
- cmd;
28
- /**
29
- * @type {SubProcessOptions}
30
- */
31
- opts;
32
- /**
33
- * @type {boolean}
34
- */
35
- expectingExit;
36
- /**
37
- * @type {string}
38
- */
39
- rep;
40
-
41
- /**
42
- * @param {string} cmd
43
- * @param {string[]} [args=[]]
44
- * @param {TSubProcessOptions} [opts]
45
- */
46
- constructor (cmd, args = [], opts) {
47
- super();
48
- if (!cmd) throw new Error('Command is required'); // eslint-disable-line curly
49
- if (!_.isString(cmd)) throw new Error('Command must be a string'); // eslint-disable-line curly
50
- if (!_.isArray(args)) throw new Error('Args must be an array'); // eslint-disable-line curly
51
-
52
- this.cmd = cmd;
53
- this.args = args;
54
- this.proc = null;
55
- this.opts = opts ?? {};
56
- this.expectingExit = false;
57
-
58
- // get a quoted representation of the command for error strings
59
- this.rep = quote([cmd, ...args]);
60
- }
61
-
62
- get isRunning () {
63
- // presence of `proc` means we have connected and started
64
- return !!this.proc;
65
- }
66
-
67
- /**
68
- *
69
- * @param {string} streamName
70
- * @param {Iterable<string>|string} lines
71
- */
72
- emitLines (streamName, lines) {
73
- const doEmit = (/** @type {string} */ line) => this.emit('stream-line', `[${streamName.toUpperCase()}] ${line}`);
74
-
75
- if (_.isString(lines)) {
76
- doEmit(lines);
77
- } else {
78
- for (const line of lines) {
79
- doEmit(line);
80
- }
81
- }
82
- }
83
-
84
- /**
85
- * spawn the subprocess and return control whenever we deem that it has fully
86
- * "started"
87
- *
88
- * @param {StartDetector|number?} startDetector
89
- * @param {number?} timeoutMs
90
- * @param {boolean} detach
91
- * @returns {Promise<void>}
92
- */
93
- async start (startDetector = null, timeoutMs = null, detach = false) {
94
- let startDelay = 10;
95
-
96
- const genericStartDetector = /** @type {StartDetector} */(function genericStartDetector (stdout, stderr) {
97
- return stdout || stderr;
98
- });
99
-
100
- // the default start detector simply returns true when we get any output
101
- if (startDetector === null) {
102
- startDetector = genericStartDetector;
103
- }
104
-
105
- // if the user passes a number, then we simply delay a certain amount of
106
- // time before returning control, rather than waiting for a condition
107
- if (_.isNumber(startDetector)) {
108
- startDelay = startDetector;
109
- startDetector = null;
110
- }
111
-
112
- // if the user passes in a boolean as one of the arguments, use it for `detach`
113
- if (_.isBoolean(startDetector) && startDetector) {
114
- if (!this.opts.detached) {
115
- throw new Error(`Unable to detach process that is not started with 'detached' option`);
116
- }
117
- detach = true;
118
- startDetector = genericStartDetector;
119
- } else if (_.isBoolean(timeoutMs) && timeoutMs) {
120
- if (!this.opts.detached) {
121
- throw new Error(`Unable to detach process that is not started with 'detached' option`);
122
- }
123
- detach = true;
124
- timeoutMs = null;
125
- }
126
-
127
- // return a promise so we can wrap the async behavior
128
- return await new B((resolve, reject) => {
129
- // actually spawn the subproc
130
- this.proc = spawn(this.cmd, this.args, this.opts);
131
-
132
- // this function handles output that we collect from the subproc
133
- /**
134
- * @param { {
135
- * stdout: TSubProcessOptions extends TIsBufferOpts ? Buffer : string,
136
- * stderr: TSubProcessOptions extends TIsBufferOpts ? Buffer : string
137
- * } } streams
138
- */
139
- const handleOutput = (streams) => {
140
- const {stdout, stderr} = streams;
141
- // if we have a startDetector, run it on the output so we can resolve/
142
- // reject and move on from start
143
- try {
144
- if (_.isFunction(startDetector) && startDetector(stdout, stderr)) {
145
- startDetector = null;
146
- resolve();
147
- }
148
- } catch (e) {
149
- reject(e);
150
- }
151
-
152
- // emit the actual output for whomever's listening
153
- this.emit('output', stdout, stderr);
154
- };
155
-
156
- // if we get an error spawning the proc, reject and clean up the proc
157
- this.proc.on('error', /** @param {NodeJS.ErrnoException} err */ async (err) => {
158
- this.proc?.removeAllListeners('exit');
159
- this.proc?.kill('SIGINT');
160
-
161
- if (err.code === 'ENOENT') {
162
- err = await formatEnoent(err, this.cmd, this.opts?.cwd?.toString());
163
- }
164
- reject(err);
165
-
166
- this.proc?.unref();
167
- this.proc = null;
168
- });
169
-
170
- const handleStreamLines = (/** @type {string} */ streamName, /** @type {import('stream').Readable} */ input) => {
171
- const rl = createInterface({input});
172
- rl.on('line', (line) => {
173
- // This event is a legacy one
174
- // It always produces a single-item array
175
- if (this.listenerCount(`lines-${streamName}`)) {
176
- this.emit(`lines-${streamName}`, [line]);
177
- }
178
- this.emit(`line-${streamName}`, line);
179
- if (this.listenerCount('stream-line')) {
180
- this.emitLines(streamName, line);
181
- }
182
- });
183
- };
184
-
185
- const isBuffer = Boolean(this.opts.isBuffer);
186
- const encoding = this.opts.encoding || 'utf8';
187
-
188
- if (this.proc.stdout) {
189
- this.proc.stdout.on('data', (chunk) =>
190
- handleOutput({
191
- stdout: isBuffer ? chunk : chunk.toString(encoding),
192
- // @ts-ignore This is OK
193
- stderr: isBuffer ? Buffer.alloc(0) : '',
194
- }),
195
- );
196
- handleStreamLines('stdout', this.proc.stdout);
197
- }
198
-
199
- if (this.proc.stderr) {
200
- this.proc.stderr.on('data', (chunk) =>
201
- handleOutput({
202
- // @ts-ignore This is OK
203
- stdout: isBuffer ? Buffer.alloc(0) : '',
204
- stderr: isBuffer ? chunk : chunk.toString(encoding)
205
- }),
206
- );
207
- handleStreamLines('stderr', this.proc.stderr);
208
- }
209
-
210
- // when the proc exits, we might still have a buffer of lines we were
211
- // waiting on more chunks to complete. Go ahead and emit those, then
212
- // re-emit the exit so a listener can handle the possibly-unexpected exit
213
- this.proc.on('exit', (code, signal) => {
214
- this.emit('exit', code, signal);
215
-
216
- // in addition to the bare exit event, also emit one of three other
217
- // events that contain more helpful information:
218
- // 'stop': we stopped this
219
- // 'die': the process ended out of our control with a non-zero exit
220
- // 'end': the process ended out of our control with a zero exit
221
- let event = this.expectingExit ? 'stop' : 'die';
222
- if (!this.expectingExit && code === 0) {
223
- event = 'end';
224
- }
225
- this.emit(event, code, signal);
226
-
227
- // finally clean up the proc and make sure to reset our exit
228
- // expectations
229
- this.proc = null;
230
- this.expectingExit = false;
231
- });
232
-
233
- // if the user hasn't given us a startDetector, instead just resolve
234
- // when startDelay ms have passed
235
- if (!startDetector) {
236
- setTimeout(() => { resolve(); }, startDelay);
237
- }
238
-
239
- // if the user has given us a timeout, start the clock for rejecting
240
- // the promise if we take too long to start
241
- if (_.isNumber(timeoutMs)) {
242
- setTimeout(() => {
243
- reject(new Error(`The process did not start within ${timeoutMs}ms ` +
244
- `(cmd: '${this.rep}')`));
245
- }, timeoutMs);
246
- }
247
- }).finally(() => {
248
- if (detach && this.proc) {
249
- this.proc.unref();
250
- }
251
- });
252
- }
253
-
254
- /**
255
- * @deprecated This method is deprecated and will be removed
256
- */
257
- handleLastLines () {
258
- // TODO: THis is a noop left for backward compatibility.
259
- // TODO: Remove it after the major version bump
260
- }
261
-
262
- /**
263
- *
264
- * @param {NodeJS.Signals} signal
265
- * @param {number} timeout
266
- * @returns {Promise<void>}
267
- */
268
- async stop (signal = 'SIGTERM', timeout = 10000) {
269
- if (!this.isRunning) {
270
- throw new Error(`Can't stop process; it's not currently running (cmd: '${this.rep}')`);
271
- }
272
- return await new B((resolve, reject) => {
273
- this.proc?.on('close', resolve);
274
- this.expectingExit = true;
275
- this.proc?.kill(signal);
276
- // this timeout needs unref() or node will wait for the timeout to fire before
277
- // exiting the process.
278
- setTimeout(() => {
279
- reject(new Error(`Process didn't end after ${timeout}ms (cmd: '${this.rep}')`));
280
- }, timeout).unref();
281
- });
282
- }
283
-
284
- async join (allowedExitCodes = [0]) {
285
- if (!this.isRunning) {
286
- throw new Error(`Cannot join process; it is not currently running (cmd: '${this.rep}')`);
287
- }
288
-
289
- return await new B((resolve, reject) => {
290
- this.proc?.on('exit', (code) => {
291
- if (code !== null && allowedExitCodes.indexOf(code) === -1) {
292
- reject(new Error(`Process ended with exitcode ${code} (cmd: '${this.rep}')`));
293
- } else {
294
- resolve(code);
295
- }
296
- });
297
- });
298
- }
299
-
300
- /*
301
- * This will only work if the process is created with the `detached` option
302
- */
303
- detachProcess () {
304
- if (!this.opts.detached) {
305
- // this means that there is a misconfiguration in the calling code
306
- throw new Error(`Unable to detach process that is not started with 'detached' option`);
307
- }
308
- if (this.proc) {
309
- this.proc.unref();
310
- }
311
- }
312
-
313
- get pid () {
314
- return this.proc ? this.proc.pid : null;
315
- }
316
- }
317
-
318
- export default SubProcess;
319
-
320
- /**
321
- * @typedef {Object} SubProcessCustomOptions
322
- * @property {boolean} [isBuffer]
323
- * @property {string} [encoding]
324
- */
325
-
326
- /**
327
- * @typedef {SubProcessCustomOptions & import('child_process').SpawnOptionsWithoutStdio} SubProcessOptions
328
- * @typedef {{isBuffer: true}} TIsBufferOpts
329
- */