lazarus/components/simplewebservergui/simplewebsrvcontroller.pas
2022-04-23 17:10:19 +02:00

2234 lines
63 KiB
ObjectPascal

{
Author: Mattias Gaertner
Working:
- Start/Stop compileserver
- server log
- on socketerror find conflicting process (Linux, Windows, Macos)
- on socketerror find free port (Linux, Windows, Macos)
- option: port
- option: interfaceaddress
- change port
- add/delete location
- enable/disable location
- list of locations: location, enabled, filepath, lpi
- floating/dockable window
- IDE menu item: View / Simple Web Server
- options frame
- apply options: restart server
- stop server on close IDE
- options: check and warn about wrong values
- BindAny
- IDE macros SWSPort,SWSAddress, SWSExe
- listview
- delete button
- custom http servers on different ports
- check port conflict
- changing main port: check conflict with custom servers
- list in window
- stop all on close
- dialog to add user folder
ToDos:
- log with time and port
- ide macro SWSExe param: 'base', 'used' and ''
- Windows: add GetUDPTable2
- SSL
}
unit SimpleWebSrvController;
{$mode objfpc}{$H+}
interface
uses
Math, Types, Classes, SysUtils, process, Pipes, Contnrs, fpjson, fphttpclient,
Sockets,
// lazutils
LazLoggerBase, FileUtil, LazUTF8, LazFileUtils, LazMethodList, LazUtilities,
LazStringUtils, LazFileCache,
// LCL
Forms, Dialogs, Controls, LazHelpIntf, LCLIntf,
// IDEIntf
IDEDialogs, IDEMsgIntf, LazIDEIntf, IDEExternToolIntf, MacroIntf,
MacroDefIntf,
// sws
ProjectIntf, SimpleWebSrvUtils, SimpleWebSrvOptions,
SimpleWebSrvStrConsts;
type
ESimpleWebServerException = class(Exception)
end;
TSWebServerState = (
swssNone,
swssStarting,
swssRunning,
swssStopping
);
TSWebServerStates = set of TSWebServerState;
TSWebServerError = (
swseNone,
swseBindingOfSocketFailed
);
TSWebServerErrors = set of TSWebServerError;
{ TSWSLocation }
TSWSLocation = class(TPersistent)
public
Location: string; // URL subfolder
Path: string; // path on disk
Origin: string; // e.g. a lpi file or 'user'
Enable: boolean;
ErrorDesc: string;
end;
TSWServerThread = class;
TSimpleWebServerController = class;
{ TSWSInstance }
TSWSInstance = class(TPersistent)
Private
FParams: TStrings;
procedure SetParams(AValue: TStrings);
public
Controller: TSimpleWebServerController;
Exe: string; // may contain macros and not expanded
ExeUsed: string; // resolved macros, expanded filename
Port: word;
Path: string; // path on disk, can contain macros
PathUsed: string; // path on disk, resolved macros, expanded filename
Origin: string; // e.g. a lpi file or 'user'
ErrorDesc: string;
State: TSWebServerState;
Thread: TSWServerThread;
ExitCode: integer;
constructor Create;
destructor Destroy; override;
Property Params : TStrings Read FParams Write SetParams;
end;
TSWServerLogEvent = procedure(Sender: TObject; OutLines: TStrings) of object;
TSWServerStateChangedEvent = procedure(Sender: TObject; Instance: TSWSInstance) of object;
{ TSWServerThread }
TSWServerThread = class(TThread)
private
FOwner: TSWSInstance;
protected
fSleepEvent: PRTLEvent;
FOutLines: TStrings;
procedure Execute; override; // this thread
procedure HandleOutput; // this thread
procedure SynchronizedHandleOuput; // main thread
procedure QueuedFinished({%H-}Data: PtrInt); // main thread
public
TheProcess: TProcess;
OnOutput: TSWServerLogEvent; // main thread
OnFinished: TNotifyEvent; // main thread
constructor Create(TheOwner: TSWSInstance; aProcess: TProcess);
destructor Destroy; override;
procedure ShutDown(Gracefully: boolean); // main thread
procedure TerminateProcess;
property Owner: TSWSInstance read FOwner;
end;
TSWSGetLocationsLocation = record
Location: string; // http name
Path: string; // path on disk
end;
TSWSGetLocationsLocationArray = array of TSWSGetLocationsLocation;
TSWSGetLocationsResponse = class
public
Locations: TSWSGetLocationsLocationArray;
end;
TSWSCHandler = (
swschStateChanged, // TSWServerStateChangedEvent
swschLocationsChanged, // TNotifyEvent
swschServerLog // TSWServerLogEvent
);
TSWSCHandlers = set of TSWSCHandler;
{ TSimpleWebServerController }
TSimpleWebServerController = class(TComponent)
private
FAPIKey: string;
fAPIPath: string;
FDestroying: boolean;
FMainSrvAddr: string;
FMainSrvBindAny: boolean;
fHandlers: array[TSWSCHandler] of TMethodList;
FInstances: TFPList; // list of TSWSInstance
FLocations: TObjectList; // list of TSWSLocation
FMainSrvInstance: TSWSInstance;
FLogLines: TStrings;
FOptions: TSimpleWebServerOptions;
FMainSrvError: TSWebServerError;
FUtility: TSimpleWebServerUtility;
FViewCaption: string;
function GetLocationCount: integer;
function GetLocations(Index: integer): TSWSLocation;
function GetMainSrvExe: string;
function GetMainSrvExitCode: integer;
function GetMainSrvPort: word;
function GetMainSrvState: TSWebServerState;
function GetMainSrvThread: TSWServerThread;
function GetServerCount: integer;
function GetServers(Index: integer): TSWSInstance;
function GetSWSAddress(const s: string; const {%H-}Data: PtrInt;
var Abort: boolean): string;
function GetSWSExe(const s: string; const {%H-}Data: PtrInt; var Abort: boolean
): string;
function GetSWSPort(const s: string; const {%H-}Data: PtrInt; var Abort: boolean
): string;
procedure OnApplyOptions(Sender: TObject);
procedure OnIDEClose(Sender: TObject);
procedure OnServerFinished(Sender: TObject);
procedure OnServerOutput(Sender: TObject; OutLines: TStrings);
protected
IDEMacroSWSAddress: TTransferMacro;
IDEMacroSWSExe: TTransferMacro;
IDEMacroSWSPort: TTransferMacro;
function ParseServerResponse(Response: TStream): TSWSGetLocationsResponse; virtual;
procedure GetLocationsFromServer; virtual;
procedure AddServerLocation(Location, Path: string); virtual;
procedure RemoveServerLocation(Location: string); virtual;
procedure AddIDEMessageInfo(DbgPrefix, Msg: string);
procedure SetServerState(Instance: TSWSInstance; NewState: TSWebServerState); virtual;
procedure StateChanged(Instance: TSWSInstance); virtual;
procedure LocationsChanged; virtual;
procedure ServerLog(OutLines: TStrings); virtual;
function StartServerInstance(Instance: TSWSInstance; ResolveMacros, Interactive: boolean): boolean; virtual;
function StopServerInstance(Instance: TSWSInstance; Interactive: boolean): boolean; virtual;
procedure StopAllServers; virtual;
function GetMainServerExeHint: string;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure HookMacros; virtual;
procedure UnhookMacros; virtual;
public
// main server where multiple locations share one port
function StartMainServer(Interactive: boolean): boolean; virtual;
function StopMainServer(Interactive: boolean): boolean; virtual;
function AddLocation(Location, Path, Origin: string; Enable: boolean): TSWSLocation; virtual;
function AddProjectLocation(aProject: TLazProject; Location, Path: string; Interactive: boolean): TSWSLocation; virtual;
procedure DeleteLocation(Location: string); virtual;
procedure EnableLocation(Location: string; Enable: boolean); virtual;
function IndexOfLocation(Location: string): integer; virtual;
function FindLocation(Location: string): TSWSLocation; virtual;
function FindLocationWithOrigin(Origin: string): TSWSLocation; virtual;
procedure AddHandlerLocationsChanged(const OnLocationsChanged: TNotifyEvent; AsLast: boolean = false);
procedure RemoveHandlerLocationsChanged(const OnLocationsChanged: TNotifyEvent);
procedure AddHandlerStateChanged(const OnStateChanged: TSWServerStateChangedEvent; AsLast: boolean = false);
procedure RemoveHandlerStateChanged(const OnStateChanged: TSWServerStateChangedEvent);
procedure AddHandlerServerLog(const OnServerLog: TSWServerLogEvent; AsLast: boolean = false);
procedure RemoveHandlerServerLog(const OnServerLog: TSWServerLogEvent);
procedure RemoveAllHandlersOfObject(AnObject: TObject);
// custom servers
function AddServer(Port: word; Exe: string; Params: TStrings;
Path, Origin: string; ResolveMacros, Interactive: boolean): TSWSInstance; virtual;
function AddProjectServer(aProject: TLazProject; Port: word;
Path: string; Interactive: boolean): TSWSInstance; virtual;
function FindServerWithPort(Port: word): TSWSInstance; virtual;
function FindServerWithOrigin(Origin: string): TSWSInstance; virtual;
function FindFreePort(Interactive, CheckServers: boolean; aStartPort: word = 0): word; virtual;
function StopServer(Instance: TSWSInstance; Interactive: boolean): boolean; virtual;
function SubstitutePortMacro(aValue, aPort: string): string;
function SubstituteURLMacro(aValue, AnURL: string): string;
function GetDefaultServerExe: string; virtual;
// browser
function GetURLWithServer(aServer: TSWSInstance; HTMLFilename: string): string; virtual;
function GetURLWithLocation(aLocation: TSWSLocation; HTMLFilename: string): string; virtual;
function OpenBrowserWithURL(URL, WorkDir: string): boolean; virtual;
function OpenBrowserWithLocation(aLocation: TSWSLocation; HTMLFilename: string): boolean; virtual;
function OpenBrowserWithServer(aServer: TSWSInstance; HTMLFilename: string): boolean; virtual;
function FindBrowserFile(ShortFilename: string): string; virtual;
function FindBrowserPath(Filenames: array of string; URL: string; Params: TStrings): string; virtual;
function GetBrowser(URL: string; Params: TStrings): string; virtual;
function GetBrowserChrome(URL: string; Params: TStrings): string; virtual;
function GetBrowserFirefox(URL: string; Params: TStrings): string; virtual;
function GetBrowserOpera(URL: string; Params: TStrings): string; virtual;
function GetBrowserVivaldi(URL: string; Params: TStrings): string; virtual;
{$IFDEF Darwin}
function GetBrowserSafari(URL: string; Params: TStrings): string; virtual;
{$ENDIF}
{$IFDEF MSWindows}
function GetBrowserEdge(URL: string; Params: TStrings): string; virtual;
{$ENDIF}
public
property Destroying: boolean read FDestroying;
property LocationCount: integer read GetLocationCount;
property Locations[Index: integer]: TSWSLocation read GetLocations;
property LogLines: TStrings read FLogLines;
property MainSrvAPIKey: string read FAPIKey;
property MainSrvAPIPath: string read fAPIPath;
property MainSrvPort: word read GetMainSrvPort;
property MainSrvBindAny: boolean read FMainSrvBindAny;
property MainSrvAddr: string read FMainSrvAddr;
property MainSrvError: TSWebServerError read FMainSrvError;
property MainSrvExe: string read GetMainSrvExe;
property MainSrvExitCode: integer read GetMainSrvExitCode;
property MainSrvThread: TSWServerThread read GetMainSrvThread;
property MainSrvState: TSWebServerState read GetMainSrvState;
property MainSrvInstance: TSWSInstance read FMainSrvInstance;
property Options: TSimpleWebServerOptions read FOptions;
property ServerCount: integer read GetServerCount;
property Servers[Index: integer]: TSWSInstance read GetServers;
property Utility: TSimpleWebServerUtility read FUtility;
property ViewCaption: string read FViewCaption; // the title in the IDE's Messages window
end;
var
SimpleWebServerController: TSimpleWebServerController; // created by Register
function CheckCompileServerExeQuality(var ServerExe: string; const BaseDir: string;
aSubtituteMacros: boolean): string; // on fail returns errormessage
implementation
function CheckCompileServerExeQuality(var ServerExe: string;
const BaseDir: string; aSubtituteMacros: boolean): string;
var
OutStr, ExpBaseDir: string;
begin
if aSubtituteMacros then
begin
if not IDEMacros.SubstituteMacros(ServerExe) then
exit('invalid macro');
end;
ServerExe:=Trim(ServerExe);
if ServerExe='' then
exit('file not found');
if ExtractFilePath(ServerExe)='' then
begin
ExpBaseDir:=BaseDir;
if aSubtituteMacros and not IDEMacros.SubstituteMacros(ExpBaseDir) then
exit('invalid macro in BaseDir');
if ExpBaseDir='' then
begin
ExpBaseDir:='$(LazarusDir)';
IDEMacros.SubstituteMacros(ExpBaseDir);
end;
ServerExe:=FindDefaultExecutablePath(ServerExe,ExpBaseDir);
if ServerExe='' then
exit('file not found in PATH');
end else
ServerExe:=ExpandFileNameUTF8(ServerExe);
if not FileExistsUTF8(ServerExe) then
exit('file not found');
if not FileIsExecutable(ServerExe) then
exit('file is not executable');
if not RunCommand(ServerExe,['--version'],OutStr) then
exit('compileserver does not support --version, maybe this is an old version?');
Result:='';
end;
{ TSWSInstance }
procedure TSWSInstance.SetParams(AValue: TStrings);
begin
if FParams=AValue then Exit;
FParams.Assign(AValue);
end;
constructor TSWSInstance.Create;
begin
FParams:=TStringList.Create;
end;
destructor TSWSInstance.Destroy;
begin
FreeAndNil(Thread);
FreeAndNil(FParams);
inherited Destroy;
end;
{ TSimpleWebServerController }
function TSimpleWebServerController.GetLocationCount: integer;
begin
Result:=FLocations.Count;
end;
function TSimpleWebServerController.GetLocations(Index: integer): TSWSLocation;
begin
Result:=TSWSLocation(FLocations[Index]);
end;
function TSimpleWebServerController.GetMainSrvExe: string;
begin
Result:=FMainSrvInstance.Exe;
end;
function TSimpleWebServerController.GetMainSrvExitCode: integer;
begin
Result:=FMainSrvInstance.ExitCode;
end;
function TSimpleWebServerController.GetMainSrvPort: word;
begin
Result:=FMainSrvInstance.Port;
end;
function TSimpleWebServerController.GetMainSrvState: TSWebServerState;
begin
Result:=FMainSrvInstance.State;
end;
function TSimpleWebServerController.GetMainSrvThread: TSWServerThread;
begin
Result:=FMainSrvInstance.Thread;
end;
function TSimpleWebServerController.GetServerCount: integer;
begin
Result:=FInstances.Count;
end;
function TSimpleWebServerController.GetServers(Index: integer): TSWSInstance;
begin
Result:=TSWSInstance(FInstances[Index]);
end;
function TSimpleWebServerController.GetSWSAddress(const s: string;
const Data: PtrInt; var Abort: boolean): string;
begin
Abort:=False;
if (s<>'') and (ConsoleVerbosity>=0) then
debugln(['Hint: (lazarus) [TSimpleWebServerController.GetSWSAddress] ignoring macro SWSAddress parameter "',s,'"']);
Result:=MainSrvAddr;
if Result='' then
Result:='SWSServerAddress'; // always return something to get nicer error messages
end;
function TSimpleWebServerController.GetSWSExe(const s: string;
const Data: PtrInt; var Abort: boolean): string;
begin
Abort:=False;
if (s<>'') and (ConsoleVerbosity>=0) then
debugln(['Hint: (lazarus) [TSimpleWebServerController.GetSWSExe] ignoring macro SWSExe parameter "',s,'"']);
Result:=MainSrvExe;
if Result='' then
Result:='SWSServerExe'; // always return something to get nicer error messages
end;
function TSimpleWebServerController.GetSWSPort(const s: string;
const Data: PtrInt; var Abort: boolean): string;
begin
Abort:=False;
if (s<>'') and (ConsoleVerbosity>=0) then
debugln(['Hint: (lazarus) [TSimpleWebServerController.GetSWSPort] ignoring macro SWSPort parameter "',s,'"']);
Result:=IntToStr(MainSrvPort);
if Result='' then
Result:='SWSServerPort'; // always return something to get nicer error messages
end;
procedure TSimpleWebServerController.OnApplyOptions(Sender: TObject);
var
OldRunning: Boolean;
begin
OldRunning:=MainSrvState=swssRunning;
StopMainServer(true);
FMainSrvBindAny:=Options.BindAny;
FMainSrvAddr:=Options.ServerAddr;
FMainSrvInstance.Exe:=Options.ServerExe;
FMainSrvInstance.Port:=Options.ServerPort;
if OldRunning then
StartMainServer(true);
end;
procedure TSimpleWebServerController.OnIDEClose(Sender: TObject);
begin
StopAllServers;
end;
procedure TSimpleWebServerController.OnServerFinished(Sender: TObject);
var
aProcDescription: string;
aPID, i: integer;
r: TModalResult;
NewPort: Word;
IPAddr: in_addr;
Instance: TSWSInstance;
begin
//debugln(['TSimpleWebServerController.OnServerFinished START']);
if Destroying then
exit;
Instance:=Sender as TSWSInstance;
if Instance.State=swssRunning then
StopServerInstance(Instance,false); // notify handlers
if FMainSrvInstance=Instance then
begin
// main server finished
// error handling
if (Instance.State<>swssStopping) and (MainSrvError=swseBindingOfSocketFailed) then
begin
if MainSrvBindAny then
IPAddr.s_addr:=0
else
IPAddr:=StrToHostAddr(MainSrvAddr);
if not FUtility.FindProcessListeningOnPort(IPAddr,MainSrvPort,aProcDescription,aPID) then
begin
IDEMessageDialog(rsSWError,
ViewCaption+':'+sLineBreak
+rsSWBindingOfSocketFailed+': '+MainSrvAddr+':'+IntToStr(MainSrvPort
), mtError, [mbOk]);
exit;
end;
r:=IDEQuestionDialog(rsSWError,
ViewCaption+':'+sLineBreak
+rsSWBindingOfSocketFailed+': '+MainSrvAddr+':'+IntToStr(MainSrvPort)+
sLineBreak
+sLineBreak
+rsSWTheFollowingProcessAlreadyListens+sLineBreak
+'PID: '+IntToStr(aPID)+sLineBreak
+aProcDescription+sLineBreak
+sLineBreak
+rsSWKillProcess
, mtError, [mrYes, Format(rsSWKillPID, [IntToStr(aPID)]), mrRetry,
rsSWTryAnotherPort, mrCancel], '');
case r of
mrYes:
if not FUtility.KillProcess(aPID) then
exit;
mrRetry:
begin
NewPort:=FindFreePort(true,false);
if NewPort=0 then
NewPort:=GetNextIPPort(MainSrvInstance.Port);
FMainSrvInstance.Port:=NewPort;
end;
else
exit;
end;
// try again
StartMainServer(true);
end;
end else begin
// custom server finished -> delete
FInstances.Remove(Instance);
i:=FInstances.IndexOf(Instance);
if i>=0 then
FInstances.Delete(i);
LocationsChanged;
end;
end;
procedure TSimpleWebServerController.OnServerOutput(Sender: TObject;
OutLines: TStrings);
const
BindingOfSocketFailed = 'Binding of socket failed: ';
var
i: Integer;
Line: String;
aThread: TSWServerThread;
begin
//debugln(['TSimpleWebServerController.OnServerOutput checking StdOut=',dbgs(FMainSrvThread.OutLines.Count),' ...']);
aThread:=Sender as TSWServerThread;
if aThread.Owner=FMainSrvInstance then
begin
for i:=0 to OutLines.Count-1 do
begin
Line:=OutLines[i];
//debugln(['Hint: TSimpleWebServerController.OnServerOutput {',Line,'}']);
if LeftStr(Line,length(BindingOfSocketFailed))=BindingOfSocketFailed then
FMainSrvError:=swseBindingOfSocketFailed;
end;
end;
ServerLog(OutLines);
end;
function TSimpleWebServerController.ParseServerResponse(Response: TStream
): TSWSGetLocationsResponse;
procedure Err(DbgPrefix, Msg: string);
begin
AddIDEMessageInfo(DbgPrefix,'get location failed: '+Msg);
end;
var
JSON: TJSONData;
Obj: TJSONObject;
Arr: TJSONArray;
i: Integer;
Location, Path: String;
ok: Boolean;
begin
Result:=TSWSGetLocationsResponse.Create;
ok:=false;
try
JSON:=GetJSON(Response);
if not (JSON is TJSONObject) then
begin
Err('20210913154156','response is not a JSON object');
exit;
end;
Obj:=TJSONObject(JSON);
JSON:=Obj.Find('data');
if not (JSON is TJSONArray) then
begin
Err('20210913154715','response data is not a JSON array');
exit;
end;
Arr:=TJSONArray(JSON);
SetLength(Result.Locations,Arr.Count);
for i:=0 to Arr.Count-1 do
begin
JSON:=Arr[i];
if not (JSON is TJSONObject) then
begin
Err('20210913154923','response data[i] is not a JSON object');
exit;
end;
Obj:=TJSONObject(JSON);
JSON:=Obj.Find('location');
if not (JSON is TJSONString) then
begin
Err('20210913155015','response data[i].location is not a JSON string');
exit;
end;
Location:=String(TJSONString(JSON).AsString);
JSON:=Obj.Find('path');
if not (JSON is TJSONString) then
begin
Err('20210913155016','response data[i].path is not a JSON string');
exit;
end;
Path:=String(TJSONString(JSON).AsString);
Result.Locations[i].Location:=Location;
Result.Locations[i].Path:=Path;
debugln('TSimpleWebServerController.ParseServerResponse ',dbgs(i),' location="',Location,'" path="',Path,'"');
end;
ok:=true;
finally
if not ok then
Result.Free;
end;
end;
procedure TSimpleWebServerController.GetLocationsFromServer;
procedure Err(Msg: string);
begin
AddIDEMessageInfo('[20210913154042]','get location failed: '+Msg);
end;
var
Response: TMemoryStream;
URL: String;
SrvLocations: TSWSGetLocationsResponse;
Client: TFPHTTPClient;
LocArr: TSWSGetLocationsLocationArray;
i: Integer;
Loc: TSWSLocation;
j: SizeInt;
HasChanged: Boolean;
begin
URL:='http://'+MainSrvAddr+':'+IntToStr(MainSrvPort)+'/'+MainSrvAPIPath+'/?APIKey='+MainSrvAPIKey+'&fmt=1';
Response:=TMemoryStream.Create;
SrvLocations:=nil;
Client:=TFPHTTPClient.Create(Nil);
try
HasChanged:=false;
Client.Get(URL,Response);
Response.Position:=0;
SrvLocations:=ParseServerResponse(Response);
LocArr:=SrvLocations.Locations;
for i:=0 to LocationCount-1 do
begin
Loc:=Locations[i];
j:=0;
while (j<length(LocArr)) and (LocArr[j].Location<>Loc.Location) do
inc(j);
if j=length(LocArr) then
begin
if Loc.Enable then
HasChanged:=true;
Loc.Enable:=false;
end else
begin
if not Loc.Enable then
HasChanged:=true;
Loc.Enable:=true;
if Loc.Path<>LocArr[j].Path then
AddIDEMessageInfo('20210915144542','path of location "'+Loc.Location+'" differ. IDE="'+Loc.Path+'" Server="'+LocArr[j].Path+'"');
end;
end;
finally
SrvLocations.Free;
Response.Free;
Client.Free;
end;
if HasChanged then
LocationsChanged;
end;
procedure TSimpleWebServerController.AddServerLocation(Location, Path: string);
var
SrvAddr, URL: String;
JSON: TStringStream;
Response: TMemoryStream;
Client: TFPHTTPClient;
begin
if MainSrvState<>swssRunning then exit;
SrvAddr:='http://'+MainSrvAddr+':'+IntToStr(MainSrvPort)+'/';
URL:=SrvAddr+MainSrvAPIPath+'/?APIKey='+MainSrvAPIKey+'&fmt=1';
JSON:=TStringStream.Create('{'
+'"location" : "'+StringToJSONString(Location)+'",'
+'"path": "'+StringToJSONString(Path)+'"'
+'}');
Response:=TMemoryStream.Create;
Client:=TFPHTTPClient.Create(Nil);
try
Client.RequestBody:=JSON;
Client.AddHeader('Content-Type','application/json');
AddIDEMessageInfo('20210913104107','adding "'+SrvAddr+Location+'", path="'+Path+'"');
try
Client.HTTPMethod('POST',URL,Response,[201,200]);
except
on E: Exception do begin
AddIDEMessageInfo('20220108152508','Failed: '+E.Message);
end;
end;
Response.Position:=0;
// maybe: check response
finally
Response.Free;
JSON.Free;
Client.Free;
end;
end;
procedure TSimpleWebServerController.RemoveServerLocation(Location: string);
var
URL: String;
Response: TMemoryStream;
Client: TFPHTTPClient;
begin
if MainSrvState<>swssRunning then exit;
URL:='http://'+MainSrvAddr+':'+IntToStr(MainSrvPort)+'/'+MainSrvAPIPath+'/'+Location+'?APIKey='+MainSrvAPIKey+'&fmt=1';
Response:=TMemoryStream.Create;
Client:=TFPHTTPClient.Create(Nil);
try
AddIDEMessageInfo('20210915112101','removing location "'+Location+'"');
try
Client.HTTPMethod('DELETE',URL,Response,[204,200]);
except
on E: EHTTPClient do begin
if E.StatusCode=404 then
// already gone
else
AddIDEMessageInfo('20210915113102','removing location "'+Location+'" failed: '+E.Message);
end;
on E: Exception do begin
AddIDEMessageInfo('20210915112928','removing location "'+Location+'" failed: '+E.Message);
end;
end;
Response.Position:=0;
// maybe: check response
finally
Response.Free;
Client.Free;
end;
end;
procedure TSimpleWebServerController.AddIDEMessageInfo(DbgPrefix,
Msg: string);
var
OutLines: TStringListUTF8Fast;
begin
debugln(['Hint: [',DbgPrefix,'] ',Msg]);
if ViewCaption='' then ;
OutLines:=TStringListUTF8Fast.Create;
try
OutLines.Add('IDE: '+Msg);
ServerLog(OutLines);
finally
OutLines.Free;
end;
if IDEMessagesWindow<>nil then
IDEMessagesWindow.AddCustomMessage(mluImportant,Msg,'',0,0,ViewCaption);
end;
procedure TSimpleWebServerController.SetServerState(Instance: TSWSInstance;
NewState: TSWebServerState);
begin
if Instance.State=NewState then exit;
Instance.State:=NewState;
StateChanged(Instance);
end;
procedure TSimpleWebServerController.StateChanged(Instance: TSWSInstance);
var
h: TMethodList;
i: Integer;
begin
h:=fHandlers[swschStateChanged];
i:=0;
while (i<h.Count) and not Destroying do
begin
TSWServerStateChangedEvent(h[i])(Self,Instance);
inc(i);
end;
end;
procedure TSimpleWebServerController.LocationsChanged;
begin
fHandlers[swschLocationsChanged].CallNotifyEvents(Self);
end;
procedure TSimpleWebServerController.ServerLog(OutLines: TStrings);
var
h: TMethodList;
i: Integer;
sl: TStringListUTF8Fast;
begin
FLogLines.AddStrings(OutLines);
if FLogLines.Count>SWSLogMaxLines then
begin
// delete old messages
sl:=TStringListUTF8Fast.Create;
try
for i:=FLogLines.Count-SWSLogMaxLines to FLogLines.Count-1 do
sl.Add(FLogLines[i]);
FLogLines.Assign(sl);
finally
sl.Free;
end;
end;
h:=fHandlers[swschServerLog];
i:=0;
while (i<h.Count) and not Destroying do
begin
TSWServerLogEvent(h[i])(Self,OutLines);
inc(i);
end;
end;
function TSimpleWebServerController.StartServerInstance(Instance: TSWSInstance;
ResolveMacros, Interactive: boolean): boolean;
var
PortStr, LinePostfix, TxtPostfix, ServerExeHelp: string;
function SubstituteMacro(PropName, Value: string; out ParsedValue: string): Boolean;
begin
ParsedValue:=SubstitutePortMacro(Value,PortStr);
if IDEMacros.SubstituteMacros(ParsedValue) then
exit(true);
Instance.ErrorDesc:='Failed substituting macros of property "'+PropName+'". '+LinePostfix;
AddIDEMessageInfo('20220126161036',Instance.ErrorDesc);
if Interactive then
IDEMessageDialog('Macro Error',
ViewCaption+':'+sLineBreak
+'Failed substituting macros of property "'+PropName+'".'+sLineBreak
+TxtPostfix,
mtError,[mbOk]);
Result:=false;
end;
function ErrDlg(aCaption, aMsg: string; IsExe: boolean): TModalResult;
var
s: String;
begin
s:=ViewCaption+':'+sLineBreak
+aMsg+sLineBreak
+TxtPostfix;
if IsExe then
s:=s+ServerExeHelp;
Result:=IDEMessageDialog(aCaption,s,mtError,[mbOk]);
end;
var
ExeUsed, PathUsed, Value, s, LazDir: String;
Params: TStringList;
i: Integer;
TheProcess: TProcess;
aThread: TSWServerThread;
ProcOpts: TProcessOptions;
begin
Result:=false;
case Instance.State of
swssNone: ;
swssStopping: exit(false);
swssStarting: exit(false);
swssRunning: exit(true);
end;
Instance.ExeUsed:='';
Instance.ErrorDesc:='';
Instance.ExitCode:=0;
Instance.PathUsed:='';
PortStr:=IntToStr(Instance.Port);
LinePostfix:='Port '+IntToStr(Instance.Port)+', Origin="'+Instance.Origin+'"';
TxtPostfix:='Port='+IntToStr(Instance.Port)+sLineBreak
+'Origin="'+Instance.Origin+'"'+sLineBreak;
if Instance=FMainSrvInstance then
ServerExeHelp:=GetMainServerExeHint
else
ServerExeHelp:='';
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StartServerInstance START']);
Params:=TStringList.Create;
try
if ResolveMacros then
begin
// resolve macros
if not SubstituteMacro('Executable',Instance.Exe,ExeUsed) then exit;
if not SubstituteMacro('Local Directory',Instance.Path,PathUsed) then exit;
if Instance.Params<>nil then
begin
for i:=0 to Instance.Params.Count-1 do
begin
if not SubstituteMacro('Params['+IntToStr(i)+']',Instance.Params[i],Value) then exit;
Params.Add(Value);
end;
end;
end else begin
ExeUsed:=Instance.Exe;
PathUsed:=Instance.Path;
Params.AddStrings(Instance.Params);
end;
// check for empty values
if ExeUsed='' then
begin
Instance.ErrorDesc:='missing server exe. '+LinePostfix;
AddIDEMessageInfo('20220127115641',Instance.ErrorDesc);
if Interactive then
ErrDlg(rsSWError, rsSWMissingServerExecutable, true);
exit;
end;
if PathUsed='' then
begin
Instance.ErrorDesc:='missing local directory. '+LinePostfix;
AddIDEMessageInfo('20220127115738',Instance.ErrorDesc);
if Interactive then
ErrDlg(rsSWError, rsSWMissingLocalDirectory, false);
exit;
end;
// expand local dir
PathUsed:=ChompPathDelim(ExpandFileNameUTF8(PathUsed));
if not DirectoryExistsUTF8(PathUsed) then
begin
// Note: main server does not need a main dir, the locations have their own dirs
Instance.ErrorDesc:='server directory not found "'+PathUsed+'". '+LinePostfix;
AddIDEMessageInfo('20220127122933',Instance.ErrorDesc);
if Interactive then
ErrDlg(rsSWError, Format(rsSWServerDirectoryNotFound, [PathUsed]), false
);
exit;
end;
// check exe
if ExtractFilePath(ExeUsed)='' then
begin
LazDir:='$(LazarusDir)';
IDEMacros.SubstituteMacros(LazDir);
s:=FindDefaultExecutablePath(ExeUsed,LazDir);
if s='' then
begin
Instance.ErrorDesc:='server exe "'+ExeUsed+'" not found in PATH. '+LinePostfix;
AddIDEMessageInfo('20220127115917',Instance.ErrorDesc);
if Interactive then
ErrDlg(rsSWError, Format(rsSWServerExecutableNotFoundInPATH, [ExeUsed]
), true);
exit;
end;
ExeUsed:=s;
end else
ExeUsed:=ExpandFileNameUTF8(ExeUsed);
debugln(['Hint: [simplewebserver_startserver] ',ViewCaption,' Exe="',ExeUsed,'" LocalDir="',PathUsed,'"',
' Params=[',MergeCmdLineParams(Params),'] ',LinePostfix]);
if not FileExistsUTF8(ExeUsed) then
begin
AddIDEMessageInfo('20220127114637','Error: server exe not found "'+ExeUsed+'"');
if Interactive then
ErrDlg(rsSWError, Format(rsSWFileNotFound, [ExeUsed]), true);
exit;
end;
if not FileIsExecutable(ExeUsed) then
begin
AddIDEMessageInfo('20220127121636','Error: server exe not executable "'+ExeUsed+'"');
if Interactive then
ErrDlg(rsSWError, Format(rsSWServerExeIsNotExecutable, [ExeUsed]), true
);
exit;
end;
// let's get to work
Instance.ExeUsed:=ExeUsed;
Instance.PathUsed:=PathUsed;
SetServerState(Instance,swssStarting);
// start process
AddIDEMessageInfo('20210909125756','run: '+MaybeQuote(ExeUsed)+' '+MergeCmdLineParams(Params));
TheProcess := TProcess.Create(nil);
try
TheProcess.Executable:=ExeUsed;
TheProcess.Parameters.Assign(Params);
ProcOpts:=[poUsePipes];
TheProcess.Options:= ProcOpts;
TheProcess.ShowWindow := swoHide; // needed by Windows, ignored by Unix
TheProcess.StartupOptions:=[suoUseShowWindow]; // needed by Windows, ignored by Unix
TheProcess.CurrentDirectory:=PathUsed;
TheProcess.Execute;
except
on E: Exception do begin
AddIDEMessageInfo('20210909125752',
'unable to run '+MaybeQuote(ExeUsed)+' '+MergeCmdLineParams(Params)+': '+E.Message);
TheProcess.Free;
exit;
end;
end;
Result:=true;
finally
Params.Free;
if not Result then
begin
SetServerState(Instance,swssNone);
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StartServerInstance END']);
end;
end;
try
// start thread
aThread:=TSWServerThread.Create(Instance,TheProcess);
Instance.Thread:=aThread;
aThread.OnOutput:=@OnServerOutput;
aThread.OnFinished:=@OnServerFinished;
aThread.FreeOnTerminate:=false;
aThread.Start;
SetServerState(Instance,swssRunning);
finally
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StartServerInstance END']);
end;
end;
function TSimpleWebServerController.StopServerInstance(Instance: TSWSInstance;
Interactive: boolean): boolean;
var
aThread: TSWServerThread;
begin
Result:=false;
if Interactive then ;
case Instance.State of
swssNone: exit(true);
swssStarting: exit(false);
swssRunning: ;
swssStopping: exit(false);
end;
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StopServer START Port=',Instance.Port,' Origin=',Instance.Origin,' ',MaybeQuote(Instance.ExeUsed)]);
SetServerState(Instance,swssStopping);
try
aThread:=Instance.Thread;
if aThread=nil then
begin
Result:=true;
exit;
end;
aThread.ShutDown(true);
if aThread.TheProcess<>nil then
begin
Instance.ExitCode:=aThread.TheProcess.ExitCode;
if (Instance=FMainSrvInstance) and (Instance.ExitCode=1) then
FMainSrvError:=swseBindingOfSocketFailed;
end
else
Instance.ExitCode:=0;
FreeAndNil(Instance.Thread);
Instance.ExeUsed:='';
Instance.PathUsed:='';
Result:=true;
finally
SetServerState(Instance,swssNone);
end;
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StopServer END']);
end;
procedure TSimpleWebServerController.StopAllServers;
function CleanUp: boolean;
var
i: Integer;
Instance: TSWSInstance;
aThread: TSWServerThread;
begin
for i:=ServerCount-1 downto 0 do
begin
Instance:=Servers[i];
aThread:=Instance.Thread;
if (Instance.State<>swssRunning) or ((aThread<>nil) and aThread.Finished) then
begin
FInstances.Remove(Instance);
if Instance=FMainSrvInstance then
FMainSrvInstance:=nil;
Instance.Free;
end;
end;
Result:=ServerCount=0;
end;
procedure TerminateSrv(Instance: TSWSInstance);
var
aThread: TSWServerThread;
begin
aThread:=Instance.Thread;
if (Instance.State=swssRunning) and (aThread<>nil) then
begin
Instance.State:=swssStopping;
aThread.TerminateProcess;
aThread.Terminate;
end;
end;
var
i: Integer;
Instance: TSWSInstance;
begin
// terminate all processes and threads
for i:=0 to ServerCount-1 do
TerminateSrv(Servers[i]);
if CleanUp then exit;
// wait for all threads (max 2 seconds)
i:=0;
repeat
sleep(15); // windows sleep has a resolution of 1/64th of a second
CheckSynchronize(1);
inc(i);
until CleanUp or (i=140);
// free all instances
FMainSrvInstance:=nil;
for i:=ServerCount-1 downto 0 do
begin
Instance:=Servers[i];
Instance.Free;
end;
FInstances.Clear;
end;
function TSimpleWebServerController.GetMainServerExeHint: string;
begin
Result:='see Tools / Options / Environment / Simple Web Server / Compileserver';
end;
constructor TSimpleWebServerController.Create(AOwner: TComponent);
var
h: TSWSCHandler;
begin
inherited Create(AOwner);
FUtility:=TSimpleWebServerUtility.Create;
FViewCaption:='Simple Web Server';
FUtility.ViewCaption:=FViewCaption;
FMainSrvBindAny:=false;
FMainSrvAddr:=SWSDefaultServerAddr;
fAPIPath:=SWSDefaultAPIPath;
FLocations:=TObjectList.Create;
FLogLines:=TStringListUTF8Fast.Create;
FInstances:=TFPList.Create;
FMainSrvInstance:=TSWSInstance.Create;
FInstances.Add(FMainSrvInstance);
FMainSrvInstance.Controller:=Self;
FMainSrvInstance.Port:=SWSDefaultServerPort;
FMainSrvInstance.Exe:='compileserver'+GetExeExt;
for h in TSWSCHandler do
fHandlers[h]:=TMethodList.Create;
FOptions:=TSimpleWebServerOptions.Create;
FOptions.AddHandlerApply(@OnApplyOptions);
LazarusIDE.AddHandlerOnIDEClose(@OnIDEClose);
end;
destructor TSimpleWebServerController.Destroy;
var
h: TSWSCHandler;
begin
FDestroying:=true;
StopAllServers;
UnhookMacros;
FreeAndNil(FInstances);
FreeAndNil(FOptions);
for h in TSWSCHandler do
FreeAndNil(fHandlers[h]);
FreeAndNil(FLogLines);
FreeAndNil(FLocations);
FreeAndNil(FUtility);
inherited Destroy;
end;
function TSimpleWebServerController.StartMainServer(Interactive: boolean
): boolean;
function ErrDlg(aCaption, aMsg: string; IsExe: boolean): TModalResult;
var
s: String;
begin
s:=ViewCaption+':'+sLineBreak
+aMsg+sLineBreak;
if IsExe then
s:=s+GetMainServerExeHint;
Result:=IDEMessageDialog(aCaption,s,mtError,[mbOk]);
end;
var
LazCfgDir, PathUsed, IniFilename, ErrMsg, ExeUsed: String;
IniLines: TStringListUTF8Fast;
MsgResult: TModalResult;
SecretGUID: TGUID;
i: Integer;
Loc: TSWSLocation;
begin
Result:=false;
case MainSrvState of
swssNone: ;
swssStopping: exit(false);
swssStarting: exit(false);
swssRunning: exit(true);
end;
if ConsoleVerbosity>1 then
debugln(['Hint: TSimpleWebServerController.StartMainServer START']);
LazCfgDir:=AppendPathDelim(LazarusIDE.GetPrimaryConfigPath);
PathUsed:=LazCfgDir+SWSMainServerPath;
IniFilename:=LazCfgDir+SWSCompileServerIni;
CreateGUID(SecretGUID);
FAPIKey:='';
for i:=0 to 15 do
FAPIKey:=FAPIKey+HexStr(PByte(@SecretGUID)[i],2);
if not ForceDirectoriesUTF8(PathUsed) then
begin
ErrMsg:=rsSWErrorCreatingDirectory+' "'+PathUsed+'"';
AddIDEMessageInfo('20220414180151',ErrMsg);
if Interactive then
ErrDlg(rsSWError, ErrMsg, false);
exit;
end;
FMainSrvInstance.Params.Free;
FMainSrvInstance.Params:=TStringListUTF8Fast.Create;
FMainSrvInstance.Params.Add('-c');
FMainSrvInstance.Params.Add(IniFilename);
// append custom options last
FMainSrvInstance.Params.AddStrings(Options.ServerOpts);
FMainSrvInstance.Path:=PathUsed;
ExeUsed:=Options.ServerExe;
ErrMsg:=CheckCompileServerExeQuality(ExeUsed,'',false);
FMainSrvInstance.Exe:=ExeUsed;
if ErrMsg<>'' then
begin
if Options.ServerExe<>ExeUsed then
ErrMsg:=ErrMsg+'. Path="'+ExeUsed+'"';
debugln(['Error: [TSimpleWebServerController.StartServerInstance] invalid ServerExe="',Options.ServerExe,'".']);
AddIDEMessageInfo('20220118164525',ErrMsg);
if Interactive then
ErrDlg(rsSWError, Format(rsSWWrongCompileserverExe, [ErrMsg]), true);
exit;
end;
// create workdir
while not DirectoryExistsUTF8(PathUsed) do
begin
if CreateDirUTF8(PathUsed) then
break;
MsgResult:=IDEMessageDialog(rsSWError,
ViewCaption+':'+sLineBreak
+rsSWErrorCreatingDirectory+sLineBreak
+'"'+PathUsed+'"'+sLineBreak,
mtError,[mbRetry,mbCancel]);
case MsgResult of
mrRetry: ;
else exit(false);
end;
end;
// write main server ini
IniLines:=TStringListUTF8Fast.Create;
try
IniLines.Add('[Server]');
IniLines.Add('SimpleServer=1');
IniLines.Add('NoIndexPage=1');
if MainSrvBindAny then
IniLines.Add('Interface=0.0.0.0')
else
IniLines.Add('Interface='+MainSrvAddr);
IniLines.Add('Port='+IntToStr(MainSrvPort));
IniLines.Add('Directory='+PathUsed);
IniLines.Add('API='+MainSrvAPIPath+','+MainSrvAPIKey);
IniLines.Add('[Locations]');
for i:=0 to LocationCount-1 do
begin
Loc:=Locations[i];
if not Loc.Enable then continue;
if not DirectoryExists(Loc.Path) then
begin
AddIDEMessageInfo('20220118165651','Warn: location "'+Loc.Location+'" directory not found: "'+Loc.Path+'"');
Loc.ErrorDesc:=rsSWDirectoryNotFound;
continue;
end else
Loc.ErrorDesc:='';
IniLines.Add(Loc.Location+'='+Loc.Path);
end;
repeat
Result:=false;
try
if ConsoleVerbosity>=0 then
debugln(['Hint: [simplewebserver_startserver] writing ',IniFilename]);
IniLines.SaveToFile(IniFilename);
Result:=true;
except
on E: Exception do begin
FMainSrvInstance.ErrorDesc:='Error writing server ini "'+IniFilename+'".';
AddIDEMessageInfo('20220127123435',FMainSrvInstance.ErrorDesc);
if Interactive then
begin
MsgResult:=IDEMessageDialog(rsSWError,
ViewCaption+':'+sLineBreak
+Format(rsSWErrorWriting, [IniFilename])+sLineBreak
+E.Message,
mtError,[mbRetry,mbCancel]);
case MsgResult of
mrRetry: continue;
else exit(false);
end;
end;
end;
end;
until Result;
finally
IniLines.Free;
end;
Result:=StartServerInstance(FMainSrvInstance,true,Interactive);
end;
function TSimpleWebServerController.StopMainServer(Interactive: boolean
): boolean;
begin
Result:=StopServerInstance(FMainSrvInstance,Interactive);
end;
function TSimpleWebServerController.AddLocation(Location, Path,
Origin: string; Enable: boolean): TSWSLocation;
var
i: Integer;
ExpPath: String;
DirNotFound: Boolean;
begin
Result:=nil;
debugln(['Hint: (TSimpleWebServerController.AddLocation) Location="',Location,'" Path="',Path,'" Origin="',Origin,'" Enable=',Enable]);
ExpPath:=ExpandFileNameUTF8(Path);
if Location=SWSDefaultAPIPath then
begin
AddIDEMessageInfo('20220119150200','invalid location "'+Location+'"');
exit;
end;
DirNotFound:=not DirectoryExistsUTF8(ExpPath);
if DirNotFound then
begin
AddIDEMessageInfo('20220110182017','Directory not found "'+ExpPath+'"');
Enable:=false;
end;
i:=IndexOfLocation(Location);
if i<0 then
begin
Result:=TSWSLocation.Create;
Result.Location:=Location;
Result.Path:=ExpPath;
Result.Origin:=Origin;
Result.Enable:=Enable;
if DirNotFound then
Result.ErrorDesc:='Directory not found';
FLocations.Add(Result);
end else
begin
// already exists
Result:=Locations[i];
if DirNotFound then
Result.ErrorDesc:=rsSWDirectoryNotFound;
if (Result.Enable=Enable) and (Result.Path=ExpPath) and (Result.Origin=Origin) then
exit;
if Result.Enable then
begin
if Result.Path=ExpPath then
begin
Result.Origin:=Origin;
LocationsChanged;
if not Enable then
RemoveServerLocation(Location);
exit;
end else
begin
// active location, ExpPath change
end;
end else
begin
// location was not active
end;
Result.Enable:=Enable;
Result.Path:=ExpPath;
Result.Origin:=Origin;
end;
LocationsChanged;
if Enable then
AddServerLocation(Location,ExpPath);
end;
function TSimpleWebServerController.AddProjectLocation(aProject: TLazProject;
Location, Path: string; Interactive: boolean): TSWSLocation;
var
aServer: TSWSInstance;
function StopOldServer(ID: int64; const Msg: string): boolean;
begin
if not StopServer(aServer,Interactive) then
begin
debugln(['Error: TSimpleWebServerController.AddProjectLocation ',ID,': ',Msg,', unable to stop old server']);
exit(false);
end;
aServer:=nil;
Result:=true;
end;
var
Origin: String;
begin
Result:=nil;
if aProject.IsVirtual then
Origin:=SWSTestprojectOrigin
else
Origin:=aProject.ProjectInfoFile;
aServer:=FindServerWithOrigin(Origin);
if (aServer<>nil) and (aServer<>MainSrvInstance) then
if not StopOldServer(20220423152004,'Path changed') then exit;
Result:=AddLocation(Location,Path,Origin,true);
if not StartMainServer(Interactive) then
exit(nil);
end;
procedure TSimpleWebServerController.DeleteLocation(Location: string);
var
i: Integer;
Loc: TSWSLocation;
WasEnabled: Boolean;
begin
if Destroying then exit;
i:=IndexOfLocation(Location);
if i<0 then
begin
debugln(['Warn: (TSimpleWebServerController.DeleteLocation) Location not found "',Location,'" ']);
exit;
end;
Loc:=Locations[i];
debugln(['Hint: (TSimpleWebServerController.DeleteLocation) Location="',Loc.Location,'" Path="',Loc.Path,'" Origin="',Loc.Origin,'" Enabled=',Loc.Enable]);
WasEnabled:=Loc.Enable;
FLocations.Delete(i);
LocationsChanged;
if WasEnabled then
RemoveServerLocation(Location);
end;
procedure TSimpleWebServerController.EnableLocation(Location: string;
Enable: boolean);
var
i: Integer;
Loc: TSWSLocation;
begin
if Destroying then exit;
i:=IndexOfLocation(Location);
if i<0 then
begin
debugln(['Warn: (TSimpleWebServerController.EnableLocation) Location not found "',Location,'" ']);
exit;
end;
Loc:=Locations[i];
if Loc.Enable=Enable then exit;
debugln(['Hint: (TSimpleWebServerController.EnableLocation) Location="',Loc.Location,'" Path="',Loc.Path,'" Origin="',Loc.Origin,'" Enable=',Enable]);
Loc.Enable:=Enable;
LocationsChanged;
if Enable then
begin
AddServerLocation(Loc.Location,Loc.Path);
end else begin
RemoveServerLocation(Loc.Location);
end;
end;
function TSimpleWebServerController.IndexOfLocation(Location: string): integer;
begin
Result:=0;
repeat
if Result=LocationCount then
exit(-1);
if Locations[Result].Location=Location then
exit;
inc(Result);
until false;
end;
function TSimpleWebServerController.FindLocation(Location: string
): TSWSLocation;
var
i: Integer;
begin
i:=IndexOfLocation(Location);
if i<0 then
Result:=nil
else
Result:=Locations[i];
end;
function TSimpleWebServerController.FindLocationWithOrigin(Origin: string
): TSWSLocation;
var
i: Integer;
begin
for i:=0 to LocationCount-1 do
begin
Result:=Locations[i];
if Result.Origin=Origin then
exit;
end;
Result:=nil;
end;
procedure TSimpleWebServerController.AddHandlerLocationsChanged(
const OnLocationsChanged: TNotifyEvent; AsLast: boolean);
begin
fHandlers[swschLocationsChanged].Add(TMethod(OnLocationsChanged),AsLast);
end;
procedure TSimpleWebServerController.RemoveHandlerLocationsChanged(
const OnLocationsChanged: TNotifyEvent);
begin
fHandlers[swschLocationsChanged].Remove(TMethod(OnLocationsChanged));
end;
procedure TSimpleWebServerController.AddHandlerStateChanged(
const OnStateChanged: TSWServerStateChangedEvent; AsLast: boolean);
begin
fHandlers[swschStateChanged].Add(TMethod(OnStateChanged),AsLast);
end;
procedure TSimpleWebServerController.RemoveHandlerStateChanged(
const OnStateChanged: TSWServerStateChangedEvent);
begin
fHandlers[swschStateChanged].Remove(TMethod(OnStateChanged));
end;
procedure TSimpleWebServerController.AddHandlerServerLog(
const OnServerLog: TSWServerLogEvent; AsLast: boolean);
begin
fHandlers[swschServerLog].Add(TMethod(OnServerLog),AsLast);
end;
procedure TSimpleWebServerController.RemoveHandlerServerLog(
const OnServerLog: TSWServerLogEvent);
begin
fHandlers[swschServerLog].Remove(TMethod(OnServerLog));
end;
procedure TSimpleWebServerController.RemoveAllHandlersOfObject(AnObject: TObject);
var
h: TSWSCHandler;
begin
for h in TSWSCHandler do
fHandlers[h].RemoveAllMethodsOfObject(AnObject);
end;
function TSimpleWebServerController.AddServer(Port: word; Exe: string;
Params: TStrings; Path, Origin: string; ResolveMacros, Interactive: boolean
): TSWSInstance;
begin
Result:=nil;
try
if Port=0 then
Port:=FindFreePort(Interactive,true);
if FindServerWithPort(Port)<>nil then
raise ESimpleWebServerException.Create('port '+IntToStr(Port)+' already in use');
Result:=TSWSInstance.Create;
Result.Controller:=Self;
Result.Port:=Port;
Result.Exe:=Exe;
Result.Params:=Params;
Result.Path:=Path;
Result.Origin:=Origin;
FInstances.Add(Result);
finally
if Result=nil then
Params.Free;
end;
try
StartServerInstance(Result,ResolveMacros,Interactive);
finally
LocationsChanged;
end;
end;
function TSimpleWebServerController.AddProjectServer(aProject: TLazProject;
Port: word; Path: string; Interactive: boolean): TSWSInstance;
var
aServer: TSWSInstance;
function StopOldServer(ID: int64; const Msg: string): boolean;
begin
if not StopServer(aServer,Interactive) then
begin
debugln(['Error: TSimpleWebServerController.AddProjectServer ',ID,': ',Msg,', unable to stop old server']);
exit(false);
end;
aServer:=nil;
Result:=true;
end;
var
Exe, Origin, aProcDescription, SrvAddr: String;
Params: TStringList;
ConflictServer: TSWSInstance;
IPAddr: in_addr;
aPID: integer;
r: TModalResult;
aLocation: TSWSLocation;
begin
Result:=nil;
if aProject.IsVirtual then
Origin:=SWSTestprojectOrigin
else
Origin:=aProject.ProjectInfoFile;
aLocation:=FindLocationWithOrigin(Origin);
if aLocation<>nil then
DeleteLocation(aLocation.Location);
aServer:=FindServerWithOrigin(Origin);
if (aServer=nil) and not aProject.IsVirtual then
aServer:=FindServerWithOrigin(SWSTestprojectOrigin);
if aServer=FMainSrvInstance then
begin
debugln(['Error: TSimpleWebServerController.AddProjectServer 20220423165802 origin=mainsrv origin']);
IDEMessageDialog('Error','Conflict with main server',mtError,[mbOk]);
exit;
end;
if (aServer<>nil) and (aServer.Path<>Path) then
if not StopOldServer(20220410145323,'Path changed') then exit;
if (aServer<>nil) and (Port>0) and (aServer.Port<>Port) then
if not StopOldServer(20220410145340,'Port changed') then exit;
Exe:=GetDefaultServerExe;
if (aServer<>nil) and (aServer.Exe<>Exe) then
if not StopOldServer(20220410145357,'ServerExe changed') then exit;
if Port=0 then
begin
if aServer<>nil then
Port:=aServer.Port // keep port
else
Port:=FindFreePort(Interactive,true);
end else begin
ConflictServer:=FindServerWithPort(Port);
if (ConflictServer<>nil) and (ConflictServer<>aServer) then
begin
// conflicting server
if (ConflictServer=MainSrvInstance) or not Interactive then
r:=mrRetry
else
r:=IDEQuestionDialog(rsSWError,
Format(rsSWThereIsAlreadyAServerOnPortOriginPath, [IntToStr(Port),
sLineBreak, ConflictServer.Origin+sLineBreak, ConflictServer.Path+
sLineBreak]),
mtError,[mrYes, rsSWStopServer, mrRetry, rsSWTryAnotherPort, mrCancel],
'');
case r of
mrYes:
if not StopServer(ConflictServer,Interactive) then
exit;
mrRetry:
Port:=FindFreePort(Interactive,true);
else
exit;
end;
end
else if (aServer=nil) or (aServer.ErrorDesc<>'') then begin
SrvAddr:='127.0.0.1';
IPAddr:=StrToHostAddr(SrvAddr);
if FUtility.FindProcessListeningOnPort(IPAddr,Port,aProcDescription,aPID) then
begin
// conflicting foreign process
r:=IDEQuestionDialog(rsSWError,
ViewCaption+':'+sLineBreak
+rsSWBindingOfSocketFailed+': '+SrvAddr+':'+IntToStr(Port)+sLineBreak
+sLineBreak
+rsSWTheFollowingProcessAlreadyListens+sLineBreak
+'PID: '+IntToStr(aPID)+sLineBreak
+aProcDescription+sLineBreak
+sLineBreak
+rsSWKillProcess
, mtError, [mrYes, Format(rsSWKillPID, [IntToStr(aPID)]), mrRetry,
rsSWTryAnotherPort, mrCancel], '');
case r of
mrYes:
if not FUtility.KillProcess(aPID) then
exit;
mrRetry:
begin
Port:=FindFreePort(true,true);
end;
else
exit;
end;
end;
end;
end;
Params:=TStringList.Create;
try
Params.Add('-s');
Params.Add('-n');
Params.Add('-I');
Params.Add('127.0.0.1');
Params.Add('--port='+IntToStr(Port));
if (aServer<>nil) and not Params.Equals(aServer.Params) then
if not StopOldServer(20220410145559,'Params changed') then exit;
if aServer<>nil then
begin
aServer.Origin:=Origin;
Result:=aServer;
exit;
end;
Result:=AddServer(Port,Exe,Params,Path,Origin,false,Interactive);
finally
Params.Free;
end;
end;
function TSimpleWebServerController.FindServerWithPort(Port: word): TSWSInstance;
var
i: Integer;
begin
for i:=0 to ServerCount-1 do
if Servers[i].Port=Port then
exit(Servers[i]);
Result:=nil;
end;
function TSimpleWebServerController.FindServerWithOrigin(Origin: string
): TSWSInstance;
var
i: Integer;
begin
for i:=0 to ServerCount-1 do
if Servers[i].Origin=Origin then
exit(Servers[i]);
Result:=nil;
end;
function TSimpleWebServerController.FindFreePort(Interactive,
CheckServers: boolean; aStartPort: word): word;
var
AvoidPorts: TWordDynArray;
i: Integer;
begin
if aStartPort=0 then
aStartPort:=MainSrvPort;
if CheckServers then
begin
Setlength(AvoidPorts{%H-},ServerCount);
for i:=0 to ServerCount-1 do
AvoidPorts[i]:=Servers[i].Port;
end else
AvoidPorts:=nil;
Result:=FUtility.FindFreePort(aStartPort,Interactive,AvoidPorts);
end;
function TSimpleWebServerController.StopServer(Instance: TSWSInstance;
Interactive: boolean): boolean;
begin
if Instance=FMainSrvInstance then
Result:=StopMainServer(Interactive)
else
Result:=StopServerInstance(Instance,Interactive);
end;
function TSimpleWebServerController.SubstitutePortMacro(aValue, aPort: string
): string;
var
l, i: SizeInt;
begin
Result:=aValue;
//debugln(['TSimpleWebServerController.SubstitutePortMacro Value="',aValue,'" Port=',aPort]);
l:=length('$(port)');
for i:=length(Result)-l+1 downto 1 do
begin
if (Result[i]='$') and SameText(copy(Result,i,l),'$(port)') then
LazStringUtils.ReplaceSubstring(Result,i,l,aPort);
end;
//debugln(['TSimpleWebServerController.SubstitutePortMacro Result="',Result,'"']);
end;
function TSimpleWebServerController.SubstituteURLMacro(aValue, AnURL: string
): string;
var
l, i: SizeInt;
begin
Result:=aValue;
l:=length('$(url)');
for i:=length(Result)-l+1 downto 1 do
begin
if (Result[i]='$') and SameText(copy(Result,i,l),'$(url)') then
LazStringUtils.ReplaceSubstring(Result,i,l,AnURL);
end;
end;
function TSimpleWebServerController.GetDefaultServerExe: string;
begin
Result:=Options.ServerExe;
if IDEMacros.SubstituteMacros(Result) then
exit;
debugln(['TSimpleWebServerController.GetDefaultServerExe Options.ServerExe=[',Options.ServerExe,']']);
raise Exception.Create('TSimpleWebServerController.GetDefaultServerExe: invalid macro');
end;
function TSimpleWebServerController.GetURLWithServer(aServer: TSWSInstance;
HTMLFilename: string): string;
begin
Result:=CreateRelativePath(HTMLFilename,aServer.Path);
Result:=FilenameToURLPath(Result);
Result:='http://'+MainSrvAddr+':'+IntToStr(aServer.Port)+'/'+Result;
end;
function TSimpleWebServerController.GetURLWithLocation(aLocation: TSWSLocation;
HTMLFilename: string): string;
begin
Result:=CreateRelativePath(HTMLFilename,aLocation.Path);
Result:=FilenameToURLPath(Result);
Result:='http://'+MainSrvAddr+':'+IntToStr(MainSrvPort)+'/'+aLocation.Location+'/'+Result;
end;
function TSimpleWebServerController.OpenBrowserWithURL(URL, WorkDir: string
): boolean;
var
Params: TStringList;
Exe: String;
Tool: TIDEExternalToolOptions;
begin
Params:=TStringList.Create;
try
Exe:=GetBrowser(URL,Params);
if Exe='' then
begin
IDEMessageDialog(rsSWError,
rsSWCannotFindBrowserSee+sLineBreak
+rsSWToolsOptionsSimpleWebServerBrowser,mtError,[mbOk]);
exit(false);
end;
Tool:=TIDEExternalToolOptions.Create;
Tool.Title:='Browser('+ExtractFileName(Exe)+')';
Tool.Executable:=Exe;
Tool.CmdLineParams:=MergeCmdLineParams(Params);
Tool.WorkingDirectory:=WorkDir;
Tool.MaxIdleInMS:=1000;
Result:=RunExternalTool(Tool);
finally
Params.Free;
end;
end;
function TSimpleWebServerController.OpenBrowserWithLocation(
aLocation: TSWSLocation; HTMLFilename: string): boolean;
var
URL: String;
begin
if aLocation=nil then
raise Exception.Create('TSimpleWebServerController.OpenBrowserWithLocation 20220423153505');
if not FilenameIsAbsolute(HTMLFilename) then
raise Exception.Create('TSimpleWebServerController.OpenBrowserWithLocation 20220423153507');
URL:=GetURLWithLocation(aLocation,HTMLFilename);
Result:=OpenBrowserWithURL(URL,ExtractFilePath(HTMLFilename));
end;
function TSimpleWebServerController.OpenBrowserWithServer(
aServer: TSWSInstance; HTMLFilename: string): boolean;
var
URL: String;
begin
if aServer=nil then
raise Exception.Create('TSimpleWebServerController.OpenBrowserWithServer 20220410185207');
if not FilenameIsAbsolute(HTMLFilename) then
raise Exception.Create('TSimpleWebServerController.OpenBrowserWithServer 20220410185208');
URL:=GetURLWithServer(aServer,HTMLFilename);
Result:=OpenBrowserWithURL(URL,ExtractFilePath(HTMLFilename));
end;
function TSimpleWebServerController.FindBrowserFile(ShortFilename: string
): string;
begin
Result := SearchFileInPath(ShortFilename + GetExeExt, '',
GetEnvironmentVariableUTF8('PATH'), PathSeparator,
[sffDontSearchInBasePath]);
end;
function TSimpleWebServerController.FindBrowserPath(Filenames: array of string;
URL: string; Params: TStrings): string;
var
i: Integer;
aFilename: String;
begin
Result:='';
for i:=low(Filenames) to high(Filenames) do
begin
aFilename:=Filenames[i];
if FilenameIsAbsolute(aFilename) then
begin
if FileExistsCached(aFilename) then
begin
Result:=aFilename;
break;
end;
end else begin
Result := FindBrowserFile(aFilename);
if Result<>'' then break;
end;
end;
if Result<>'' then
begin
Params.Add(URL);
end;
end;
function TSimpleWebServerController.GetBrowser(URL: string; Params: TStrings
): string;
var
Cmd: String;
begin
Result:='';
case Options.BrowserKind of
swsbkCustom:
begin
Cmd:=Options.BrowserCmd;
Cmd:=SubstituteURLMacro(Cmd,URL);
if not IDEMacros.SubstituteMacros(Cmd) then
begin
IDEMessageDialog(rsSWError, rsSWInvalidMacroSee+sLineBreak +
rsSWToolsOptionsSimpleWebServerBrowser,mtError,[mbOk]);
exit;
end;
SplitCmdLineParams(Cmd,Params);
Result:=Params[0];
Params.Delete(0);
end;
swsbkFirefox: Result:=GetBrowserFirefox(URL,Params);
swsbkChrome: Result:=GetBrowserChrome(URL,Params);
swsbkOpera: Result:=GetBrowserOpera(URL,Params);
swsbkVivaldi: Result:=GetBrowserVivaldi(URL,Params);
{$IFDEF Darwin}
swsbkSafari: Result:=GetBrowserSafari(URL,Params);
{$ENDIF}
{$IFDEF MSWindows}
swsbkEdge: Result:=GetBrowserEdge(URL,Params);
{$ENDIF}
else
begin
FindDefaultBrowser(Result,Cmd);
Cmd:=Format(Cmd,[URL]);
SplitCmdLineParams(Cmd,Params);
end;
end;
end;
function TSimpleWebServerController.GetBrowserChrome(URL: string;
Params: TStrings): string;
begin
Result := FindBrowserPath([
{$IFDEF Darwin}'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',{$ENDIF}
'google-chrome'],URL,Params);
end;
function TSimpleWebServerController.GetBrowserFirefox(URL: string;
Params: TStrings): string;
begin
Result := FindBrowserPath([
{$IFDEF Darwin}'/Applications/Firefox.app/Contents/MacOS/firefox',{$ENDIF}
'firefox','mozilla'],URL,Params);
end;
function TSimpleWebServerController.GetBrowserOpera(URL: string;
Params: TStrings): string;
begin
Result := FindBrowserPath([
{$IFDEF Darwin}'/Applications/Opera.app/Contents/MacOS/Opera',{$ENDIF}
'opera'],URL,Params);
end;
function TSimpleWebServerController.GetBrowserVivaldi(URL: string;
Params: TStrings): string;
begin
Result := FindBrowserPath([
{$IFDEF Darwin}'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',{$ENDIF}
'vivaldi'],URL,Params);
end;
{$IFDEF Darwin}
function TSimpleWebServerController.GetBrowserSafari(URL: string; Params: TStrings
): string;
begin
Result := FindBrowserPath(['/Applications/Safari.app/Contents/MacOS/Safari','safari'],URL,Params);
end;
{$ENDIF}
{$IFDEF MSWindows}
function TSimpleWebServerController.GetBrowserEdge(URL: string; Params: TStrings
): string;
begin
Result := FindBrowserPath(['edge'],URL,Params);
end;
{$ENDIF}
procedure TSimpleWebServerController.HookMacros;
function Add(const AName, ADescription: string;
AMacroFunction: TMacroFunction): TTransferMacro;
begin
Result:=TTransferMacro.Create(AName, '', ADescription, AMacroFunction, []);
IDEMacros.Add(Result);
end;
begin
IDEMacroSWSAddress:=Add('SWSAddress', rsSWSimpleWebServerAddress, @GetSWSAddress);
IDEMacroSWSPort:=Add('SWSPort', rsSWSimpleWebServerPort, @GetSWSPort);
IDEMacroSWSExe:=Add('SWSExe', rsSWSimpleWebServerExecutable, @GetSWSExe);
end;
procedure TSimpleWebServerController.UnhookMacros;
begin
// nothing at the moment
end;
{ TSWServerThread }
procedure TSWServerThread.Execute;
var
Buf: string;
function ReadInputPipe(aStream: TInputPipeStream; var LineBuf: string;
IsStdErr: boolean): boolean;
// true if some bytes have been read
var
Count: DWord;
StartPos: Integer;
i: DWord;
begin
Result:=false;
if Terminated or (aStream=nil) then exit;
Count:=aStream.NumBytesAvailable;
if Count=0 then exit;
Count:=aStream.Read(Buf[1],Min(length(Buf),Count));
if Count=0 then exit;
Result:=true;
StartPos:=1;
i:=1;
while i<=Count do begin
if Buf[i] in [#10,#13] then begin
LineBuf:=LineBuf+copy(Buf,StartPos,i-StartPos);
if IsStdErr then ;
FOutLines.Add(LineBuf);
LineBuf:='';
if (i<Count) and (Buf[i+1] in [#10,#13]) and (Buf[i]<>Buf[i+1])
then
inc(i);
StartPos:=i+1;
end;
inc(i);
end;
LineBuf:=LineBuf+copy(Buf,StartPos,Count-StartPos+1);
end;
var
OutputLine, StdErrLine: String;
HasOutput: Boolean;
begin
OutputLine:='';
StdErrLine:='';
Buf:='';
SetLength(Buf,4096);
try
repeat
if Terminated then exit;
HasOutput:=ReadInputPipe(TheProcess.Stderr,StdErrLine,true)
or ReadInputPipe(TheProcess.Output,OutputLine,false);
if (not HasOutput) then begin
// no more pending output
HandleOutput;
if Terminated then exit;
if not TheProcess.Running then break;
// no more pending output and process is still running -> wait a bit
RTLEventWaitFor(fSleepEvent,500);
end;
until false;
// add rest of output
if (OutputLine<>'') then
FOutLines.Add(OutputLine);
if (StdErrLine<>'') then
FOutLines.Add(StdErrLine);
if not Terminated then
FOutLines.Add('ExitCode='+IntToStr(TheProcess.ExitCode)+' ExitStatus='+IntToStr(TheProcess.ExitStatus));
HandleOutput;
//DebugLn(['Hint: [simplewebserver_serverthread] ExitCode=',TheProcess.ExitCode,' ExitStatus=',TheProcess.ExitStatus]);
finally
Application.QueueAsyncCall(@QueuedFinished,0);
end;
end;
procedure TSWServerThread.HandleOutput;
begin
if Terminated then exit;
if (FOutLines.Count=0) then exit;
Synchronize(@SynchronizedHandleOuput);
end;
procedure TSWServerThread.SynchronizedHandleOuput;
begin
if Assigned(OnOutput) then
OnOutput(Self,FOutLines);
FOutLines.Clear;
end;
procedure TSWServerThread.QueuedFinished(Data: PtrInt);
begin
if Assigned(OnFinished) then
OnFinished(Owner);
end;
constructor TSWServerThread.Create(TheOwner: TSWSInstance;
aProcess: TProcess);
begin
inherited Create(true);
FOwner:=TheOwner;
TheProcess:=aProcess;
FOutLines:=TStringList.Create;
fSleepEvent:=RTLEventCreate;
end;
destructor TSWServerThread.Destroy;
begin
ShutDown(false);
Application.RemoveAsyncCalls(Self);
FreeAndNil(TheProcess);
RTLEventDestroy(fSleepEvent);
FreeAndNil(FOutLines);
inherited Destroy;
end;
procedure TSWServerThread.ShutDown(Gracefully: boolean);
begin
if Finished then exit;
TerminateProcess;
if not Gracefully then
Terminate;
RTLEventSetEvent(fSleepEvent); // wake up thread
while not Finished do
begin
Application.ProcessMessages;
if Finished then break;
sleep(20);
end;
end;
procedure TSWServerThread.TerminateProcess;
begin
if TheProcess<>nil then
TheProcess.Terminate(0);
end;
finalization
FreeAndNil(SimpleWebServerController);
end.