program server; {$IF FPC_FULLVERSION<30101} {$ERROR You need at least fpc 3.1.1} {$ENDIF} uses {$IFDEF UNIX}cthreads,{$ENDIF} sysutils, classes, fpjson, contnrs, syncobjs, custhttpapp, fpwebfile, httproute, dirwatch, httpdefs; Type TDirWatcher = Class; THTTPApplication = Class; { TCompileItem } TCompileItem = Class(TCollectionItem) private FCommandLine: string; FFileName: string; FOutput : TStrings; FThread: TThread; function GetOutput: TStrings; Public Property FileName : string Read FFileName Write FFileName; Property CommandLine : string Read FCommandLine Write FCommandLine; Property Output : TStrings Read GetOutput; Property Thread : TThread Read FThread; end; { TCompiles } TCompiles = Class(TCollection) private function GetC(AIndex : Integer): TCompileItem; Public Property Compiles[AIndex : Integer] : TCompileItem Read GetC; default; end; { TCompileThread } TCompileThread = class(TThread) private FItem: TCompileItem; procedure SetItem(AValue: TCompileItem); Public Constructor create(aItem : TCompileItem); Procedure Execute; override; Property Item : TCompileItem read FItem write SetItem; end; { TDirWatcher } TDirWatcher = Class(TComponent) Private FApp : THTTPApplication; FDW : TDirWatch; procedure DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents); Public Constructor Create(App : THTTPApplication; ADir : String); reintroduce; Destructor Destroy; override; end; { THTTPApplication } THTTPApplication = Class(TCustomHTTPApplication) private FProjectFile: String; FStatusLock : TCriticalSection; FQuiet: Boolean; FWatch: Boolean; FDW : TDirWatcher; FStatusList : TFPObjectList; FCompiles : TCompiles; Procedure AddToStatus(AEntry : TDirectoryEntry; AEvents : TFileEvents); procedure DoStatusRequest(ARequest: TRequest; AResponse: TResponse); procedure DoRecompile(ARequest: TRequest; AResponse: TResponse); function ScheduleCompile(const aProjectFile: String; ACommandLine : String = ''): Integer; procedure StartWatch(ADir: String); procedure Usage(Msg: String); public Constructor Create(AOWner : TComponent); override; Destructor Destroy; override; procedure DoLog(EventType: TEventType; const Msg: String); override; Procedure DoRun; override; property Quiet : Boolean read FQuiet Write FQuiet; Property Watch : Boolean Read FWatch Write FWatch; Property ProjectFile : String Read FProjectFile Write FProjectFile; end; { TCompileThread } procedure TCompileThread.SetItem(AValue: TCompileItem); begin if FItem=AValue then Exit; FItem:=AValue; end; constructor TCompileThread.create(aItem: TCompileItem); begin end; procedure TCompileThread.Execute; begin end; { TCompiles } function TCompiles.GetC(AIndex : Integer): TCompileItem; begin Result:=Items[Aindex] as TCompileItem; end; { TCompileItem } function TCompileItem.GetOutput: TStrings; begin If (FOutput=Nil) then FOutput:=TStringList.Create; Result:=FOutput; end; { TDirWatcher } procedure TDirWatcher.DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents); begin if Assigned(FApp) then FApp.AddToStatus(AEntry,AEvents); end; constructor TDirWatcher.Create(App: THTTPApplication; ADir: String); begin Inherited create(APP); FApp:=App; FDW:=TDirwatch.Create(Self); FDW.AddWatch(ADir,allEvents); FDW.OnChange:=@DoChange; TThread.ExecuteInThread(@FDW.StartWatch); end; destructor TDirWatcher.Destroy; begin FApp:=Nil; FDW.Terminate; FreeAndNil(FDW); inherited Destroy; end; { THTTPApplication } procedure THTTPApplication.DoLog(EventType: TEventType; const Msg: String); begin if Quiet then exit; if IsConsole then Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg) else inherited DoLog(EventType, Msg); end; procedure THTTPApplication.Usage(Msg : String); begin if (Msg<>'') then Writeln('Error: ',Msg); Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] '); Writeln('Where options is one or more of : '); Writeln('-d --directory=dir Base directory from which to serve files.'); Writeln(' Default is current working directory: ',GetCurrentDir); Writeln('-h --help This help text'); Writeln('-i --indexpage=name Directory index page to use (default: index.html)'); Writeln('-n --noindexpage Do not allow index page.'); Writeln('-p --port=NNNN TCP/IP port to listen on (default is 3000)'); Writeln('-q --quiet Do not write diagnostic messages'); Writeln('-w --watch Watch directory for changes'); Halt(Ord(Msg<>'')); end; constructor THTTPApplication.Create(AOWner: TComponent); begin inherited Create(AOWner); FStatusLock:=TCriticalSection.Create; FStatusList:=TFPObjectList.Create(False); FCompiles:=TCompiles.Create(TCompileItem); end; destructor THTTPApplication.Destroy; begin FStatusLock.Enter; try FreeAndNil(FCompiles); FreeAndNil(FStatusList); finally FStatusLock.Leave; end; FreeAndNil(FStatusLock); inherited Destroy; end; procedure THTTPApplication.StartWatch(ADir : String); begin FDW:=TDirWatcher.Create(Self,ADir); end; procedure THTTPApplication.AddToStatus(AEntry: TDirectoryEntry; AEvents: TFileEvents); begin Log(etDebug,'File change detected: %s (%s)',[AEntry.name,FileEventsToStr(AEvents)]); FStatusLock.Enter; try FStatusList.Add(TJSONObject.Create(['action','file','name',AEntry.name,'events',FileEventsToStr(AEvents)])); finally FStatusLock.Leave; end; end; procedure THTTPApplication.DoStatusRequest(ARequest : TRequest; AResponse : TResponse); Var R,O : TJSONObject; A : TJSONArray; I : integer; begin Log(etDebug,'Status request from: %s',[ARequest.RemoteAddress]); R:=Nil; try FStatusLock.Enter; try if (FStatusList.Count=0) then R:=TJSONObject.Create(['ping',True]) else begin O:=FStatusList[0] as TJSONObject; FStatusList.Delete(0); if O.Get('action','')<>'file' then R:=O else begin // If first event is file event, then add and delete all file events in list. A:=TJSONArray.Create([O]); O.Delete('action'); R:=TJSONObject.Create(['action','sync','files',A]); For I:=FStatusList.Count-1 downto 0 do begin O:=FStatusList[0] as TJSONObject; if (O.Get('action','')='file') then begin A.Add(O); O.Delete('action'); FStatusList.Delete(I); end; end; end end; finally FStatusLock.Leave; end; AResponse.ContentType:='application/json'; AResponse.Content:=R.AsJSON; AResponse.SendResponse; finally R.Free; end; end; Function THTTPApplication.ScheduleCompile(const aProjectFile : String; ACommandLine : String = '') : Integer; Var CI : TCompileItem; begin CI:=FCompiles.Add as TCompileItem; CI.FileName:=AProjectFile; CI.FThread:=TCompileThread.Create(CI); Result:=CI.ID; end; procedure THTTPApplication.DoRecompile(ARequest: TRequest; AResponse: TResponse); Var ID : Integer; PF,CL : String; begin PF:=ARequest.ContentFields.Values['ProjectFile']; CL:=ARequest.ContentFields.Values['CommandLine']; if PF='' then PF:=ProjectFile; If (PF='') then begin AResponse.Code:=404; AResponse.CodeText:='No project file'; AResponse.ContentType:='application/json'; AResponse.Content:='{ "success" : false, "message": "no project file set or provided" }'; end else begin ID:=ScheduleCompile(PF,CL); AResponse.Code:=200; AResponse.ContentType:='application/json'; AResponse.Content:=Format('{ "success" : true, "file": "%s", "commandLine" : "%s", "compileID": %d }',[StringToJSONString(PF),StringToJSONString(CL),ID]); end; end; procedure THTTPApplication.DoRun; Var S,IndexPage,D : String; begin S:=Checkoptions('hqd:ni:p:wc::',['help','quiet','noindexpage','directory:','port:','indexpage:','watch','compile::']); if (S<>'') or HasOption('h','help') then usage(S); Quiet:=HasOption('q','quiet'); Watch:=HasOption('w','watch'); Port:=StrToIntDef(GetOptionValue('p','port'),3000); D:=GetOptionValue('d','directory'); if D='' then D:=GetCurrentDir; Log(etInfo,'Listening on port %d, serving files from directory: %s',[Port,D]); {$ifdef unix} {$ifdef darwin} MimeTypesFile:='/private/etc/apache2/mime.types'; {$else} MimeTypesFile:='/etc/mime.types'; {$endif} {$endif} if Watch then StartWatch(D); httprouter.RegisterRoute('$sys/status',rmGet,@DoStatusRequest); if Hasoption('c','compile') then begin ProjectFile:=GetOptionValue('c','compile'); if ProjectFile='' then ProjectFile:=IncludeTrailingPathDelimiter(D)+'server.lpr'; If Not FileExists(ProjectFile) then ProjectFile:=IncludeTrailingPathDelimiter(D)+'server.lpr'; httprouter.RegisterRoute('$sys/compile',rmPost,@DoRecompile); end; TSimpleFileModule.BaseDir:=IncludeTrailingPathDelimiter(D); TSimpleFileModule.OnLog:=@Log; If not HasOption('n','noindexpage') then begin IndexPage:=GetOptionValue('i','indexpage'); if IndexPage='' then IndexPage:='index.html'; Log(etInfo,'Using index page %s',[IndexPage]); TSimpleFileModule.IndexPageName:=IndexPage; end; TSimpleFileModule.RegisterDefaultRoute; inherited; end; Var Application : THTTPApplication; begin Application:=THTTPApplication.Create(Nil); Application.Initialize; Application.Run; Application.Free; end.