httpcompiler.pp 15 KB

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