httpcompiler.pp 16 KB

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