2
0

httpcompiler.pp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. unit httpcompiler;
  2. {$mode objfpc}
  3. {$H+}
  4. interface
  5. uses
  6. sysutils, classes, fpjson, contnrs, syncobjs, fpmimetypes, custhttpapp,
  7. fpwebfile, httproute, httpdefs, dirwatch, Pas2JSFSCompiler, Pas2JSCompilerCfg;
  8. Const
  9. nErrTooManyThreads = -1;
  10. Type
  11. TDirWatcher = Class;
  12. THTTPCompilerApplication = Class;
  13. { TCompileItem }
  14. TCompileItem = Class(TCollectionItem)
  15. private
  16. FBaseDir: string;
  17. FConfigFile: String;
  18. FFileName: string;
  19. FOutput : TStrings;
  20. FOptions : TStrings;
  21. FSuccess: Boolean;
  22. FThread: TThread;
  23. function GetOptions: TStrings;
  24. function GetOutput: TStrings;
  25. Public
  26. Destructor Destroy; override;
  27. Property BaseDir : string Read FBaseDir Write FBaseDir;
  28. Property FileName : string Read FFileName Write FFileName;
  29. Property ConfigFile: String Read FConfigFile Write FConfigFile;
  30. Property Options : TStrings Read GetOptions;
  31. Property Output : TStrings Read GetOutput;
  32. Property Thread : TThread Read FThread;
  33. Property Success : Boolean Read FSuccess;
  34. end;
  35. { TCompiles }
  36. TCompiles = Class(TCollection)
  37. private
  38. function GetC(AIndex : Integer): TCompileItem;
  39. Public
  40. Property Compiles[AIndex : Integer] : TCompileItem Read GetC; default;
  41. end;
  42. { TCompileThread }
  43. TCompileThread = class(TThread)
  44. private
  45. FApp : THTTPCompilerApplication;
  46. FItem: TCompileItem;
  47. procedure DoCompilerLog(Sender: TObject; const Msg: String);
  48. procedure SetItem(AValue: TCompileItem);
  49. Public
  50. Constructor create(App : THTTPCompilerApplication; aItem : TCompileItem);
  51. Procedure Execute; override;
  52. Property Item : TCompileItem read FItem write SetItem;
  53. end;
  54. { TDirWatcher }
  55. TDirWatcher = Class(TComponent)
  56. Private
  57. FApp : THTTPCompilerApplication;
  58. FDW : TDirWatch;
  59. procedure DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents);
  60. Public
  61. Constructor Create(App : THTTPCompilerApplication; ADir : String);overload;
  62. Destructor Destroy; override;
  63. end;
  64. { THTTPCompilerApplication }
  65. THTTPCompilerApplication = Class(TCustomHTTPApplication)
  66. private
  67. FBaseDir: String;
  68. FConfigFile: String;
  69. FProjectFile: String;
  70. FStatusLock : TCriticalSection;
  71. FQuiet: Boolean;
  72. FWatch: Boolean;
  73. FDW : TDirWatcher;
  74. FStatusList : TFPObjectList;
  75. FCompiles : TCompiles;
  76. FServeOnly : Boolean;
  77. procedure AddToStatus(O: TJSONObject);
  78. function HandleCompileOptions(aDir: String): Boolean;
  79. function ProcessOptions: Boolean;
  80. Procedure ReportBuilding(AItem : TCompileItem);
  81. Procedure ReportBuilt(AItem : TCompileItem);
  82. Procedure AddToStatus(AEntry : TDirectoryEntry; AEvents : TFileEvents);
  83. procedure DoStatusRequest(ARequest: TRequest; AResponse: TResponse);
  84. procedure DoRecompile(ARequest: TRequest; AResponse: TResponse);
  85. function ScheduleCompile(const aProjectFile: String; Options : TStrings = Nil): Integer;
  86. procedure StartWatch(ADir: String);
  87. protected
  88. procedure Usage(Msg: String); virtual;
  89. function GetDefaultMimeTypesFile: string; virtual;
  90. procedure LoadDefaultMimeTypes; virtual;
  91. public
  92. Constructor Create(AOWner : TComponent); override;
  93. Destructor Destroy; override;
  94. procedure DoLog(EventType: TEventType; const Msg: String); override;
  95. Procedure DoRun; override;
  96. property Quiet : Boolean read FQuiet Write FQuiet;
  97. Property Watch : Boolean Read FWatch Write FWatch;
  98. Property ProjectFile : String Read FProjectFile Write FProjectFile;
  99. Property ConfigFile : String Read FConfigFile Write FConfigFile;
  100. Property BaseDir : String Read FBaseDir;
  101. Property ServeOnly : Boolean Read FServeOnly;
  102. end;
  103. Implementation
  104. { TCompileThread }
  105. procedure TCompileThread.SetItem(AValue: TCompileItem);
  106. begin
  107. if FItem=AValue then Exit;
  108. FItem:=AValue;
  109. end;
  110. procedure TCompileThread.DoCompilerLog(Sender: TObject; const Msg: String);
  111. begin
  112. If Assigned(Item) then
  113. Item.Output.Add(Msg);
  114. end;
  115. constructor TCompileThread.create(App: THTTPCompilerApplication; aItem: TCompileItem);
  116. begin
  117. FItem:=aItem;
  118. FApp:=App;
  119. FreeOnTerminate:=True;
  120. inherited create(False);
  121. end;
  122. procedure TCompileThread.Execute;
  123. Var
  124. C : TPas2JSFSCompiler;
  125. L : TStrings;
  126. begin
  127. L:=Nil;
  128. C:=TPas2JSFSCompiler.Create;
  129. Try
  130. C.ConfigSupport:=TPas2JSFileConfigSupport.Create(C);
  131. FApp.ReportBuilding(Item);
  132. L:=TStringList.Create;
  133. L.Assign(Item.Options);
  134. if (Item.ConfigFile<>'') then
  135. L.Add('@'+Item.ConfigFile);
  136. L.Add(Item.FileName);
  137. C.Log.OnLog:=@DoCompilerLog;
  138. try
  139. C.Run(ParamStr(0),Item.BaseDir,L,True);
  140. Item.FSuccess:=True;
  141. except
  142. On E : Exception do
  143. Item.Output.Add(Format('Error %s compiling %s: %s',[E.ClassName,Item.FileName,E.Message]));
  144. end;
  145. FApp.ReportBuilt(Item);
  146. Finally
  147. C.Free;
  148. L.Free;
  149. end;
  150. Item.FThread:=Nil;
  151. end;
  152. { TCompiles }
  153. function TCompiles.GetC(AIndex : Integer): TCompileItem;
  154. begin
  155. Result:=Items[Aindex] as TCompileItem;
  156. end;
  157. { TCompileItem }
  158. function TCompileItem.GetOutput: TStrings;
  159. begin
  160. If (FOutput=Nil) then
  161. FOutput:=TStringList.Create;
  162. Result:=FOutput;
  163. end;
  164. function TCompileItem.GetOptions: TStrings;
  165. begin
  166. If (FOptions=Nil) then
  167. FOptions:=TStringList.Create;
  168. Result:=FOptions;
  169. end;
  170. destructor TCompileItem.Destroy;
  171. begin
  172. FreeAndNil(FOutput);
  173. FreeAndNil(FOptions);
  174. inherited Destroy;
  175. end;
  176. { TDirWatcher }
  177. procedure TDirWatcher.DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents);
  178. begin
  179. if Assigned(FApp) then
  180. FApp.AddToStatus(AEntry,AEvents);
  181. end;
  182. constructor TDirWatcher.Create(App: THTTPCompilerApplication; ADir: String);
  183. begin
  184. Inherited create(APP);
  185. FApp:=App;
  186. FDW:=TDirwatch.Create(Self);
  187. FDW.AddWatch(ADir,allEvents);
  188. FDW.OnChange:=@DoChange;
  189. TThread.ExecuteInThread(@FDW.StartWatch);
  190. end;
  191. destructor TDirWatcher.Destroy;
  192. begin
  193. FApp:=Nil;
  194. FDW.Terminate;
  195. FreeAndNil(FDW);
  196. inherited Destroy;
  197. end;
  198. { THTTPCompilerApplication }
  199. procedure THTTPCompilerApplication.DoLog(EventType: TEventType; const Msg: String);
  200. begin
  201. {AllowWriteln}
  202. if Quiet then
  203. exit;
  204. if IsConsole then
  205. Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg)
  206. else
  207. inherited DoLog(EventType, Msg);
  208. {AllowWriteln-}
  209. end;
  210. procedure THTTPCompilerApplication.Usage(Msg : String);
  211. begin
  212. {AllowWriteln}
  213. if (Msg<>'') then
  214. Writeln('Error: ',Msg);
  215. Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] ');
  216. Writeln('Where options is one or more of : ');
  217. Writeln('-d --directory=dir Base directory from which to serve files.');
  218. Writeln(' Default is current working directory: ',GetCurrentDir);
  219. Writeln('-h --help This help text');
  220. Writeln('-i --indexpage=name Directory index page to use (default: index.html)');
  221. Writeln('-n --noindexpage Do not allow index page.');
  222. Writeln('-p --port=NNNN TCP/IP port to listen on (default is 3000)');
  223. Writeln('-q --quiet Do not write diagnostic messages');
  224. Writeln('-w --watch Watch directory for changes');
  225. Writeln('-c --compile[=proj] Recompile project if pascal files change. Default project is app.lpr');
  226. Writeln('-m --mimetypes=file filename of mimetypes. Default is ',GetDefaultMimeTypesFile);
  227. Writeln('-s --simpleserver Only serve files, do not enable compilation.');
  228. Halt(Ord(Msg<>''));
  229. {AllowWriteln-}
  230. end;
  231. function THTTPCompilerApplication.GetDefaultMimeTypesFile: string;
  232. begin
  233. {$ifdef unix}
  234. Result:='/etc/mime.types';
  235. {$ifdef darwin}
  236. if not FileExists(Result) then
  237. Result:='/private/etc/apache2/mime.types';
  238. {$endif}
  239. {$else}
  240. Result:=ExtractFilePath(System.ParamStr(0))+'mime.types';
  241. {$endif}
  242. end;
  243. procedure THTTPCompilerApplication.LoadDefaultMimeTypes;
  244. begin
  245. MimeTypes.LoadKnownTypes;
  246. // To be sure
  247. MimeTypes.AddType('application/xhtml+xml','xhtml;xht');
  248. MimeTypes.AddType('text/html','html;htm');
  249. MimeTypes.AddType('text/plain','txt');
  250. MimeTypes.AddType('application/javascript','js');
  251. MimeTypes.AddType('text/plain','map');
  252. MimeTypes.AddType('application/json','json');
  253. MimeTypes.AddType('image/png','png');
  254. MimeTypes.AddType('image/jpeg','jpeg;jpg');
  255. MimeTypes.AddType('image/gif','gif');
  256. MimeTypes.AddType('image/jp2','jp2');
  257. MimeTypes.AddType('image/tiff','tiff;tif');
  258. MimeTypes.AddType('application/pdf','pdf');
  259. MimeTypes.AddType('text/css','css');
  260. end;
  261. constructor THTTPCompilerApplication.Create(AOWner: TComponent);
  262. begin
  263. inherited Create(AOWner);
  264. FStatusLock:=TCriticalSection.Create;
  265. FStatusList:=TFPObjectList.Create(False);
  266. FCompiles:=TCompiles.Create(TCompileItem);
  267. end;
  268. destructor THTTPCompilerApplication.Destroy;
  269. begin
  270. FStatusLock.Enter;
  271. try
  272. FreeAndNil(FCompiles);
  273. FreeAndNil(FStatusList);
  274. finally
  275. FStatusLock.Leave;
  276. end;
  277. FreeAndNil(FStatusLock);
  278. inherited Destroy;
  279. end;
  280. procedure THTTPCompilerApplication.StartWatch(ADir : String);
  281. begin
  282. FDW:=TDirWatcher.Create(Self,ADir);
  283. end;
  284. procedure THTTPCompilerApplication.ReportBuilding(AItem: TCompileItem);
  285. Var
  286. O : TJSONObject;
  287. begin
  288. O:=TJSONObject.Create(['action','building','compileID',AItem.ID,'project',AItem.FileName,'config',AItem.ConfigFile]);
  289. AddToStatus(O);
  290. end;
  291. procedure THTTPCompilerApplication.ReportBuilt(AItem: TCompileItem);
  292. Var
  293. O : TJSONObject;
  294. A : TJSONArray;
  295. I : Integer;
  296. begin
  297. A:=TJSONArray.Create;
  298. For I:=0 to AItem.Output.Count-1 do
  299. A.Add(AItem.Output[i]);
  300. O:=TJSONObject.Create(['action','built','compileID',AItem.ID,'project',AItem.FileName,'config',AItem.ConfigFile,'output',A,'success',AItem.Success]);
  301. AddToStatus(O);
  302. end;
  303. procedure THTTPCompilerApplication.AddToStatus(O : TJSONObject);
  304. begin
  305. FStatusLock.Enter;
  306. try
  307. {$ifdef VerboseHTTPCompiler}
  308. Writeln('Adding to status ',Assigned(O),' : ',O.ClassName);
  309. {$endif}
  310. FStatusList.Add(O);
  311. finally
  312. FStatusLock.Leave;
  313. end;
  314. end;
  315. procedure THTTPCompilerApplication.AddToStatus(AEntry: TDirectoryEntry; AEvents: TFileEvents);
  316. Var
  317. O : TJSONObject;
  318. FN : String;
  319. begin
  320. Log(etDebug,'File change detected: %s (%s)',[AEntry.name,FileEventsToStr(AEvents)]);
  321. O:=TJSONObject.Create(['action','file','name',AEntry.name,'events',FileEventsToStr(AEvents)]);
  322. if Pos(ExtractFileExt(AEntry.Name),'.lpr.pas.pp.inc.dpr')>0 then
  323. FN:=AEntry.Name;
  324. if (FN<>'') then
  325. O.Add('recompile',true);
  326. AddToStatus(O);
  327. if (FN<>'') then
  328. begin
  329. Log(etDebug,'File change forces recompile: %s',[AEntry.name]);
  330. ScheduleCompile('',Nil);
  331. end;
  332. end;
  333. procedure THTTPCompilerApplication.DoStatusRequest(ARequest : TRequest; AResponse : TResponse);
  334. Var
  335. R,O : TJSONObject;
  336. A : TJSONArray;
  337. I : integer;
  338. begin
  339. Log(etDebug,'Status request from: %s',[ARequest.RemoteAddress]);
  340. R:=Nil;
  341. try
  342. FStatusLock.Enter;
  343. try
  344. if (FStatusList.Count=0) then
  345. R:=TJSONObject.Create(['ping',True])
  346. else
  347. begin
  348. {$ifdef VerboseHTTPCompiler}
  349. Writeln(FStatusList[0].ClassName);
  350. {$endif}
  351. O:=FStatusList[0] as TJSONObject;
  352. FStatusList.Delete(0);
  353. if O.Get('action','')<>'file' then
  354. R:=O
  355. else
  356. begin
  357. // If first event is file event, then add and delete all file events in list.
  358. A:=TJSONArray.Create([O]);
  359. O.Delete('action');
  360. R:=TJSONObject.Create(['action','sync','files',A]);
  361. For I:=FStatusList.Count-1 downto 0 do
  362. begin
  363. O:=FStatusList[I] as TJSONObject;
  364. if (O.Get('action','')='file') then
  365. begin
  366. A.Add(O);
  367. O.Delete('action');
  368. FStatusList.Delete(I);
  369. end;
  370. end;
  371. end
  372. end;
  373. finally
  374. FStatusLock.Leave;
  375. end;
  376. AResponse.ContentType:='application/json';
  377. AResponse.Content:=R.AsJSON;
  378. AResponse.SendResponse;
  379. finally
  380. R.Free;
  381. end;
  382. end;
  383. function THTTPCompilerApplication.ScheduleCompile(const aProjectFile: String;
  384. Options: TStrings): Integer;
  385. Var
  386. CI : TCompileItem;
  387. I,TC : Integer;
  388. begin
  389. TC:=0;
  390. For I:=0 to FCompiles.Count-1 do
  391. if Assigned(FCompiles[I].THread) then
  392. Inc(TC);
  393. if TC>10 then
  394. begin
  395. Log(etError,'Refusing compile of file "%s" using config file "%s"',[AProjectFile, ConfigFile]);
  396. Exit(nErrTooManyThreads);
  397. end;
  398. CI:=FCompiles.Add as TCompileItem;
  399. Log(etInfo,'Scheduling compile ID %d of file "%s" using config file "%s"',[CI.ID,AProjectFile, ConfigFile]);
  400. CI.BaseDir:=BaseDir;
  401. CI.FileName:=AProjectFile;
  402. CI.ConfigFile:=ConfigFile;
  403. if Assigned(Options) then
  404. CI.Options.Assign(Options);
  405. TCompileThread.Create(Self,CI);
  406. Result:=CI.ID;
  407. end;
  408. procedure THTTPCompilerApplication.DoRecompile(ARequest: TRequest; AResponse: TResponse);
  409. Var
  410. ID : Integer;
  411. PF,CL : String;
  412. Options: TStrings;
  413. begin
  414. PF:=ARequest.ContentFields.Values['ProjectFile'];
  415. CL:=ARequest.ContentFields.Values['CompileOptions'];
  416. if PF='' then
  417. PF:=ProjectFile;
  418. If (PF='') then
  419. begin
  420. AResponse.Code:=404;
  421. AResponse.CodeText:='No project file';
  422. AResponse.ContentType:='application/json';
  423. AResponse.Content:='{ "success" : false, "message": "no project file set or provided" }';
  424. end
  425. else
  426. begin
  427. Options:=Nil;
  428. try
  429. if CL<>'' then
  430. begin
  431. Options:=TStringList.Create;
  432. Options.Text:=Cl;
  433. end;
  434. ID:=ScheduleCompile(PF,Options);
  435. finally
  436. FreeAndNil(Options);
  437. end;
  438. if ID=nErrTooManyThreads then
  439. begin
  440. AResponse.Code:=403;
  441. AResponse.CodeText:='Too many compiles';
  442. AResponse.ContentType:='application/json';
  443. AResponse.Content:='{ "success" : false, "message": "Too many compiles running" }';
  444. end
  445. else
  446. begin
  447. AResponse.Code:=200;
  448. AResponse.ContentType:='application/json';
  449. AResponse.Content:=Format('{ "success" : true, "file": "%s", "commandLine" : "%s", "compileID": %d }',[StringToJSONString(PF),StringToJSONString(CL),ID]);
  450. end
  451. end;
  452. AResponse.SendResponse;
  453. end;
  454. function THTTPCompilerApplication.HandleCompileOptions(aDir: String): Boolean;
  455. begin
  456. Result:=False;
  457. Watch:=HasOption('w','watch');
  458. if Hasoption('P','project') then
  459. begin
  460. ProjectFile:=GetOptionValue('P','project');
  461. if ProjectFile='' then
  462. ProjectFile:=IncludeTrailingPathDelimiter(aDir)+'app.lpr';
  463. If Not FileExists(ProjectFile) then
  464. begin
  465. Terminate;
  466. Log(etError,'Project file "%s" does not exist, aborting.',[ProjectFile]);
  467. Exit;
  468. end;
  469. ConfigFile:=GetOptionValue('c','config');
  470. if (ConfigFile='') then
  471. ConfigFile:=ChangeFileExt(Projectfile,'.cfg');
  472. if not FileExists(ConfigFile) then
  473. ConfigFile:='';
  474. end;
  475. if Watch then
  476. begin
  477. if (ProjectFile='') then
  478. Log(etWarning,'No project file specified, disabling watch.') ;
  479. StartWatch(aDir);
  480. end;
  481. Result:=True;
  482. end;
  483. function THTTPCompilerApplication.ProcessOptions: Boolean;
  484. Var
  485. S,IndexPage,D : String;
  486. begin
  487. Result:=False;
  488. S:=Checkoptions('shqd:ni:p:wP::cm:',['help','quiet','noindexpage','directory:','port:','indexpage:','watch','project::','config:','simpleserver','mimetypes:']);
  489. if (S<>'') or HasOption('h','help') then
  490. usage(S);
  491. FServeOnly:=HasOption('s','serve-only');
  492. Quiet:=HasOption('q','quiet');
  493. Port:=StrToIntDef(GetOptionValue('p','port'),3000);
  494. D:=GetOptionValue('d','directory');
  495. if D='' then
  496. D:=GetCurrentDir;
  497. if HasOption('m','mimetypes') then
  498. MimeTypesFile:=GetOptionValue('m','mimetypes');
  499. if MimeTypesFile='' then
  500. begin
  501. MimeTypesFile:=GetDefaultMimeTypesFile;
  502. if not FileExists(MimeTypesFile) then
  503. begin
  504. MimeTypesFile:='';
  505. LoadDefaultMimeTypes;
  506. end;
  507. end
  508. else if not FileExists(MimeTypesFile) then
  509. Log(etWarning,'mimetypes file not found: '+MimeTypesFile);
  510. FBaseDir:=D;
  511. if not ServeOnly then
  512. if not HandleCompileOptions(D) then
  513. exit(False);
  514. TSimpleFileModule.BaseDir:=IncludeTrailingPathDelimiter(D);
  515. TSimpleFileModule.OnLog:=@Log;
  516. Log(etInfo,'Listening on port %d, serving files from directory: %s',[Port,D]);
  517. if ServeOnly then
  518. Log(etInfo,'Compile requests will be ignored.');
  519. If not HasOption('n','noindexpage') then
  520. begin
  521. IndexPage:=GetOptionValue('i','indexpage');
  522. if (IndexPage='') then
  523. IndexPage:='index.html';
  524. Log(etInfo,'Using index page %s',[IndexPage]);
  525. TSimpleFileModule.IndexPageName:=IndexPage;
  526. end;
  527. Result:=True;
  528. end;
  529. procedure THTTPCompilerApplication.DoRun;
  530. begin
  531. If not ProcessOptions then
  532. begin
  533. Terminate;
  534. exit;
  535. end;
  536. if not ServeOnly then
  537. begin
  538. httprouter.RegisterRoute('$sys/compile',rmPost,@DoRecompile);
  539. httprouter.RegisterRoute('$sys/status',rmGet,@DoStatusRequest);
  540. end;
  541. TSimpleFileModule.RegisterDefaultRoute;
  542. inherited;
  543. end;
  544. end.