dosbox_wrapper.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. {$MODE objfpc}{$H+}
  2. uses
  3. SysUtils, StrUtils,
  4. {$ifdef UseSignals}
  5. signals,
  6. {$endif def UseSignals}
  7. testu, classes,
  8. Process;
  9. const
  10. use_temp_dir : boolean = true;
  11. need_cwsdpmi : boolean = false;
  12. cwsdpmi_file : string = '';
  13. hide_execution : boolean = true;
  14. do_exit : boolean = true;
  15. verbose : boolean = false;
  16. DosBoxProcess: TProcess = nil;
  17. dosbox_timeout : integer = 400; { default timeout in seconds }
  18. DosBoxExitStatus : integer = -1;
  19. var
  20. OutputFileName : String;
  21. SourceFileName : String;
  22. StartDir, DosBoxDir: string;
  23. ExitCode: Integer = 255;
  24. DosBoxBinaryPath: string;
  25. TmpFileList : TStringList;
  26. function GenerateTempDir: string;
  27. var
  28. FileName: string;
  29. TempDir: string;
  30. Done: Boolean = False;
  31. begin
  32. TempDir := GetTempDir(False);
  33. repeat
  34. try
  35. FileName := TempDir + 'dosboxwrappertmp_' + IntToStr(Random(100000));
  36. if verbose then
  37. writeln('Trying to create directory ',Filename);
  38. MkDir(FileName);
  39. Done := True;
  40. except
  41. on E: EInOutError do
  42. begin
  43. { 5 = Access Denied, returned when a file is duplicated }
  44. if E.ErrorCode <> 5 then
  45. begin
  46. Writeln('Directory creation failed');
  47. raise;
  48. end;
  49. end;
  50. end;
  51. until Done;
  52. Result := FileName + DirectorySeparator;
  53. end;
  54. procedure GenerateDosBoxConf(const ADosBoxDir: string);
  55. var
  56. SourceConfFileName, TargetConfFileName: string;
  57. SourceFile, TargetFile: TextFile;
  58. OrigS, S: string;
  59. begin
  60. SourceConfFileName := ExtractFilePath(ParamStr(0)) + 'dosbox.conf';
  61. TargetConfFileName := ADosBoxDir + 'dosbox.conf';
  62. OutputFileName := ADosBoxDir + 'dosbox.out';
  63. if verbose then
  64. Writeln('Using target dosbox.conf ',TargetConfFileName);
  65. AssignFile(SourceFile, SourceConfFileName);
  66. AssignFile(TargetFile, TargetConfFileName);
  67. Reset(SourceFile);
  68. try
  69. Rewrite(TargetFile);
  70. try
  71. while not EoF(SourceFile) do
  72. begin
  73. Readln(SourceFile, S);
  74. OrigS:=S;
  75. S := AnsiReplaceStr(S, '$DosBoxDir', ADosBoxDir);
  76. S := AnsiReplaceStr(S, '$wrapper_output', OutputFileName);
  77. if do_exit then
  78. S := AnsiReplaceStr(S, '$exit', 'exit')
  79. else
  80. S := AnsiReplaceStr(S, '$exit', '');
  81. If verbose and (OrigS <> S) then
  82. Writeln('"',OrigS,'" transformed into "',S,'"');
  83. Writeln(TargetFile, S);
  84. end;
  85. finally
  86. CloseFile(TargetFile);
  87. end;
  88. finally
  89. CloseFile(SourceFile);
  90. end;
  91. end;
  92. { File names in Config entries assume that
  93. executables have no suffix }
  94. function TargetFileExists(AName : string) : boolean;
  95. begin
  96. result:=SysUtils.FileExists(AName);
  97. if not result then
  98. result:=SysUtils.FileExists(AName+'.exe');
  99. if not result then
  100. result:=SysUtils.FileExists(AName+'.EXE');
  101. end;
  102. procedure CopyFile(ASrcFileName, ADestFileName: string);
  103. var
  104. SrcF, DestF: File;
  105. OldFileMode: Integer;
  106. Buf: array [0..4095] of Byte;
  107. BytesRead: Integer;
  108. begin
  109. if not AnsiEndsText('.exe', ASrcFileName) and AnsiEndsText('.EXE',ADestFileName) then
  110. ASrcFileName := ASrcFileName + '.exe';
  111. if not FileExists(ASrcFileName) then
  112. begin
  113. ASrcFileName:=ASrcFileName+'.exe';
  114. ADestFileName:=ADestFileName+'.exe';
  115. end;
  116. if verbose then
  117. Writeln('CopyFile "', ASrcFileName, '" -> "', ADestFileName,'"');
  118. OldFileMode := FileMode;
  119. try
  120. try
  121. AssignFile(SrcF, ASrcFileName);
  122. AssignFile(DestF, ADestFileName);
  123. FileMode := fmOpenRead;
  124. Reset(SrcF, 1);
  125. try
  126. FileMode := fmOpenWrite;
  127. try
  128. Rewrite(DestF, 1);
  129. repeat
  130. BlockRead(SrcF, Buf, SizeOf(Buf), BytesRead);
  131. BlockWrite(DestF, Buf, BytesRead);
  132. until BytesRead < SizeOf(Buf);
  133. finally
  134. CloseFile(DestF);
  135. end;
  136. finally
  137. CloseFile(SrcF);
  138. end;
  139. finally
  140. FileMode := OldFileMode;
  141. end;
  142. except
  143. on E : Exception do
  144. writeln('Error: '+ E.ClassName + #13#10 + E.Message );
  145. end;
  146. end;
  147. function ForceExtension(Const HStr,ext:String):String;
  148. {
  149. Return a filename which certainly has the extension ext
  150. }
  151. var
  152. j : longint;
  153. begin
  154. j:=length(Hstr);
  155. while (j>0) and (Hstr[j]<>'.') do
  156. dec(j);
  157. if j=0 then
  158. j:=length(Hstr)+1;
  159. if Ext<>'' then
  160. begin
  161. if Ext[1]='.' then
  162. ForceExtension:=Copy(Hstr,1,j-1)+Ext
  163. else
  164. ForceExtension:=Copy(Hstr,1,j-1)+'.'+Ext
  165. end
  166. else
  167. ForceExtension:=Copy(Hstr,1,j-1);
  168. end;
  169. procedure CopyNeededFiles;
  170. var
  171. Config : TConfig;
  172. LocalFile, RemoteFile, s: string;
  173. LocalPath: string;
  174. i : integer;
  175. FileList : TStringList;
  176. RelativeToConfigMarker : TObject;
  177. function SplitPath(const s:string):string;
  178. var
  179. i : longint;
  180. begin
  181. i:=Length(s);
  182. while (i>0) and not(s[i] in ['/','\'{$IFDEF MACOS},':'{$ENDIF}]) do
  183. dec(i);
  184. SplitPath:=Copy(s,1,i);
  185. end;
  186. function BuildFileList: TStringList;
  187. var
  188. dfl, fl : string;
  189. begin
  190. fl:=Trim(Config.Files);
  191. dfl:=Trim(Config.DelFiles);
  192. if (fl='') and (dfl='') and (Config.ConfigFileSrc='') then
  193. begin
  194. Result:=nil;
  195. exit;
  196. end;
  197. Result:=TStringList.Create;
  198. while fl<>'' do
  199. begin
  200. LocalFile:=Trim(GetToken(fl, [' ',',',';']));
  201. Result.Add(LocalFile);
  202. if verbose then
  203. writeln('Adding file ',LocalFile,' from Config.Files');
  204. end;
  205. if Config.ConfigFileSrc<>'' then
  206. begin
  207. if Config.ConfigFileSrc=Config.ConfigFileDst then
  208. Result.AddObject(Config.ConfigFileSrc,RelativeToConfigMarker)
  209. else
  210. Result.AddObject(Config.ConfigFileSrc+'='+Config.ConfigFileDst,RelativeToConfigMarker);
  211. if verbose then
  212. writeln('Adding config file Src=',Config.ConfigFileSrc,' Dst=',Config.ConfigFileDst);
  213. end;
  214. while dfl <> '' do
  215. begin
  216. LocalFile:=Trim(GetToken(dfl, [' ',',',';']));
  217. Result.Add(LocalFile);
  218. if verbose then
  219. writeln('Adding file ',LocalFile,' from Config.DelFiles');
  220. end;
  221. end;
  222. var
  223. ddir : string;
  224. param1_dir : string;
  225. begin
  226. param1_dir:=ExtractFilePath(ParamStr(1));
  227. if not IsAbsolute(SourceFileName) and not TargetFileExists(SourceFileName) then
  228. begin
  229. ddir:=GetEnvironmentVariable('BASEDIR');
  230. if ddir='' then
  231. GetDir(0,ddir);
  232. // writeln('Start ddir=',ddir);
  233. while (ddir<>'') do
  234. begin
  235. if TargetFileExists(ddir+DirectorySeparator+SourceFileName) then
  236. begin
  237. SourceFileName:=ddir+DirectorySeparator+SourceFileName;
  238. break;
  239. end
  240. else
  241. begin
  242. if ddir=splitpath(ddir) then
  243. break
  244. else
  245. ddir:=splitpath(ddir);
  246. if ddir[length(ddir)]=DirectorySeparator then
  247. ddir:=copy(ddir,1,length(ddir)-1);
  248. // writeln('Next ddir=',ddir);
  249. end;
  250. end;
  251. end;
  252. if not TargetFileExists(SourceFileName) then
  253. begin
  254. writeln('File ',SourceFileName,' not found');
  255. exit;
  256. end
  257. else if verbose then
  258. writeln('Analyzing source file ',SourceFileName);
  259. if not GetConfig(SourceFileName,config) then
  260. exit;
  261. RelativeToConfigMarker:=TObject.Create;
  262. FileList:=BuildFileList;
  263. TmpFileList:=TStringList.Create;
  264. if assigned(FileList) then
  265. begin
  266. LocalPath:=SplitPath(SourceFileName);
  267. if (Length(LocalPath) > 0) and (LocalPath[Length(LocalPath)]<>DirectorySeparator) then
  268. LocalPath:=LocalPath+DirectorySeparator;
  269. for i:=0 to FileList.count-1 do
  270. begin
  271. if FileList.Names[i]<>'' then
  272. begin
  273. LocalFile:=FileList.Names[i];
  274. RemoteFile:=FileList.ValueFromIndex[i];
  275. end
  276. else
  277. begin
  278. LocalFile:=FileList[i];
  279. RemoteFile:=LocalFile;
  280. end;
  281. if FileList.Objects[i]=RelativeToConfigMarker then
  282. s:='config/'+LocalFile
  283. else
  284. s:=LocalPath+LocalFile;
  285. if not TargetFileExists(s) then
  286. if TargetFileExists(param1_dir+DirectorySeparator+LocalFile) then
  287. s:=param1_dir+DirectorySeparator+LocalFile;
  288. CopyFile(s,DosBoxDir+RemoteFile);
  289. TmpFileList.Add(RemoteFile);
  290. end;
  291. FileList.Free;
  292. end;
  293. RelativeToConfigMarker.Free;
  294. end;
  295. { On modified dosbox executable it is possible to get
  296. a copy of all output to CON into a file, simply write it
  297. back to output, so it ends up into testname.elg file.
  298. Skip all until line beginning with 'Drive C is mounted as' }
  299. procedure EchoOutput;
  300. const
  301. SkipUntilText = 'Drive C is mounted as ';
  302. var
  303. StdText : TextFile;
  304. st : string;
  305. line : longint;
  306. SkipUntilSeen : boolean;
  307. begin
  308. if FileExists(OutputFileName) then
  309. begin
  310. if verbose then
  311. Writeln('Trying to open ',OutputFileName);
  312. try
  313. AssignFile(StdText, OutputFileName);
  314. Reset(StdText);
  315. if verbose then
  316. Writeln('Successfully opened ',OutputFileName,', copying content to output');
  317. try
  318. line:=0;
  319. SkipUntilSeen:=false;
  320. while not eof(StdText) do
  321. begin
  322. Readln(StdText,st);
  323. inc(line);
  324. if not SkipUntilSeen then
  325. SkipUntilSeen:=pos(SkipUntilText,st)>0;
  326. if SkipUntilSeen then
  327. Writeln(line,': ',st);
  328. end;
  329. finally
  330. if not SkipUntilSeen then
  331. Writeln('Could not find "',SkipUntilText,'" in file ',OutputFilename);
  332. Flush(output);
  333. CloseFile(StdText);
  334. end;
  335. finally
  336. if use_temp_dir then
  337. DeleteFile(OutputFileName);
  338. end;
  339. end;
  340. end;
  341. function ReadExitCode(const ADosBoxDir: string): Integer;
  342. var
  343. F: TextFile;
  344. begin
  345. AssignFile(F, ADosBoxDir + 'EXITCODE.TXT');
  346. try
  347. Reset(F);
  348. Readln(F, Result);
  349. if Result <> 0 then
  350. Writeln('ExitCode=',Result);
  351. CloseFile(F);
  352. except
  353. Writeln('Unable to read exitcode value');
  354. if (DosBoxExitStatus <> 0) then
  355. Writeln('DosBox exit status = ',DosBoxExitStatus);
  356. ReadExitCode:=127*256;
  357. end;
  358. end;
  359. function ExecuteDosBox(const ADosBoxBinaryPath, ADosBoxDir: string) : Integer;
  360. var
  361. Time: Integer = 0;
  362. begin
  363. DosBoxProcess := TProcess.Create(nil);
  364. result:=-1;
  365. try
  366. DosBoxProcess.Executable := ADosBoxBinaryPath;
  367. DosBoxProcess.Parameters.Add('-conf');
  368. DosBoxProcess.Parameters.Add(ADosBoxDir + 'dosbox.conf');
  369. if hide_execution then
  370. DosBoxProcess.ShowWindow := swoHIDE;
  371. DosBoxProcess.Execute;
  372. repeat
  373. Inc(Time);
  374. if (Time > 10*dosbox_timeout) and do_exit then
  375. break;
  376. Sleep(100);
  377. until not DosBoxProcess.Running;
  378. if DosBoxProcess.Running then
  379. begin
  380. Writeln('Timeout exceeded. Killing dosbox...');
  381. DosBoxProcess.Terminate(254);
  382. Sleep(100);
  383. end;
  384. finally
  385. result:=DosBoxProcess.ExitStatus;
  386. DosBoxProcess.Free;
  387. DosBoxProcess:=nil;
  388. EchoOutput;
  389. end;
  390. end;
  391. function DeleteIfExists(const AFileName: string) : boolean;
  392. begin
  393. result:=false;
  394. if FileExists(AFileName) then
  395. result:=DeleteFile(AFileName);
  396. if not result and FileExists(AFileName+'.exe') then
  397. result:=DeleteFile(AFileName+'.exe');
  398. if not result and FileExists(AFileName+'.EXE') then
  399. result:=DeleteFile(AFileName+'.EXE');
  400. end;
  401. { RemoveDir, with removal of files or subdirectories inside first.
  402. ADirName is supposed to finish with DirectorySeparator }
  403. function RemoveDir(const ADirName: string) : boolean;
  404. var
  405. Info : TSearchRec;
  406. begin
  407. Result:=true;
  408. If FindFirst (AdirName+'*',faAnyFile and faDirectory,Info)=0 then
  409. begin
  410. repeat
  411. with Info do
  412. begin
  413. If (Attr and faDirectory) = faDirectory then
  414. begin
  415. { Skip present and parent directory }
  416. if (Name<>'..') and (Name<>'.') then
  417. if not RemoveDir(ADirName+Name+DirectorySeparator) then
  418. begin
  419. writeln('Failed to remove dir '+ADirName+Name+DirectorySeparator);
  420. result:=false;
  421. FindClose(Info);
  422. exit;
  423. end;
  424. end
  425. else
  426. if not DeleteFile(ADirName+Name) then
  427. begin
  428. writeln('Failed to remove file '+ADirName+Name);
  429. result:=false;
  430. FindClose(Info);
  431. exit;
  432. end;
  433. end;
  434. Until FindNext(info)<>0;
  435. end;
  436. FindClose(Info);
  437. RemoveDir:=SysUtils.RemoveDir(ADirName);
  438. end;
  439. procedure Cleanup(const ADosBoxDir: string);
  440. var
  441. i : longint;
  442. begin
  443. DeleteIfExists(ADosBoxDir + 'dosbox.conf');
  444. DeleteIfExists(ADosBoxDir + 'EXITCODE.TXT');
  445. DeleteIfExists(ADosBoxDir + 'EXITCODE.EXE');
  446. DeleteIfExists(ADosBoxDir + 'CWSDPMI.EXE');
  447. DeleteIfExists(ADosBoxDir + 'TEST.EXE');
  448. if Assigned(TmpFileList) then
  449. begin
  450. for i:=0 to TmpFileList.count-1 do
  451. if TmpFileList[i]<>'' then
  452. DeleteIfExists(ADosBoxDir + TmpFileList[i]);
  453. end;
  454. TmpFileList.Free;
  455. ChDir(StartDir);
  456. if not RemoveDir(ADosBoxDir) then
  457. writeln('Failed to remove dir ',ADosBoxDir);
  458. end;
  459. {$ifdef UseSignals}
  460. const
  461. SignalCalled : boolean = false;
  462. SignalNb : longint = 0;
  463. function DosBoxSignal(signal:longint):longint; cdecl;
  464. begin
  465. SignalCalled:=true;
  466. SignalNb:=signal;
  467. end;
  468. {$endif def UseSignals}
  469. procedure ExitProc;
  470. var
  471. count : longint;
  472. begin
  473. if assigned(DosBoxProcess) and (DosBoxProcess.Running) then
  474. begin
  475. Writeln('In ExitProc. Killing dosbox...');
  476. DosBoxProcess.Terminate(254*1024);
  477. Sleep(100);
  478. count:=1;
  479. while (DosBoxProcess.Running) do
  480. begin
  481. Sleep(100);
  482. inc(count);
  483. if (count mod 20=0) then
  484. DosBoxProcess.Terminate(254*1024+count);
  485. end;
  486. if count>1 then
  487. Writeln('In ExitProc. Wait for termination dosbox..., time=',count/10);
  488. EchoOutput;
  489. end;
  490. end;
  491. begin
  492. Randomize;
  493. if GetEnvironmentVariable('DOSBOX_NO_TEMPDIR')<>'' then
  494. begin
  495. use_temp_dir:=false;
  496. Writeln('use_temp_dir set to false');
  497. end;
  498. if GetEnvironmentVariable('DOSBOX_NO_HIDE')<>'' then
  499. begin
  500. hide_execution:=false;
  501. Writeln('hide_execution set to false');
  502. end;
  503. if GetEnvironmentVariable('DOSBOX_NO_EXIT')<>'' then
  504. begin
  505. do_exit:=false;
  506. Writeln('do_exit set to false');
  507. end;
  508. if GetEnvironmentVariable('DOSBOX_VERBOSE')<>'' then
  509. begin
  510. verbose:=true;
  511. Writeln('verbose set to true');
  512. end;
  513. if (GetEnvironmentVariable('DOSBOX_NEEDS_CWSDPMI')<>'') or
  514. (GetEnvironmentVariable('TEST_OS_TARGET')='go32v2') then
  515. begin
  516. need_cwsdpmi:=true;
  517. Writeln('need_cwsdpmi set to true');
  518. end;
  519. if GetEnvironmentVariable('DOSBOX_TIMEOUT')<>'' then
  520. begin
  521. dosbox_timeout:=StrToInt(GetEnvironmentVariable('DOSBOX_TIMEOUT'));
  522. Writeln('dosbox_timeout set to ', dosbox_timeout, ' seconds');
  523. end;
  524. if ParamCount = 0 then
  525. begin
  526. Writeln('Usage: ' + ParamStr(0) + ' <executable> (-Ssourcename)');
  527. Writeln('Set DOSBOX_NO_TEMPDIR env variable to 1 to avoid using a temporary directory');
  528. Writeln('Set DOSBOX_NO_HIDE to avoid running dosbox in an hidden window');
  529. Writeln('Set DOSBOX_NO_EXIT to avoid exiting dosbox after test has been run');
  530. Writeln('Set DOSBOX_TIMEOUT to set the timeout in seconds before killing the dosbox process, assuming the test has hanged');
  531. halt(1);
  532. end;
  533. DosBoxBinaryPath := GetEnvironmentVariable('DOSBOX');
  534. if DosBoxBinaryPath = '' then
  535. begin
  536. Writeln('Please set the DOSBOX environment variable to the dosbox executable');
  537. halt(1);
  538. end
  539. else
  540. begin
  541. Writeln('Using DOSBOX executable: ',DosBoxBinaryPath);
  542. end;
  543. { DosBoxDir is used inside dosbox.conf as a MOUNT parameter }
  544. if use_temp_dir then
  545. begin
  546. GetDir(0,StartDir);
  547. DosBoxDir := GenerateTempDir;
  548. { All executable test have t.*.pp pattern }
  549. if (paramcount>1) and (copy(paramstr(2),1,2)='-S') then
  550. SourceFileName:=copy(paramstr(2),3,length(paramstr(2)))
  551. else
  552. SourceFileName:=ForceExtension(Paramstr(1),'.pp');
  553. CopyNeededFiles;
  554. end
  555. else
  556. begin
  557. Writeln('Using ',ParamStr(1));
  558. DosBoxDir:=ExtractFilePath(ParamStr(1));
  559. if DosBoxDir='' then
  560. DosBoxDir:=GetCurrentDir+DirectorySeparator;
  561. Writeln('Using DosBoxDir=',DosBoxDir);
  562. { Get rid of previous exicode.txt file }
  563. DeleteIfExists(DosBoxDir + 'EXITCODE.TXT');
  564. end;
  565. try
  566. {$ifdef UseSignals}
  567. Signal(SIGINT,@DosBoxSignal);
  568. Signal(SIGQUIT,@DosBoxSignal);
  569. Signal(SIGTERM,@DosBoxSignal);
  570. {$endif def UseSignals}
  571. GenerateDosBoxConf(DosBoxDir);
  572. CopyFile(ExtractFilePath(ParamStr(0)) + 'exitcode.exe', DosBoxDir + 'EXITCODE.EXE');
  573. CopyFile(ParamStr(1), DosBoxDir + 'TEST.EXE');
  574. if need_cwsdpmi then
  575. begin
  576. cwsdpmi_file:=FileSearch('cwsdpmi.exe',GetEnvironmentVariable('PATH'));
  577. if cwsdpmi_file<>'' then
  578. CopyFile(cwsdpmi_file, DosBoxDir + 'CWSDPMI.EXE')
  579. else if verbose then
  580. writeln('cwsdpmi executable missing');
  581. end;
  582. DosBoxExitStatus:=ExecuteDosBox(DosBoxBinaryPath, DosBoxDir);
  583. finally
  584. ExitProc;
  585. end;
  586. {$ifdef UseSignals}
  587. if SignalCalled then
  588. begin
  589. Writeln('Signal ',SignalNb,' called');
  590. end;
  591. {$endif def UseSignals}
  592. ExitProc;
  593. ExitCode:=ReadExitCode(DosBoxDir);
  594. if use_temp_dir then
  595. Cleanup(DosBoxDir);
  596. halt(ExitCode);
  597. end.