1{
2    This file is part of the Free Pascal run time library.
3    Copyright (c) 2003 by the Free Pascal development team
4
5    CustomApplication class.
6
7    See the file COPYING.FPC, included in this distribution,
8    for details about the copyright.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
14 **********************************************************************}
15{$mode objfpc}
16{$h+}
17unit CustApp;
18
19Interface
20
21uses SysUtils,Classes,singleinstance;
22
23Type
24  TStringArray = Array of string;
25  TExceptionEvent = Procedure (Sender : TObject; E : Exception) Of Object;
26  TEventLogTypes = Set of TEventType;
27
28  { TCustomApplication }
29
30  TCustomApplication = Class(TComponent)
31  Private
32    FEventLogFilter: TEventLogTypes;
33    FOnException: TExceptionEvent;
34    FSingleInstance: TBaseSingleInstance;
35    FSingleInstanceClass: TBaseSingleInstanceClass; // set before FSingleInstance is created
36    FSingleInstanceEnabled: Boolean; // set before Initialize is called
37    FTerminated : Boolean;
38    FHelpFile,
39    FTitle : String;
40    FOptionChar : Char;
41    FCaseSensitiveOptions : Boolean;
42    FStopOnException : Boolean;
43    FExceptionExitCode : Integer;
44    function GetEnvironmentVar(VarName : String): String;
45    function GetExeName: string;
46    Function GetLocation : String;
47    function GetSingleInstance: TBaseSingleInstance;
48    procedure SetSingleInstanceClass(
49      const ASingleInstanceClass: TBaseSingleInstanceClass);
50    function GetTitle: string;
51  Protected
52    function GetOptionAtIndex(AIndex: Integer; IsLong: Boolean): String;
53    procedure SetTitle(const AValue: string); Virtual;
54    Function GetConsoleApplication : boolean; Virtual;
55    Procedure DoRun; Virtual;
56    Function GetParams(Index : Integer) : String;virtual;
57    function GetParamCount: Integer;Virtual;
58    Procedure DoLog(EventType : TEventType; const Msg : String);  virtual;
59  Public
60    constructor Create(AOwner: TComponent); override;
61    destructor Destroy; override;
62    // Some Delphi methods.
63    procedure HandleException(Sender: TObject); virtual;
64    procedure Initialize; virtual;
65    procedure Run;
66    procedure ShowException(E: Exception);virtual;
67    procedure Terminate; virtual;
68    procedure Terminate(AExitCode : Integer) ; virtual;
69    // Extra methods.
70    function FindOptionIndex(Const S : String; Var Longopt : Boolean; StartAt : Integer = -1) : Integer;
71    Function GetOptionValue(Const S : String) : String;
72    Function GetOptionValue(Const C: Char; Const S : String) : String;
73    Function GetOptionValues(Const C: Char; Const S : String) : TStringArray;
74    Function HasOption(Const S : String) : Boolean;
75    Function HasOption(Const C : Char; Const S : String) : Boolean;
76    Function CheckOptions(Const ShortOptions : String; Const Longopts : TStrings; Opts,NonOpts : TStrings; AllErrors : Boolean = False) : String;
77    Function CheckOptions(Const ShortOptions : String; Const Longopts : Array of string; Opts,NonOpts : TStrings; AllErrors : Boolean = False) : String;
78    Function CheckOptions(Const ShortOptions : String; Const Longopts : TStrings; AllErrors : Boolean = False) : String;
79    Function CheckOptions(Const ShortOptions : String; Const LongOpts : Array of string; AllErrors : Boolean = False) : String;
80    Function CheckOptions(Const ShortOptions : String; Const LongOpts : String; AllErrors : Boolean = False) : String;
81    Function GetNonOptions(Const ShortOptions : String; Const Longopts : Array of string) : TStringArray;
82    Procedure GetNonOptions(Const ShortOptions : String; Const Longopts : Array of string; NonOptions : TStrings);
83    Procedure GetEnvironmentList(List : TStrings;NamesOnly : Boolean);
84    Procedure GetEnvironmentList(List : TStrings);
85    Procedure Log(EventType : TEventType; const Msg : String);
86    Procedure Log(EventType : TEventType; const Fmt : String; const Args : array of const);
87    // Delphi properties
88    property ExeName: string read GetExeName;
89    property HelpFile: string read FHelpFile write FHelpFile;
90    property Terminated: Boolean read FTerminated;
91    property Title: string read FTitle write SetTitle;
92    property OnException: TExceptionEvent read FOnException write FOnException;
93    // Extra properties
94    Property ConsoleApplication : Boolean Read GetConsoleApplication;
95    Property Location : String Read GetLocation;
96    Property Params [Index : integer] : String Read GetParams;
97    Property ParamCount : Integer Read GetParamCount;
98    Property EnvironmentVariable[envName : String] : String Read GetEnvironmentVar;
99    Property OptionChar : Char Read FoptionChar Write FOptionChar;
100    Property CaseSensitiveOptions : Boolean Read FCaseSensitiveOptions Write FCaseSensitiveOptions;
101    Property StopOnException : Boolean Read FStopOnException Write FStopOnException;
102    Property ExceptionExitCode : Longint Read FExceptionExitCode Write FExceptionExitCode;
103    Property EventLogFilter : TEventLogTypes Read FEventLogFilter Write FEventLogFilter;
104    Property SingleInstance: TBaseSingleInstance read GetSingleInstance;
105    Property SingleInstanceClass: TBaseSingleInstanceClass read FSingleInstanceClass write SetSingleInstanceClass;
106    Property SingleInstanceEnabled: Boolean read FSingleInstanceEnabled write FSingleInstanceEnabled;
107  end;
108
109var CustomApplication : TCustomApplication = nil;
110
111Implementation
112
113{$ifdef darwin}
114uses
115  MacOSAll;
116{$endif}
117
118{ TCustomApplication }
119
120function TCustomApplication.GetExeName: string;
121{$if defined(darwin)}
122var
123  mainBundle: CFBundleRef;
124  executableUrl: CFURLRef;
125  executableFSPath: CFStringRef;
126  utf16len: ptrint;
127  error: boolean;
128begin
129  error:=false;
130  { Get main bundle. This even works most of the time for command line
131    applications
132  }
133  mainbundle:=CFBundleGetMainBundle;
134  if assigned(mainbundle) then
135    begin
136      { get the URL pointing to the executable of the bundle }
137      executableUrl:=CFBundleCopyExecutableURL(mainBundle);
138      if assigned(executableUrl) then
139        begin
140          { convert the url to a POSIX path }
141          executableFSPath:=CFURLCopyFileSystemPath(executableUrl,kCFURLPOSIXPathStyle);
142          CFRelease(executableUrl);
143          { convert to UTF-8 -- this is not really clean since in theory the
144            ansi-encoding could be different, but
145              a) all file i/o routines on Darwin expect utf-8-encoded strings
146              b) there is no easy way to convert the Unix LANG encoding
147                 setting to an equivalent CoreFoundation encoding
148          }
149          utf16len:=CFStringGetLength(executableFSPath);
150          // +1 for extra terminating #0 in the worst case, so the pos below
151          // will always find the #0
152          setlength(result,utf16len*3+1);
153          if CFStringGetCString(executableFSPath,@result[1],length(result),kCFStringEncodingUTF8) then
154            { truncate to actual length, #0 cannot appear in a file path }
155            setlength(result,pos(#0,result)-1)
156          else
157            error:=true;
158          CFRelease(executableFSPath);
159        end
160      else
161        error:=true;
162    end
163  else
164    error:=true;
165  if error then
166    { can't do better than this }
167    Result:=Paramstr(0);
168end;
169{$else darwin}
170begin
171  Result:=Paramstr(0);
172end;
173{$endif darwin}
174
175Procedure SysGetEnvironmentList(List : TStrings;NamesOnly : Boolean);
176
177var
178   s : string;
179   i,l,j,count : longint;
180
181begin
182  count:=GetEnvironmentVariableCount;
183  if count>0 then
184    for j:=1 to count  do
185     begin
186       s:=GetEnvironmentString(j);
187       l:=Length(s);
188       If NamesOnly then
189          begin
190            I:=pos('=',s);
191            If (I>0) then
192              S:=Copy(S,1,I-1);
193          end;
194       List.Add(S);
195    end;
196end;
197
198function TCustomApplication.GetEnvironmentVar(VarName : String): String;
199begin
200  Result:=GetEnvironmentVariable(VarName);
201end;
202
203procedure TCustomApplication.GetEnvironmentList(List: TStrings;
204  NamesOnly: Boolean);
205
206begin
207  // Routine must be in custapp.inc
208  SysGetEnvironmentList(List,NamesOnly);
209end;
210
211procedure TCustomApplication.GetEnvironmentList(List: TStrings);
212
213begin
214  GetEnvironmentList(List,False);
215end;
216
217function TCustomApplication.GetLocation: String;
218begin
219  Result:=ExtractFilePath(GetExeName);
220end;
221
222function TCustomApplication.GetParamCount: Integer;
223begin
224   Result:=System.ParamCount;
225end;
226
227function TCustomApplication.GetTitle: string;
228begin
229  Result:=FTitle;
230end;
231
232function TCustomApplication.GetParams(Index: Integer): String;
233begin
234  Result:=ParamStr(Index);
235end;
236
237function TCustomApplication.GetSingleInstance: TBaseSingleInstance;
238begin
239  if FSingleInstance = nil then
240    begin
241    if FSingleInstanceClass=Nil then
242      Raise ESingleInstance.Create('No single instance provider class set! Include a single-instance class unit such as advsingleinstance');
243    FSingleInstance := FSingleInstanceClass.Create(Self);
244    end;
245  Result := FSingleInstance;
246end;
247
248procedure TCustomApplication.SetTitle(const AValue: string);
249begin
250  FTitle:=AValue;
251end;
252
253function TCustomApplication.GetConsoleApplication: boolean;
254begin
255  Result:=IsConsole;
256end;
257
258procedure TCustomApplication.DoRun;
259begin
260  if Assigned(FSingleInstance) then
261    if FSingleInstance.IsServer then
262      FSingleInstance.ServerCheckMessages;
263
264  // Override in descendent classes.
265end;
266
267procedure TCustomApplication.DoLog(EventType: TEventType; const Msg: String);
268
269begin
270  // Do nothing, override in descendants
271end;
272
273procedure TCustomApplication.Log(EventType: TEventType; const Msg: String);
274
275begin
276  If (FEventLogFilter=[]) or (EventType in FEventLogFilter) then
277    DoLog(EventType,Msg);
278end;
279
280procedure TCustomApplication.Log(EventType: TEventType; const Fmt: String;
281  const Args: array of const);
282begin
283  try
284    Log(EventType, Format(Fmt, Args));
285  except
286    On E : Exception do
287      Log(etError,Format('Error formatting message "%s" with %d arguments: %s',[Fmt,Length(Args),E.Message]));
288  end
289end;
290
291constructor TCustomApplication.Create(AOwner: TComponent);
292begin
293  inherited Create(AOwner);
294  FOptionChar:='-';
295  FCaseSensitiveOptions:=True;
296  FStopOnException:=False;
297  FSingleInstanceClass := DefaultSingleInstanceClass;
298end;
299
300destructor TCustomApplication.Destroy;
301begin
302  inherited Destroy;
303end;
304
305procedure TCustomApplication.HandleException(Sender: TObject);
306begin
307  If Not (ExceptObject is Exception) then
308    SysUtils.showexception(ExceptObject,ExceptAddr)
309  else
310    begin
311    If Not Assigned(FOnexception) then
312      ShowException(Exception(ExceptObject))
313    else
314      FOnException(Sender,Exception(ExceptObject));
315    end;
316  If FStopOnException then
317    Terminate(ExceptionExitCode);
318end;
319
320
321procedure TCustomApplication.Initialize;
322begin
323  FTerminated:=False;
324  if FSingleInstanceEnabled then
325  begin
326    case SingleInstance.Start of
327      siClient:
328      begin
329        SingleInstance.ClientPostParams;
330        FTerminated:=True;
331      end;
332      siNotResponding:
333        FTerminated:=True;
334    end;
335  end;
336end;
337
338procedure TCustomApplication.Run;
339
340begin
341  Repeat
342    Try
343      DoRun;
344    except
345      HandleException(Self);
346    end;
347  Until FTerminated;
348end;
349
350procedure TCustomApplication.SetSingleInstanceClass(
351  const ASingleInstanceClass: TBaseSingleInstanceClass);
352begin
353  Assert((FSingleInstance = nil) and (ASingleInstanceClass <> nil));
354  FSingleInstanceClass := ASingleInstanceClass;
355end;
356
357procedure TCustomApplication.ShowException(E: Exception);
358
359begin
360  Sysutils.ShowException(E,ExceptAddr)
361end;
362
363procedure TCustomApplication.Terminate;
364begin
365  Terminate(ExitCode);
366end;
367
368procedure TCustomApplication.Terminate(AExitCode : Integer) ;
369
370begin
371  FTerminated:=True;
372  ExitCode:=AExitCode;
373end;
374
375function TCustomApplication.GetOptionAtIndex(AIndex : Integer; IsLong: Boolean): String;
376
377Var
378  P : Integer;
379  O : String;
380
381begin
382  Result:='';
383  If (AIndex=-1) then
384    Exit;
385  If IsLong then
386    begin // Long options have form --option=value
387    O:=Params[AIndex];
388    P:=Pos('=',O);
389   If (P=0) then
390      P:=Length(O);
391    Delete(O,1,P);
392    Result:=O;
393    end
394  else
395    begin // short options have form '-o value'
396    If (AIndex<ParamCount) then
397      if (Copy(Params[AIndex+1],1,1)<>OptionChar) then
398        Result:=Params[AIndex+1];
399    end;
400  end;
401
402
403function TCustomApplication.GetOptionValue(const S: String): String;
404begin
405  Result:=GetoptionValue(#255,S);
406end;
407
408function TCustomApplication.GetOptionValue(const C: Char; const S: String
409  ): String;
410
411Var
412  B : Boolean;
413  I : integer;
414
415begin
416  Result:='';
417  I:=FindOptionIndex(C,B);
418  If (I=-1) then
419    I:=FindOptionIndex(S,B);
420  If I<>-1 then
421    Result:=GetOptionAtIndex(I,B);
422end;
423
424function TCustomApplication.GetOptionValues(const C: Char; const S: String): TStringArray;
425
426Var
427  I,Cnt : Integer;
428  B : Boolean;
429
430begin
431  SetLength(Result,ParamCount);
432  Cnt:=0;
433  I:=-1;
434  Repeat
435    I:=FindOptionIndex(C,B,I);
436    If I<>-1 then
437      begin
438      Inc(Cnt);
439      Dec(I);
440      end;
441  Until I=-1;
442  Repeat
443    I:=FindOptionIndex(S,B,I);
444    If I<>-1 then
445      begin
446      Inc(Cnt);
447      Dec(I);
448      end;
449  Until I=-1;
450  SetLength(Result,Cnt);
451  Cnt:=0;
452  I:=-1;
453  Repeat
454    I:=FindOptionIndex(C,B,I);
455    If (I<>-1) then
456      begin
457      Result[Cnt]:=GetOptionAtIndex(I,False);
458      Inc(Cnt);
459      Dec(i);
460      end;
461  Until (I=-1);
462  I:=-1;
463  Repeat
464    I:=FindOptionIndex(S,B,I);
465    If I<>-1 then
466      begin
467      Result[Cnt]:=GetOptionAtIndex(I,True);
468      Inc(Cnt);
469      Dec(i);
470      end;
471  Until (I=-1);
472end;
473
474function TCustomApplication.HasOption(const S: String): Boolean;
475
476Var
477  B : Boolean;
478
479begin
480  Result:=FindOptionIndex(S,B)<>-1;
481end;
482
483function TCustomApplication.FindOptionIndex(const S: String;
484  var Longopt: Boolean; StartAt : Integer = -1): Integer;
485
486Var
487  SO,O : String;
488  I,P : Integer;
489
490begin
491  If Not CaseSensitiveOptions then
492    SO:=UpperCase(S)
493  else
494    SO:=S;
495  Result:=-1;
496  I:=StartAt;
497  if (I=-1) then
498    I:=ParamCount;
499  While (Result=-1) and (I>0) do
500    begin
501    O:=Params[i];
502    // - must be seen as an option value
503    If (Length(O)>1) and (O[1]=FOptionChar) then
504      begin
505      Delete(O,1,1);
506      LongOpt:=(Length(O)>0) and (O[1]=FOptionChar);
507      If LongOpt then
508        begin
509        Delete(O,1,1);
510        P:=Pos('=',O);
511        If (P<>0) then
512          O:=Copy(O,1,P-1);
513        end;
514      If Not CaseSensitiveOptions then
515        O:=UpperCase(O);
516      If (O=SO) then
517        Result:=i;
518      end;
519    Dec(i);
520    end;
521end;
522
523function TCustomApplication.HasOption(const C: Char; const S: String): Boolean;
524
525Var
526  B : Boolean;
527
528begin
529  Result:=(FindOptionIndex(C,B)<>-1) or (FindOptionIndex(S,B)<>-1);
530end;
531
532
533function TCustomApplication.CheckOptions(const ShortOptions: String;
534  const Longopts: TStrings; AllErrors: Boolean): String;
535
536begin
537  Result:=CheckOptions(ShortOptions,LongOpts,Nil,Nil,AllErrors);
538end;
539
540ResourceString
541  SErrInvalidOption = 'Invalid option at position %d: "%s"';
542  SErrNoOptionAllowed = 'Option at position %d does not allow an argument: %s';
543  SErrOptionNeeded = 'Option at position %d needs an argument : %s';
544
545function TCustomApplication.CheckOptions(const ShortOptions: String;
546  const Longopts: TStrings; Opts, NonOpts: TStrings; AllErrors: Boolean
547  ): String;
548
549Var
550  I,J,L,P : Integer;
551  O,OV,SO : String;
552  UsedArg,HaveArg : Boolean;
553
554  Function FindLongOpt(S : String) : boolean;
555
556  Var
557    I : integer;
558
559  begin
560    Result:=Assigned(LongOpts);
561    if Not Result then
562      exit;
563    If CaseSensitiveOptions then
564      begin
565      I:=LongOpts.Count-1;
566      While (I>=0) and (LongOpts[i]<>S) do
567        Dec(i);
568      end
569    else
570      begin
571      S:=UpperCase(S);
572      I:=LongOpts.Count-1;
573      While (I>=0) and (UpperCase(LongOpts[i])<>S) do
574        Dec(i);
575      end;
576    Result:=(I<>-1);
577  end;
578
579  Procedure AddToResult(Const Msg : string);
580
581  begin
582    If (Result<>'') then
583      Result:=Result+sLineBreak;
584    Result:=Result+Msg;
585  end;
586
587begin
588  If CaseSensitiveOptions then
589    SO:=Shortoptions
590  else
591    SO:=LowerCase(Shortoptions);
592  Result:='';
593  I:=1;
594  While (I<=ParamCount) and ((Result='') or AllErrors) do
595    begin
596    O:=Paramstr(I);
597    If (Length(O)=0) or (O[1]<>FOptionChar) then
598      begin
599      If Assigned(NonOpts) then
600        NonOpts.Add(O);
601      end
602    else
603      begin
604      If (Length(O)<2) then
605        AddToResult(Format(SErrInvalidOption,[i,O]))
606      else
607        begin
608        HaveArg:=False;
609        OV:='';
610        // Long option ?
611        If (O[2]=FOptionChar) then
612          begin
613          Delete(O,1,2);
614          J:=Pos('=',O);
615          If J<>0 then
616            begin
617            HaveArg:=true;
618            OV:=O;
619            Delete(OV,1,J);
620            O:=Copy(O,1,J-1);
621            end;
622          // Switch Option
623          If FindLongopt(O) then
624            begin
625            If HaveArg then
626              AddToResult(Format(SErrNoOptionAllowed,[I,O]));
627            end
628          else
629            begin // Required argument
630            If FindLongOpt(O+':') then
631              begin
632              If Not HaveArg then
633                AddToResult(Format(SErrOptionNeeded,[I,O]));
634              end
635            else
636              begin // Optional Argument.
637              If not FindLongOpt(O+'::') then
638                AddToResult(Format(SErrInvalidOption,[I,O]));
639              end;
640            end;
641          end
642        else // Short Option.
643          begin
644          HaveArg:=(I<ParamCount) and (Length(ParamStr(I+1))>0) and (ParamStr(I+1)[1]<>FOptionChar);
645          UsedArg:=False;
646          If Not CaseSensitiveOptions then
647            O:=LowerCase(O);
648          L:=Length(O);
649          J:=2;
650          While ((Result='') or AllErrors) and (J<=L) do
651            begin
652            P:=Pos(O[J],SO);
653            If (P=0) or (O[j]=':') then
654              AddToResult(Format(SErrInvalidOption,[I,O[J]]))
655            else
656              begin
657              If (P<Length(SO)) and (SO[P+1]=':') then
658                begin
659                // Required argument
660                If ((P+1)=Length(SO)) or (SO[P+2]<>':') Then
661                  If (J<L) or not haveArg then // Must be last in multi-opt !!
662                    AddToResult(Format(SErrOptionNeeded,[I,O[J]]));
663                O:=O[j]; // O is added to arguments.
664                UsedArg:=True;
665                end;
666              end;
667            Inc(J);
668            end;
669          HaveArg:=HaveArg and UsedArg;
670          If HaveArg then
671            begin
672            Inc(I); // Skip argument.
673            OV:=Paramstr(I);
674            end;
675          end;
676        If HaveArg and ((Result='') or AllErrors) then
677          If Assigned(Opts) then
678            Opts.Add(O+'='+OV);
679        end;
680      end;
681    Inc(I);
682    end;
683end;
684
685function TCustomApplication.CheckOptions(const ShortOptions: String;
686  const Longopts: array of string; Opts, NonOpts: TStrings; AllErrors: Boolean
687  ): String;
688Var
689  L : TStringList;
690  I : Integer;
691
692begin
693  L:=TStringList.Create;
694  Try
695    For I:=0 to High(LongOpts) do
696      L.Add(LongOpts[i]);
697    Result:=CheckOptions(ShortOptions,L,Opts,NonOpts,AllErrors);
698  Finally
699    L.Free;
700  end;
701end;
702
703function TCustomApplication.CheckOptions(const ShortOptions: String;
704  const LongOpts: array of string; AllErrors: Boolean): String;
705
706Var
707  L : TStringList;
708  I : Integer;
709
710begin
711  L:=TStringList.Create;
712  Try
713    For I:=0 to High(LongOpts) do
714      L.Add(LongOpts[i]);
715    Result:=CheckOptions(ShortOptions,L,AllErrors);
716  Finally
717    L.Free;
718  end;
719end;
720
721function TCustomApplication.CheckOptions(const ShortOptions: String;
722  const LongOpts: String; AllErrors: Boolean): String;
723
724Const
725  SepChars = ' '#10#13#9;
726
727Var
728  L : TStringList;
729  Len,I,J : Integer;
730
731begin
732  L:=TStringList.Create;
733  Try
734    I:=1;
735    Len:=Length(LongOpts);
736    While I<=Len do
737      begin
738      While Isdelimiter(SepChars,LongOpts,I) do
739        Inc(I);
740      J:=I;
741      While (J<=Len) and Not IsDelimiter(SepChars,LongOpts,J) do
742        Inc(J);
743      If (I<=J) then
744        L.Add(Copy(LongOpts,I,(J-I)));
745      I:=J+1;
746      end;
747    Result:=CheckOptions(Shortoptions,L,AllErrors);
748  Finally
749    L.Free;
750  end;
751end;
752
753function TCustomApplication.GetNonOptions(const ShortOptions: String;
754  const Longopts: array of string): TStringArray;
755
756Var
757  NO : TStrings;
758  I : Integer;
759
760begin
761  No:=TStringList.Create;
762  try
763    GetNonOptions(ShortOptions,LongOpts,No);
764    SetLength(Result,NO.Count);
765    For I:=0 to NO.Count-1 do
766      Result[I]:=NO[i];
767  finally
768    NO.Free;
769  end;
770end;
771
772procedure TCustomApplication.GetNonOptions(const ShortOptions: String;
773  const Longopts: array of string; NonOptions: TStrings);
774
775Var
776  S : String;
777
778begin
779  S:=CheckOptions(ShortOptions,LongOpts,Nil,NonOptions,true);
780  if (S<>'') then
781    Raise EListError.Create(S);
782end;
783
784end.
785