You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
15 KiB

  1. "use strict";
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  4. return new (P || (P = Promise))(function (resolve, reject) {
  5. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  6. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  7. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  8. step((generator = generator.apply(thisArg, _arguments || [])).next());
  9. });
  10. };
  11. Object.defineProperty(exports, "__esModule", { value: true });
  12. exports.PythonShell = exports.PythonShellError = void 0;
  13. const events_1 = require("events");
  14. const child_process_1 = require("child_process");
  15. const os_1 = require("os");
  16. const path_1 = require("path");
  17. const fs_1 = require("fs");
  18. const util_1 = require("util");
  19. function toArray(source) {
  20. if (typeof source === 'undefined' || source === null) {
  21. return [];
  22. }
  23. else if (!Array.isArray(source)) {
  24. return [source];
  25. }
  26. return source;
  27. }
  28. /**
  29. * adds arguments as properties to obj
  30. */
  31. function extend(obj, ...args) {
  32. Array.prototype.slice.call(arguments, 1).forEach(function (source) {
  33. if (source) {
  34. for (let key in source) {
  35. obj[key] = source[key];
  36. }
  37. }
  38. });
  39. return obj;
  40. }
  41. /**
  42. * gets a random int from 0-10000000000
  43. */
  44. function getRandomInt() {
  45. return Math.floor(Math.random() * 10000000000);
  46. }
  47. class PythonShellError extends Error {
  48. }
  49. exports.PythonShellError = PythonShellError;
  50. /**
  51. * An interactive Python shell exchanging data through stdio
  52. * @param {string} script The python script to execute
  53. * @param {object} [options] The launch options (also passed to child_process.spawn)
  54. * @constructor
  55. */
  56. class PythonShell extends events_1.EventEmitter {
  57. /**
  58. * spawns a python process
  59. * @param scriptPath path to script. Relative to current directory or options.scriptFolder if specified
  60. * @param options
  61. */
  62. constructor(scriptPath, options) {
  63. super();
  64. /**
  65. * returns either pythonshell func (if val string) or custom func (if val Function)
  66. */
  67. function resolve(type, val) {
  68. if (typeof val === 'string') {
  69. // use a built-in function using its name
  70. return PythonShell[type][val];
  71. }
  72. else if (typeof val === 'function') {
  73. // use a custom function
  74. return val;
  75. }
  76. }
  77. if (scriptPath.trim().length == 0)
  78. throw Error("scriptPath cannot be empty! You must give a script for python to run");
  79. let self = this;
  80. let errorData = '';
  81. events_1.EventEmitter.call(this);
  82. options = extend({}, PythonShell.defaultOptions, options);
  83. let pythonPath;
  84. if (!options.pythonPath) {
  85. pythonPath = PythonShell.defaultPythonPath;
  86. }
  87. else
  88. pythonPath = options.pythonPath;
  89. let pythonOptions = toArray(options.pythonOptions);
  90. let scriptArgs = toArray(options.args);
  91. this.scriptPath = path_1.join(options.scriptPath || '', scriptPath);
  92. this.command = pythonOptions.concat(this.scriptPath, scriptArgs);
  93. this.mode = options.mode || 'text';
  94. this.formatter = resolve('format', options.formatter || this.mode);
  95. this.parser = resolve('parse', options.parser || this.mode);
  96. // We don't expect users to ever format stderr as JSON so we default to text mode
  97. this.stderrParser = resolve('parse', options.stderrParser || 'text');
  98. this.terminated = false;
  99. this.childProcess = child_process_1.spawn(pythonPath, this.command, options);
  100. ['stdout', 'stdin', 'stderr'].forEach(function (name) {
  101. self[name] = self.childProcess[name];
  102. self.parser && self[name] && self[name].setEncoding(options.encoding || 'utf8');
  103. });
  104. // parse incoming data on stdout
  105. if (this.parser && this.stdout) {
  106. this.stdout.on('data', this.receive.bind(this));
  107. }
  108. // listen to stderr and emit errors for incoming data
  109. if (this.stderrParser && this.stderr) {
  110. this.stderr.on('data', this.receiveStderr.bind(this));
  111. }
  112. if (this.stderr) {
  113. this.stderr.on('data', function (data) {
  114. errorData += '' + data;
  115. });
  116. this.stderr.on('end', function () {
  117. self.stderrHasEnded = true;
  118. terminateIfNeeded();
  119. });
  120. }
  121. else {
  122. self.stderrHasEnded = true;
  123. }
  124. if (this.stdout) {
  125. this.stdout.on('end', function () {
  126. self.stdoutHasEnded = true;
  127. terminateIfNeeded();
  128. });
  129. }
  130. else {
  131. self.stdoutHasEnded = true;
  132. }
  133. this.childProcess.on('exit', function (code, signal) {
  134. self.exitCode = code;
  135. self.exitSignal = signal;
  136. terminateIfNeeded();
  137. });
  138. function terminateIfNeeded() {
  139. if (!self.stderrHasEnded || !self.stdoutHasEnded || (self.exitCode == null && self.exitSignal == null))
  140. return;
  141. let err;
  142. if (self.exitCode && self.exitCode !== 0) {
  143. if (errorData) {
  144. err = self.parseError(errorData);
  145. }
  146. else {
  147. err = new PythonShellError('process exited with code ' + self.exitCode);
  148. }
  149. err = extend(err, {
  150. executable: pythonPath,
  151. options: pythonOptions.length ? pythonOptions : null,
  152. script: self.scriptPath,
  153. args: scriptArgs.length ? scriptArgs : null,
  154. exitCode: self.exitCode
  155. });
  156. // do not emit error if only a callback is used
  157. if (self.listeners('error').length || !self._endCallback) {
  158. self.emit('error', err);
  159. }
  160. }
  161. self.terminated = true;
  162. self.emit('close');
  163. self._endCallback && self._endCallback(err, self.exitCode, self.exitSignal);
  164. }
  165. ;
  166. }
  167. /**
  168. * checks syntax without executing code
  169. * @returns {Promise} rejects w/ stderr if syntax failure
  170. */
  171. static checkSyntax(code) {
  172. return __awaiter(this, void 0, void 0, function* () {
  173. const randomInt = getRandomInt();
  174. const filePath = os_1.tmpdir() + path_1.sep + `pythonShellSyntaxCheck${randomInt}.py`;
  175. // todo: replace this with util.promisify (once we no longer support node v7)
  176. return new Promise((resolve, reject) => {
  177. fs_1.writeFile(filePath, code, (err) => {
  178. if (err)
  179. reject(err);
  180. resolve(this.checkSyntaxFile(filePath));
  181. });
  182. });
  183. });
  184. }
  185. static getPythonPath() {
  186. return this.defaultOptions.pythonPath ? this.defaultOptions.pythonPath : this.defaultPythonPath;
  187. }
  188. /**
  189. * checks syntax without executing code
  190. * @returns {Promise} rejects w/ stderr if syntax failure
  191. */
  192. static checkSyntaxFile(filePath) {
  193. return __awaiter(this, void 0, void 0, function* () {
  194. const pythonPath = this.getPythonPath();
  195. const compileCommand = `${pythonPath} -m py_compile ${filePath}`;
  196. return new Promise((resolve, reject) => {
  197. child_process_1.exec(compileCommand, (error, stdout, stderr) => {
  198. if (error == null)
  199. resolve();
  200. else
  201. reject(stderr);
  202. });
  203. });
  204. });
  205. }
  206. /**
  207. * Runs a Python script and returns collected messages
  208. * @param {string} scriptPath The path to the script to execute
  209. * @param {Options} options The execution options
  210. * @param {Function} callback The callback function to invoke with the script results
  211. * @return {PythonShell} The PythonShell instance
  212. */
  213. static run(scriptPath, options, callback) {
  214. let pyshell = new PythonShell(scriptPath, options);
  215. let output = [];
  216. return pyshell.on('message', function (message) {
  217. output.push(message);
  218. }).end(function (err) {
  219. return callback(err ? err : null, output.length ? output : null);
  220. });
  221. }
  222. ;
  223. /**
  224. * Runs the inputted string of python code and returns collected messages. DO NOT ALLOW UNTRUSTED USER INPUT HERE!
  225. * @param {string} code The python code to execute
  226. * @param {Options} options The execution options
  227. * @param {Function} callback The callback function to invoke with the script results
  228. * @return {PythonShell} The PythonShell instance
  229. */
  230. static runString(code, options, callback) {
  231. // put code in temp file
  232. const randomInt = getRandomInt();
  233. const filePath = os_1.tmpdir + path_1.sep + `pythonShellFile${randomInt}.py`;
  234. fs_1.writeFileSync(filePath, code);
  235. return PythonShell.run(filePath, options, callback);
  236. }
  237. ;
  238. static getVersion(pythonPath) {
  239. if (!pythonPath)
  240. pythonPath = this.getPythonPath();
  241. const execPromise = util_1.promisify(child_process_1.exec);
  242. return execPromise(pythonPath + " --version");
  243. }
  244. static getVersionSync(pythonPath) {
  245. if (!pythonPath)
  246. pythonPath = this.getPythonPath();
  247. return child_process_1.execSync(pythonPath + " --version").toString();
  248. }
  249. /**
  250. * Parses an error thrown from the Python process through stderr
  251. * @param {string|Buffer} data The stderr contents to parse
  252. * @return {Error} The parsed error with extended stack trace when traceback is available
  253. */
  254. parseError(data) {
  255. let text = '' + data;
  256. let error;
  257. if (/^Traceback/.test(text)) {
  258. // traceback data is available
  259. let lines = text.trim().split(os_1.EOL);
  260. let exception = lines.pop();
  261. error = new PythonShellError(exception);
  262. error.traceback = data;
  263. // extend stack trace
  264. error.stack += os_1.EOL + ' ----- Python Traceback -----' + os_1.EOL + ' ';
  265. error.stack += lines.slice(1).join(os_1.EOL + ' ');
  266. }
  267. else {
  268. // otherwise, create a simpler error with stderr contents
  269. error = new PythonShellError(text);
  270. }
  271. return error;
  272. }
  273. ;
  274. /**
  275. * Sends a message to the Python shell through stdin
  276. * Override this method to format data to be sent to the Python process
  277. * @returns {PythonShell} The same instance for chaining calls
  278. */
  279. send(message) {
  280. if (!this.stdin)
  281. throw new Error("stdin not open for writing");
  282. let data = this.formatter ? this.formatter(message) : message;
  283. if (this.mode !== 'binary')
  284. data += os_1.EOL;
  285. this.stdin.write(data);
  286. return this;
  287. }
  288. ;
  289. /**
  290. * Parses data received from the Python shell stdout stream and emits "message" events
  291. * This method is not used in binary mode
  292. * Override this method to parse incoming data from the Python process into messages
  293. * @param {string|Buffer} data The data to parse into messages
  294. */
  295. receive(data) {
  296. return this.receiveInternal(data, 'message');
  297. }
  298. ;
  299. /**
  300. * Parses data received from the Python shell stderr stream and emits "stderr" events
  301. * This method is not used in binary mode
  302. * Override this method to parse incoming logs from the Python process into messages
  303. * @param {string|Buffer} data The data to parse into messages
  304. */
  305. receiveStderr(data) {
  306. return this.receiveInternal(data, 'stderr');
  307. }
  308. ;
  309. receiveInternal(data, emitType) {
  310. let self = this;
  311. let parts = ('' + data).split(os_1.EOL);
  312. if (parts.length === 1) {
  313. // an incomplete record, keep buffering
  314. this._remaining = (this._remaining || '') + parts[0];
  315. return this;
  316. }
  317. let lastLine = parts.pop();
  318. // fix the first line with the remaining from the previous iteration of 'receive'
  319. parts[0] = (this._remaining || '') + parts[0];
  320. // keep the remaining for the next iteration of 'receive'
  321. this._remaining = lastLine;
  322. parts.forEach(function (part) {
  323. if (emitType == 'message')
  324. self.emit(emitType, self.parser(part));
  325. else if (emitType == 'stderr')
  326. self.emit(emitType, self.stderrParser(part));
  327. });
  328. return this;
  329. }
  330. /**
  331. * Closes the stdin stream. Unless python is listening for stdin in a loop
  332. * this should cause the process to finish its work and close.
  333. * @returns {PythonShell} The same instance for chaining calls
  334. */
  335. end(callback) {
  336. if (this.childProcess.stdin) {
  337. this.childProcess.stdin.end();
  338. }
  339. this._endCallback = callback;
  340. return this;
  341. }
  342. ;
  343. /**
  344. * Sends a kill signal to the process
  345. * @returns {PythonShell} The same instance for chaining calls
  346. */
  347. kill(signal) {
  348. this.childProcess.kill(signal);
  349. this.terminated = true;
  350. return this;
  351. }
  352. ;
  353. /**
  354. * Alias for kill.
  355. * @deprecated
  356. */
  357. terminate(signal) {
  358. // todo: remove this next breaking release
  359. return this.kill(signal);
  360. }
  361. }
  362. exports.PythonShell = PythonShell;
  363. // starting 2020 python2 is deprecated so we choose 3 as default
  364. PythonShell.defaultPythonPath = process.platform != "win32" ? "python3" : "py";
  365. PythonShell.defaultOptions = {}; //allow global overrides for options
  366. // built-in formatters
  367. PythonShell.format = {
  368. text: function toText(data) {
  369. if (!data)
  370. return '';
  371. else if (typeof data !== 'string')
  372. return data.toString();
  373. return data;
  374. },
  375. json: function toJson(data) {
  376. return JSON.stringify(data);
  377. }
  378. };
  379. //built-in parsers
  380. PythonShell.parse = {
  381. text: function asText(data) {
  382. return data;
  383. },
  384. json: function asJson(data) {
  385. return JSON.parse(data);
  386. }
  387. };
  388. ;
  389. //# sourceMappingURL=index.js.map