upython.pas 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. // SPDX-License-Identifier: GPL-3.0-only
  2. unit UPython;
  3. {$mode objfpc}{$H+}
  4. interface
  5. uses
  6. Classes, SysUtils, UProcessAuto;
  7. const
  8. DefaultPythonBin = {$IFDEF WINDOWS}'pyw'{$ELSE}'python3'{$ENDIF};
  9. {$IFDEF DARWIN}
  10. UserPythonBin = '/usr/local/bin/python3';
  11. {$ENDIF}
  12. type
  13. TReceiveLineEvent = procedure(ASender: TObject; ALine: UTF8String) of object;
  14. TCommandEvent = procedure(ASender: TObject; ACommand, AParam: UTF8String; out AResult: UTF8String) of object;
  15. TWarningEvent = procedure(ASender: TObject; AMessage: UTF8String; out AProceed: boolean) of object;
  16. { TPythonScript }
  17. TPythonScript = class
  18. private
  19. FCheckScriptSecure: boolean;
  20. FPythonBin: string;
  21. FPythonVersion: string;
  22. FLinePrefix: RawByteString;
  23. FOnCommand: TCommandEvent;
  24. FOnError: TReceiveLineEvent;
  25. FOnBusy: TNotifyEvent;
  26. FOnWarning: TWarningEvent;
  27. FOnOutputLine: TReceiveLineEvent;
  28. FPythonSend: TSendLineMethod;
  29. FErrorText: UTF8String;
  30. FFirstOutput: boolean;
  31. function GetPythonVersionMajor: integer;
  32. procedure PythonError(ALine: RawByteString);
  33. procedure PythonOutput(ALine: RawByteString);
  34. procedure PythonBusy(var {%H-}ASleep: boolean);
  35. function CheckScriptAndDependencySafe(AFilename: UTF8String; APythonVersion: integer): boolean;
  36. public
  37. constructor Create(APythonBin: string = DefaultPythonBin);
  38. function Run(AScriptFilename: UTF8String; APythonVersion: integer = 3): boolean;
  39. class function DefaultScriptDirectory: string;
  40. property OnOutputLine: TReceiveLineEvent read FOnOutputLine write FOnOutputLine;
  41. property OnError: TReceiveLineEvent read FOnError write FOnError;
  42. property OnCommand: TCommandEvent read FOnCommand write FOnCommand;
  43. property OnBusy: TNotifyEvent read FOnBusy write FOnBusy;
  44. property OnWarning: TWarningEvent read FOnWarning write FOnWarning;
  45. property PythonVersion: string read FPythonVersion;
  46. property PythonVersionMajor: integer read GetPythonVersionMajor;
  47. property ErrorText: UTF8String read FErrorText;
  48. property CheckScriptSecure: boolean read FCheckScriptSecure write FCheckScriptSecure;
  49. end;
  50. function GetPythonVersion(APythonBin: string = DefaultPythonBin): string;
  51. function GetScriptTitle(AFilename: string): string;
  52. function CheckPythonScriptSafe(AFilename: string; out ASafeModules, AUnsafeModules: TStringList): boolean;
  53. var
  54. CustomScriptDirectory: string;
  55. implementation
  56. uses process, UResourceStrings, Forms, UTranslation;
  57. var
  58. PythonVersionCache: record
  59. Bin: string;
  60. Version: string;
  61. end;
  62. function GetPythonVersion(APythonBin: string = DefaultPythonBin): string;
  63. const PythonVersionPrefix = 'Python ';
  64. var versionStr: string;
  65. begin
  66. if (PythonVersionCache.Bin <> APythonBin) or (PythonVersionCache.Version = '?') then
  67. begin
  68. RunCommand(APythonBin, ['-V'], versionStr, []);
  69. PythonVersionCache.Bin := APythonBin;
  70. if versionStr.StartsWith(PythonVersionPrefix) then
  71. PythonVersionCache.Version := trim(copy(versionStr,length(PythonVersionPrefix)+1,
  72. length(versionStr)-length(PythonVersionPrefix)))
  73. else
  74. PythonVersionCache.Version := '?';
  75. end;
  76. result := PythonVersionCache.Version;
  77. end;
  78. function GetScriptTitle(AFilename: string): string;
  79. var t: textfile;
  80. header: string;
  81. matchLang: boolean;
  82. procedure RetrieveTitle(AText: string; ADefault: boolean; var title: string; out ALangMatch: boolean);
  83. var
  84. posCloseBracket: SizeInt;
  85. lang: String;
  86. begin
  87. If AText.StartsWith('#') then
  88. Delete(AText, 1,1);
  89. AText := AText.Trim;
  90. ALangMatch := false;
  91. if AText.StartsWith('(') then
  92. begin
  93. posCloseBracket := pos(')', AText);
  94. if posCloseBracket > 0 then
  95. begin
  96. lang := copy(AText, 2, posCloseBracket-2);
  97. delete(AText, 1, posCloseBracket);
  98. AText := AText.Trim;
  99. if lang = ActiveLanguage then
  100. ALangMatch:= true;
  101. end;
  102. end else
  103. begin
  104. if not ADefault then exit;
  105. if ActiveLanguage = DesignLanguage then ALangMatch:= true;
  106. end;
  107. if ALangMatch or ADefault then
  108. begin
  109. title := AText;
  110. title := StringReplace(title, ' >', '>', [rfReplaceAll]);
  111. title := StringReplace(title, '> ', '>', [rfReplaceAll]);
  112. end;
  113. end;
  114. procedure TranslateWithPoFile(var title: string);
  115. var elements: TStringList;
  116. i: integer;
  117. u: string;
  118. begin
  119. elements := TStringList.Create;
  120. try
  121. elements.Delimiter := '>';
  122. elements.QuoteChar := #0;
  123. elements.DelimitedText := StringReplace(title, ' ', #160, [rfReplaceAll]);
  124. for i := 0 to elements.Count-1 do
  125. begin
  126. u := Trim(StringReplace(elements[i], #160, ' ', [rfReplaceAll]));
  127. elements[i] := DoTranslate('', u);
  128. end;
  129. finally
  130. title := elements.DelimitedText;
  131. elements.free;
  132. end;
  133. end;
  134. begin
  135. result := '';
  136. assignFile(t, AFilename);
  137. reset(t);
  138. try
  139. readln(t, header);
  140. if header.StartsWith('#') then
  141. begin
  142. RetrieveTitle(header, true, result, matchLang);
  143. while not matchLang do
  144. begin
  145. readln(t, header);
  146. if header.StartsWith('#') then
  147. begin
  148. RetrieveTitle(header, false, result, matchLang);
  149. end else break;
  150. end;
  151. if not matchLang then
  152. TranslateWithPoFile(result);
  153. end;
  154. finally
  155. closefile(t);
  156. end;
  157. end;
  158. function CheckPythonScriptSafe(AFilename: string; out ASafeModules, AUnsafeModules: TStringList): boolean;
  159. function binarySearch(x: string; a: array of string): integer;
  160. var L, R, M: integer; // left, right, middle
  161. begin
  162. if Length(a)=0 then Exit(-1);
  163. L := Low (a);
  164. R := High(a);
  165. while (L <= R) do begin
  166. M := (L + R) div 2;
  167. if (x = a[M]) then Exit(M); // found x in a
  168. if (x > a[M])
  169. then L := Succ(M)
  170. else R := Pred(M);
  171. end;
  172. Exit(-1) // did not found x in a
  173. end;
  174. function idOk(AId: string; var importCount: integer): boolean;
  175. const forbidden: array[0..6] of string =
  176. ('__import__',
  177. 'compile',
  178. 'eval',
  179. 'exec',
  180. 'getattr',
  181. 'globals',
  182. 'locals');
  183. begin
  184. if AId = 'import' then inc(importCount);
  185. exit(binarySearch(AId, forbidden) = -1);
  186. end;
  187. const StartIdentifier = ['A'..'Z','a'..'z','_'];
  188. const ContinueIdentifier = ['A'..'Z','a'..'z','_','0'..'9'];
  189. const WhiteSpace = [' ', #9];
  190. function importOk(const s: string; importCount: integer; previousBackslash: boolean): boolean;
  191. const ForbiddenModules: array[0..22] of string =
  192. ('builtins', // Provides direct access to all built-in identifiers of Python.
  193. 'code', // Facilities to implement interactive Python interpreters.
  194. 'codecs', // Core support for encoding and decoding text and binary data.
  195. 'ctypes', // Create and manipulate C-compatible data types in Python, and call functions in dynamic link libraries/shared libraries.
  196. 'ftplib', // Interface to the FTP protocol.
  197. 'gc', // Interface to the garbage collection facility for reference cycles.
  198. 'io', // Core tools for working with streams (core I/O operations).
  199. 'multiprocessing', // Process-based parallelism.
  200. 'os', // Interface to the operating system, including file and process operations.
  201. 'pathlib', // Object-oriented filesystem paths.
  202. 'poplib', // Client-side support for the POP3 protocol.
  203. 'pty', // Operations for handling the pseudo-terminal concept.
  204. 'runpy', // Locating and running Python programs using various modes of the `__main__` module.
  205. 'shutil', // High-level file operations, including copying and deletion.
  206. 'smtplib', // Client-side objects for the SMTP and ESMTP protocols.
  207. 'socket', // Low-level networking operations.
  208. 'subprocess', // Spawn additional processes, connect to their input/output/error pipes, and obtain their return codes.
  209. 'sys', // Access and set variables used or maintained by the Python interpreter.
  210. 'telnetlib', // Client-side support for the Telnet protocol.
  211. 'tempfile', // Generate temporary files and directories.
  212. 'threading', // Higher-level threading interfaces on top of the lower-level `_thread` module.
  213. 'wsgiref', // WSGI utility functions and reference implementation.
  214. 'xmlrpc' // XML-RPC server and client modules.
  215. );
  216. const SafeModules: array[0..26] of string =
  217. ('PIL', // Python Imaging Library, for image processing.
  218. 'array', // Basic mutable array operations.
  219. 'ast', // Abstract Syntax Trees
  220. 'bisect', // Algorithms for manipulating sorted lists.
  221. 'calendar', // Functions for working with calendars and dates.
  222. 'collections', // Container datatypes like namedtuples and defaultdict.
  223. 'colorsys', // Color system conversions.
  224. 'copy', // Shallow and deep copy operations.
  225. 'csv', // Reading and writing CSV files.
  226. 'datetime', // Basic date and time types.
  227. 'decimal', // Fixed and floating point arithmetic using decimal notation.
  228. 'enum', // Enumerations in Python.
  229. 'fractions', // Rational numbers.
  230. 'functools', // Higher-order functions and operations on callable objects.
  231. 'hashlib', // Secure hash and message digest algorithms.
  232. 'itertools', // Functions for creating iterators for efficient looping.
  233. 'json', // Encoding and decoding JSON format.
  234. 'lazpaint',
  235. 'math', // Mathematical functions.
  236. 'platform', // Access to platform-specific attributes and functions.
  237. 'queue', // A multi-producer, multi-consumer queue.
  238. 'random', // Generate pseudo-random numbers.
  239. 'statistics', // Mathematical statistics functions.
  240. 'string', // Common string operations.
  241. 'time', // Time-related functions.
  242. 'tkinter', // Standard GUI library for Python.
  243. 'uuid'); // UUID objects
  244. procedure SkipSpaces(var idx: integer);
  245. begin
  246. while (idx <= length(s)) and (s[idx] in WhiteSpace) do inc(idx);
  247. end;
  248. function GetId(var idx: integer): string;
  249. var idxEnd: integer;
  250. begin
  251. if (idx > length(s)) or not (s[idx] in StartIdentifier) then exit('');
  252. idxEnd := idx+1;
  253. while (idxEnd <= length(s)) and (s[idxEnd] in ContinueIdentifier) do inc(idxEnd);
  254. result := copy(s, idx, idxEnd-idx);
  255. idx := idxEnd;
  256. end;
  257. function SkipAs(var idx: integer): boolean;
  258. var
  259. subId: String;
  260. begin
  261. SkipSpaces(idx);
  262. if (idx > length(s)) or (s[idx] = '#') then exit(true);
  263. subId := GetId(idx);
  264. if subId = 'as' then
  265. begin
  266. SkipSpaces(idx);
  267. subId := GetId(idx);
  268. if subId = '' then exit(false); // syntax error
  269. end;
  270. exit(true);
  271. end;
  272. function ParseModuleName(var idx: integer; out AModuleName: string; out AIsSafe: boolean): boolean;
  273. var
  274. subId: String;
  275. begin
  276. SkipSpaces(idx);
  277. AIsSafe := false;
  278. AModuleName := GetId(idx);
  279. if AModuleName = '' then exit(false); // syntax error
  280. // check if module is allowed
  281. if binarySearch(AModuleName, ForbiddenModules) <> -1 then exit(false);
  282. AIsSafe := binarySearch(AModuleName, SafeModules) <> -1;
  283. SkipSpaces(idx);
  284. // submodule
  285. while (idx <= length(s)) and (s[idx] = '.') do
  286. begin
  287. inc(idx);
  288. SkipSpaces(idx);
  289. subId := GetId(idx);
  290. if subId = '' then exit(false); // syntax error
  291. AModuleName += '.' + subId;
  292. SkipSpaces(idx);
  293. end;
  294. exit(true);
  295. end;
  296. procedure AddModule(AModuleName: string; AIsSafe: boolean);
  297. begin
  298. if not AIsSafe then
  299. begin
  300. if AUnsafeModules = nil then
  301. AUnsafeModules := TStringList.Create;
  302. if AUnsafeModules.IndexOf(AModuleName) = -1 then
  303. AUnsafeModules.Add(AModuleName);
  304. end else
  305. begin
  306. if ASafeModules = nil then
  307. ASafeModules := TStringList.Create;
  308. if ASafeModules.IndexOf(AModuleName) = -1 then
  309. ASafeModules.Add(AModuleName);
  310. end;
  311. end;
  312. var idx: integer;
  313. fromClause: boolean;
  314. moduleName, subId: string;
  315. isSafe: boolean;
  316. begin
  317. if importCount <> 1 then exit(false); // syntax error
  318. if s.StartsWith('from ') then
  319. begin
  320. idx := length('from ') + 1;
  321. fromClause := true;
  322. end else
  323. if s.StartsWith('import ') then
  324. begin
  325. if previousBackslash then exit(false); // could be an exploit
  326. idx := length('import ') + 1;
  327. fromClause := false;
  328. end
  329. else
  330. exit(false); // syntax error
  331. if not ParseModuleName(idx, moduleName, isSafe) then exit(false);
  332. if fromClause then
  333. begin
  334. subId := GetId(idx);
  335. if subId <> 'import' then exit(false); // syntax error
  336. repeat
  337. SkipSpaces(idx);
  338. subId := GetId(idx);
  339. if subId = '' then exit(false); // syntax error
  340. AddModule(moduleName+'.'+subId, isSafe);
  341. if not SkipAs(idx) then exit(false);
  342. SkipSpaces(idx);
  343. if (idx <= length(s)) and (s[idx] = ',') then inc(idx)
  344. else break;
  345. until false;
  346. end else
  347. begin
  348. repeat
  349. AddModule(moduleName, isSafe);
  350. if not SkipAs(idx) then exit(false);
  351. SkipSpaces(idx);
  352. if (idx <= length(s)) and (s[idx] = ',') then
  353. begin
  354. inc(idx);
  355. if not ParseModuleName(idx, moduleName, isSafe) then exit(false);
  356. end
  357. else break;
  358. until false;
  359. end;
  360. if (idx <= length(s)) and (s[idx] <> '#') then // expect end of line
  361. exit(false); // syntax error
  362. exit(true);
  363. end;
  364. function lineOk(const s: string; previousBackslash: boolean): boolean;
  365. var
  366. startId, i: integer;
  367. importCount: integer;
  368. begin
  369. startId := -1;
  370. importCount := 0;
  371. for i := 1 to length(s) do
  372. begin
  373. // check identifier boundaries
  374. if (startId = -1) and (s[i] in StartIdentifier) then
  375. begin
  376. startId := i;
  377. end else
  378. if (startId <> -1) and not (s[i] in ContinueIdentifier) then
  379. begin
  380. if not idOk(copy(s, startId, i-startId), importCount) then exit(false);
  381. startId := -1;
  382. end;
  383. end;
  384. if (startId <> -1) and not idOk(copy(s, startId, length(s)-startId+1), importCount) then
  385. exit(false);
  386. if (importCount > 0) and not importOk(s, importCount, previousBackslash) then exit(false);
  387. exit(true);
  388. end;
  389. var
  390. t: textfile;
  391. s: string;
  392. previousBackslash: boolean;
  393. begin
  394. ASafeModules := nil;
  395. AUnsafeModules := nil;
  396. assignFile(t, AFilename);
  397. reset(t);
  398. previousBackslash := false;
  399. while not eof(t) do
  400. begin
  401. readln(t, s);
  402. s := trim(s);
  403. if not lineOk(s, previousBackslash) then exit(false);
  404. previousBackslash := s.EndsWith('\');
  405. end;
  406. closefile(t);
  407. exit(true);
  408. end;
  409. { TPythonScript }
  410. procedure TPythonScript.PythonOutput(ALine: RawByteString);
  411. var
  412. idxParam, cmdPos: SizeInt;
  413. command, param, finalLine: RawByteString;
  414. commandRes: UTF8String;
  415. i, curDisplayPos, maxDisplayLen: Integer;
  416. displayedLine: RawByteString;
  417. begin
  418. if FFirstOutput then
  419. begin
  420. if ALine <> 'LazPaint script'#9 then
  421. raise exception.Create(rsNotLazPaintScript)
  422. else
  423. begin
  424. FFirstOutput:= false;
  425. if Assigned(FPythonSend) then
  426. FPythonSend(chr(27)+'LazPaint')
  427. else
  428. raise exception.Create('"Send" callback not defined');
  429. end;
  430. end;
  431. cmdPos := pos(#27, ALine);
  432. if (cmdPos > 0) then
  433. begin
  434. FLinePrefix += copy(ALine, 1, cmdPos-1);
  435. delete(ALine, 1, cmdPos-1);
  436. idxParam := Pos(#29, ALine);
  437. param := '';
  438. if idxParam = 0 then
  439. command := copy(ALine,2,length(ALine)-1)
  440. else
  441. begin
  442. command := copy(ALine,2,idxParam-2);
  443. param := copy(ALine,idxParam+1,length(ALine)-(idxParam+1)+1);
  444. end;
  445. if command<>'' then
  446. begin
  447. if command[length(command)] = '?' then
  448. begin
  449. delete(command, length(command), 1);
  450. if Assigned(FOnCommand) then
  451. FOnCommand(self, command, param, commandRes)
  452. else
  453. commandRes := '';
  454. if Assigned(FPythonSend) then
  455. FPythonSend(commandRes);
  456. end else
  457. begin
  458. if Assigned(FOnCommand) then
  459. FOnCommand(self, command, param, commandRes);
  460. end;
  461. end;
  462. end else
  463. begin
  464. if Assigned(FOnOutputLine) then
  465. begin
  466. finalLine := FLinePrefix+ALine;
  467. displayedLine := '';
  468. setlength(displayedLine, 80);
  469. curDisplayPos := 1;
  470. maxDisplayLen := 0;
  471. for i := 1 to length(finalLine) do
  472. begin
  473. if finalLine[i] = #13 then curDisplayPos := 1 else
  474. if finalLine[i] = #8 then
  475. begin
  476. if curDisplayPos > 1 then dec(curDisplayPos);
  477. end else
  478. begin
  479. if curDisplayPos > length(displayedLine) then
  480. setlength(displayedLine, length(displayedLine)*2);
  481. displayedLine[curDisplayPos] := finalLine[i];
  482. if curDisplayPos > maxDisplayLen then
  483. maxDisplayLen := curDisplayPos;
  484. inc(curDisplayPos);
  485. end;
  486. end;
  487. setlength(displayedLine, maxDisplayLen);
  488. FOnOutputLine(self, displayedLine);
  489. end;
  490. FLinePrefix := '';
  491. end;
  492. end;
  493. procedure TPythonScript.PythonBusy(var ASleep: boolean);
  494. begin
  495. if Assigned(FOnBusy) then FOnBusy(self);
  496. end;
  497. function TPythonScript.CheckScriptAndDependencySafe(AFilename: UTF8String; APythonVersion: integer): boolean;
  498. var
  499. filesToCheck: TStringList;
  500. procedure AddModuleToCheck(AModuleName: UTF8String; ABasePath: UTF8String);
  501. var fullPath, moduleFilename: string;
  502. begin
  503. fullPath := ConcatPaths([ABasePath, StringReplace(AModuleName, '.', PathDelim, [rfReplaceAll])]);
  504. moduleFilename := fullPath+'.py';
  505. if (filesToCheck.IndexOf(moduleFilename) = -1) and FileExists(moduleFilename) then
  506. filesToCheck.Add(moduleFilename) else
  507. begin
  508. moduleFilename := fullPath+'\__init__.py';
  509. if (filesToCheck.IndexOf(moduleFilename) = -1) and FileExists(moduleFilename) then
  510. filesToCheck.Add(moduleFilename);
  511. end;
  512. end;
  513. var
  514. safeModules, unsafeModules, allUnsafeModules: TStringList;
  515. proceed: boolean;
  516. curFile, i: integer;
  517. curPath: string;
  518. begin
  519. allUnsafeModules := TStringList.Create;
  520. allUnsafeModules.Sorted := true;
  521. allUnsafeModules.Duplicates:= dupIgnore;
  522. filesToCheck := TStringList.Create;
  523. filesToCheck.Add(AFilename);
  524. curFile := 0;
  525. curPath := ExtractFilePath(AFilename);
  526. while curFile < filesToCheck.Count do
  527. begin
  528. if not CheckPythonScriptSafe(filesToCheck[curFile], safeModules, unsafeModules) then
  529. begin
  530. safeModules.Free;
  531. unsafeModules.Free;
  532. raise exception.Create(StringReplace(rsScriptNotSafe, '%1', filesToCheck[curFile], []));
  533. end;
  534. if Assigned(unsafeModules) then
  535. begin
  536. for i := 0 to unsafeModules.Count-1 do
  537. begin
  538. AddModuleToCheck(unsafeModules[i], curPath);
  539. allUnsafeModules.Add(unsafeModules[i]);
  540. end;
  541. end;
  542. if Assigned(safeModules) then
  543. begin
  544. for i := 0 to safeModules.Count-1 do
  545. AddModuleToCheck(safeModules[i], curPath);
  546. end;
  547. safeModules.Free;
  548. unsafeModules.Free;
  549. inc(curFile);
  550. end;
  551. filesToCheck.Free;
  552. if allUnsafeModules.Count > 0 then
  553. begin
  554. proceed := true;
  555. if Assigned(OnWarning) then
  556. begin
  557. OnWarning(self,
  558. StringReplace(rsSureToRunUnsafeScript, '%1',
  559. allUnsafeModules.CommaText, []),
  560. proceed);
  561. end;
  562. allUnsafeModules.Free;
  563. if not proceed then exit(false);
  564. end else
  565. allUnsafeModules.Free;
  566. if PythonVersionMajor <> APythonVersion then
  567. raise exception.Create(
  568. StringReplace( StringReplace(rsPythonUnexpectedVersion,
  569. '%1',inttostr(APythonVersion),[]),
  570. '%2',inttostr(PythonVersionMajor),[]) + #9 + rsDownload + #9 + 'https://www.python.org');
  571. exit(true);
  572. end;
  573. constructor TPythonScript.Create(APythonBin: string);
  574. begin
  575. FPythonBin := APythonBin;
  576. {$IFDEF DARWIN}
  577. if (FPythonBin = 'python3') and FileExists(UserPythonBin) then
  578. FPythonBin:= UserPythonBin;
  579. {$ENDIF}
  580. FPythonVersion:= GetPythonVersion(FPythonBin);
  581. end;
  582. procedure TPythonScript.PythonError(ALine: RawByteString);
  583. begin
  584. if Assigned(FOnError) then
  585. FOnError(self, ALine)
  586. else
  587. FErrorText += ALine+LineEnding;
  588. end;
  589. function TPythonScript.GetPythonVersionMajor: integer;
  590. var
  591. posDot: SizeInt;
  592. {%H-}errPos: integer;
  593. begin
  594. posDot := pos('.',PythonVersion);
  595. if posDot = 0 then
  596. result := 0
  597. else
  598. val(copy(PythonVersion,1,posDot-1), result, errPos);
  599. end;
  600. function TPythonScript.Run(AScriptFilename: UTF8String;
  601. APythonVersion: integer): boolean;
  602. var exitCode: integer;
  603. begin
  604. result := false;
  605. if CheckScriptSecure and
  606. not CheckScriptAndDependencySafe(AScriptFilename, APythonVersion) then exit;
  607. FLinePrefix := '';
  608. FFirstOutput:= true;
  609. AutomationEnvironment.Values['PYTHONPATH'] := DefaultScriptDirectory;
  610. AutomationEnvironment.Values['PYTHONIOENCODING'] := 'utf-8';
  611. try
  612. exitCode := RunProcessAutomation(FPythonBin, ['-u', AScriptFilename], FPythonSend, @PythonOutput, @PythonError, @PythonBusy);
  613. finally
  614. AutomationEnvironment.Clear;
  615. end;
  616. FPythonSend := nil;
  617. result := exitCode = 0;
  618. end;
  619. class function TPythonScript.DefaultScriptDirectory: string;
  620. begin
  621. if CustomScriptDirectory<>'' then
  622. result := CustomScriptDirectory
  623. else
  624. result := GetResourcePath('scripts');
  625. end;
  626. end.