1{
2 *****************************************************************************
3  This file is part of LazUtils.
4
5  See the file COPYING.modifiedLGPL.txt, included in this distribution,
6  for details about the license.
7 *****************************************************************************
8
9 Initial Revision  : Tue Dec 06 09:00:00 CET 2005
10}
11
12unit UTF8Process;
13
14{$mode objfpc}{$H+}
15
16interface
17
18uses
19  Classes, SysUtils, Process,
20  FileUtil, LazFileUtils, LazUTF8, LazUtilsStrConsts;
21
22  {$IF DEFINED(MSWINDOWS) AND NOT DECLARED(poDetached)} // we need to work around the poNoConsole->poDetached change
23    // more info: issue #32055, #35991; FPC r45228, https://forum.lazarus.freepascal.org/index.php/topic,49631.0
24    {$DEFINE UseTProcessW}
25  {$ENDIF}
26
27{ TProcessUTF8 }
28
29{$IFDEF UseTProcessW}
30{$Optimization -ORDERFIELDS }
31const
32  SNoCommandLine        = 'Cannot execute empty command-line';
33  SErrCannotExecute     = 'Failed to execute %s : %d';
34type
35  TProcessUTF8 = class(TProcess)
36  protected
37    procedure SetProcessHandle(aProcessHandle : THandle);
38    procedure SetThreadHandle(aThreadHandle : THandle);
39    procedure SetProcessID(aProcessID : Integer);
40  public
41    procedure Execute; override;
42    procedure ParseCmdLine(const CmdLine: string; ReadBackslash: boolean = false);
43  end;
44
45{$ELSE}
46
47type
48  TProcessUTF8 = class(TProcess)
49  public
50    procedure ParseCmdLine(const CmdLine: string; ReadBackslash: boolean = false);
51  end;
52{$ENDIF}
53
54procedure RunCmdFromPath(const ProgramFilename, CmdLineParameters: string);
55function FindFilenameOfCmd(ProgramFilename: string): string;
56
57function GetSystemThreadCount: integer; // guess number of cores
58
59procedure Register;
60
61implementation
62
63{$IF defined(windows)}
64uses Windows
65  {$IFDEF UseTProcessW}
66  ,pipes
67  {$ENDIF}
68;
69{$ELSEIF defined(freebsd) or defined(darwin)}
70uses ctypes, sysctl;
71{$ELSEIF defined(linux)}
72{$linklib c}
73uses ctypes;
74{$ENDIF}
75
76{$IFDEF Linux}
77const _SC_NPROCESSORS_ONLN = 83;
78function sysconf(i: cint): clong; cdecl; external name 'sysconf';
79{$ENDIF}
80
81function GetSystemThreadCount: integer;
82// returns a good default for the number of threads on this system
83{$IF defined(windows)}
84//returns total number of processors available to system including logical hyperthreaded processors
85var
86  SystemInfo: SYSTEM_INFO;
87  {$IFnDEF WinCE}
88  i: Integer;
89  ProcessAffinityMask, SystemAffinityMask: DWORD_PTR;
90  Mask: DWORD;
91  {$ENDIF}
92begin
93  {$IFnDEF WinCE}
94  if GetProcessAffinityMask(GetCurrentProcess, ProcessAffinityMask{%H-}, SystemAffinityMask{%H-})
95  then begin
96    Result := 0;
97    for i := 0 to 31 do begin
98      Mask := DWord(1) shl i;
99      if (ProcessAffinityMask and Mask)<>0 then
100        inc(Result);
101    end;
102    exit;
103  end;
104  {$ENDIF}
105  //can't get the affinity mask so we just report the total number of processors
106  GetSystemInfo(SystemInfo{%H-});
107  Result := SystemInfo.dwNumberOfProcessors;
108end;
109{$ELSEIF defined(UNTESTEDsolaris)}
110  begin
111    t = sysconf(_SC_NPROC_ONLN);
112  end;
113{$ELSEIF defined(freebsd) or defined(darwin)}
114var
115  mib: array[0..1] of cint;
116  len: cint;
117  t: cint;
118begin
119  mib[0] := CTL_HW;
120  mib[1] := HW_NCPU;
121  len := sizeof(t);
122  {$if FPC_FULLVERSION >= 30101}
123  fpsysctl(@mib, 2, @t, @len, Nil, 0);
124  {$else}
125  fpsysctl(pchar(@mib), 2, @t, @len, Nil, 0);
126  {$endif}
127  Result:=t;
128end;
129{$ELSEIF defined(linux)}
130  begin
131    Result:=sysconf(_SC_NPROCESSORS_ONLN);
132  end;
133
134{$ELSE}
135  begin
136    Result:=1;
137  end;
138{$ENDIF}
139
140function FindFilenameOfCmd(ProgramFilename: string): string;
141begin
142  Result:=TrimFilename(ProgramFilename);
143  if not FilenameIsAbsolute(Result) then begin
144    if Pos(PathDelim,Result)>0 then begin
145      // with sub directory => relative to current directory
146      Result:=CleanAndExpandFilename(Result);
147    end else begin
148      // search in PATH
149      Result:=FindDefaultExecutablePath(Result);
150    end;
151  end;
152  if (Result<>'') and not FileExistsUTF8(Result) then
153    Result:='';
154end;
155
156// Runs a short command which should point to an executable in
157// the environment PATH
158// For example: ProgramFilename='ls' CmdLineParameters='-l /home'
159// Will locate and execute the file '/bin/ls'
160// If the command isn't found, an exception will be raised
161procedure RunCmdFromPath(const ProgramFilename, CmdLineParameters: string);
162var
163  NewProgramFilename: String;
164  BrowserProcess: TProcessUTF8;
165begin
166  NewProgramFilename:=FindFilenameOfCmd(ProgramFilename);
167
168  if NewProgramFilename='' then
169    raise EFOpenError.Create(Format(lrsProgramFileNotFound, [ProgramFilename]));
170  if not FileIsExecutable(NewProgramFilename) then
171    raise EFOpenError.Create(Format(lrsCanNotExecute, [NewProgramFilename]));
172
173  // run
174  BrowserProcess := TProcessUTF8.Create(nil);
175  try
176    BrowserProcess.InheritHandles:=false;
177    // Encloses the executable with "" if its name has spaces
178    if Pos(' ',NewProgramFilename)>0 then
179      NewProgramFilename:='"'+NewProgramFilename+'"';
180
181    {$Push}
182    {$WARN SYMBOL_DEPRECATED OFF}
183    BrowserProcess.CommandLine := NewProgramFilename;
184    if CmdLineParameters<>'' then
185      BrowserProcess.CommandLine := BrowserProcess.CommandLine + ' ' + CmdLineParameters;
186    {$Pop}
187    BrowserProcess.Execute;
188  finally
189    BrowserProcess.Free;
190  end;
191end;
192
193procedure Register;
194begin
195  RegisterComponents('System',[TProcessUTF8]);
196end;
197
198{$IFDEF UseTProcessW}
199Const
200  PriorityConstants : Array [TProcessPriority] of Cardinal =
201                      (HIGH_PRIORITY_CLASS,IDLE_PRIORITY_CLASS,
202                       NORMAL_PRIORITY_CLASS,REALTIME_PRIORITY_CLASS
203                       {$if (FPC_FULLVERSION >= 30200) and not defined(WinCE)}
204                       ,BELOW_NORMAL_PRIORITY_CLASS,ABOVE_NORMAL_PRIORITY_CLASS
205                       {$endif}
206                       );
207
208function WStrAsUniquePWideChar(var s: UnicodeString): PWideChar; inline;
209begin
210  UniqueString(s);
211  if s<>'' then
212    Result:=PWideChar(s)
213  else
214    Result:=nil;
215end;
216
217Function GetStartupFlags (P : TProcessUTF8): Cardinal;
218
219begin
220  Result:=0;
221  if poUsePipes in P.Options then
222     Result:=Result or Startf_UseStdHandles;
223  if suoUseShowWindow in P.StartupOptions then
224    Result:=Result or startf_USESHOWWINDOW;
225  if suoUSESIZE in P.StartupOptions then
226    Result:=Result or startf_usesize;
227  if suoUsePosition in P.StartupOptions then
228    Result:=Result or startf_USEPOSITION;
229  if suoUSECOUNTCHARS in P.Startupoptions then
230    Result:=Result or startf_usecountchars;
231  if suoUsefIllAttribute in P.StartupOptions then
232    Result:=Result or startf_USEFILLATTRIBUTE;
233end;
234
235Function GetCreationFlags(P : TProcessUTF8) : Cardinal;
236
237begin
238  Result:=CREATE_UNICODE_ENVIRONMENT;
239  {$IF DECLARED(poDetached)}
240  if poNoConsole in P.Options then
241    Result:=Result or CREATE_NO_WINDOW;
242  if poDetached in P.Options then
243    Result:=Result or Detached_Process;
244  {$ELSE}
245  if poNoConsole in P.Options then
246    Result:=Result or Detached_Process;
247  {$ENDIF}
248  if poNewConsole in P.Options then
249    Result:=Result or Create_new_console;
250  if poNewProcessGroup in P.Options then
251    Result:=Result or CREATE_NEW_PROCESS_GROUP;
252  If poRunSuspended in P.Options Then
253    Result:=Result or Create_Suspended;
254  if poDebugProcess in P.Options Then
255    Result:=Result or DEBUG_PROCESS;
256  if poDebugOnlyThisProcess in P.Options Then
257    Result:=Result or DEBUG_ONLY_THIS_PROCESS;
258  if poDefaultErrorMode in P.Options Then
259    Result:=Result or CREATE_DEFAULT_ERROR_MODE;
260  result:=result or PriorityConstants[P.Priority];
261end;
262
263Function MaybeQuote(Const S : String) : String;
264
265begin
266  If (Pos(' ',S)<>0) then
267    Result:='"'+S+'"'
268  else
269     Result:=S;
270end;
271
272Function MaybeQuoteIfNotQuoted(Const S : String) : String;
273
274begin
275  If (Pos(' ',S)<>0) and (pos('"',S)=0) then
276    Result:='"'+S+'"'
277  else
278     Result:=S;
279end;
280
281Function StringsToWChars(List : TStrings): pointer;
282
283var
284  EnvBlock: UnicodeString;
285  I: Integer;
286
287begin
288  EnvBlock := '';
289  For I:=0 to List.Count-1 do
290    EnvBlock := EnvBlock + UTF8Decode(List[i]) + #0;
291  EnvBlock := EnvBlock + #0;
292  GetMem(Result, Length(EnvBlock)*2);
293  CopyMemory(Result, @EnvBlock[1], Length(EnvBlock)*2);
294end;
295
296Procedure InitProcessAttributes(Out PA : TSecurityAttributes);
297
298begin
299  FillChar(PA{%H-},SizeOf(PA),0);
300  PA.nLength := SizeOf(PA);
301end;
302
303Procedure InitThreadAttributes(Out TA : TSecurityAttributes);
304
305begin
306  FillChar(TA{%H-},SizeOf(TA),0);
307  TA.nLength := SizeOf(TA);
308end;
309
310Procedure InitStartupInfo(P : TProcessUTF8; Out SI : STARTUPINFOW);
311
312Const
313  SWC : Array [TShowWindowOptions] of Cardinal =
314             (0,SW_HIDE,SW_Maximize,SW_Minimize,SW_Restore,SW_Show,
315             SW_ShowDefault,SW_ShowMaximized,SW_ShowMinimized,
316               SW_showMinNOActive,SW_ShowNA,SW_ShowNoActivate,SW_ShowNormal);
317
318begin
319  FillChar(SI{%H-},SizeOf(SI),0);
320  SI.dwFlags:=GetStartupFlags(P);
321  if P.ShowWindow<>swoNone then
322   SI.dwFlags:=SI.dwFlags or Startf_UseShowWindow
323  else
324    SI.dwFlags:=SI.dwFlags and not Startf_UseShowWindow;
325  SI.wShowWindow:=SWC[P.ShowWindow];
326  if (poUsePipes in P.Options) then
327    begin
328    SI.dwFlags:=SI.dwFlags or Startf_UseStdHandles;
329    end;
330  if P.FillAttribute<>0 then
331    begin
332    SI.dwFlags:=SI.dwFlags or Startf_UseFillAttribute;
333    SI.dwFillAttribute:=P.FillAttribute;
334    end;
335   SI.dwXCountChars:=P.WindowColumns;
336   SI.dwYCountChars:=P.WindowRows;
337   SI.dwYsize:=P.WindowHeight;
338   SI.dwXsize:=P.WindowWidth;
339   SI.dwy:=P.WindowTop;
340   SI.dwX:=P.WindowLeft;
341end;
342
343{ The handles that are to be passed to the child process must be
344  inheritable. On the other hand, only non-inheritable handles
345  allow the sending of EOF when the write-end is closed. This
346  function is used to duplicate the child process's ends of the
347  handles into inheritable ones, leaving the parent-side handles
348  non-inheritable.
349}
350function DuplicateHandleFP(var handle: THandle): Boolean;
351
352var
353  oldHandle: THandle;
354begin
355  oldHandle := handle;
356  Result := DuplicateHandle
357  ( GetCurrentProcess(),
358    oldHandle,
359    GetCurrentProcess(),
360    @handle,
361    0,
362    true,
363    DUPLICATE_SAME_ACCESS
364  );
365  if Result then
366    Result := CloseHandle(oldHandle);
367end;
368
369
370Procedure CreatePipes(Var HI,HO,HE : Thandle; Var SI : TStartupInfoW; CE : Boolean; APipeBufferSize : Cardinal);
371
372begin
373  CreatePipeHandles(SI.hStdInput,HI, APipeBufferSize);
374  DuplicateHandleFP(SI.hStdInput);
375  CreatePipeHandles(HO,Si.hStdOutput, APipeBufferSize);
376  DuplicateHandleFP(   Si.hStdOutput);
377  if CE then begin
378    CreatePipeHandles(HE,SI.hStdError, APipeBufferSize);
379    DuplicateHandleFP(   SI.hStdError);
380    end
381  else
382    begin
383    SI.hStdError:=SI.hStdOutput;
384    HE:=HO;
385    end;
386end;
387
388{ TProcessUTF8 }
389
390type
391  PHandle = ^THandle;
392
393procedure TProcessUTF8.SetProcessHandle(aProcessHandle: THandle);
394var
395  P: PHandle;
396begin
397  P := @Self.ProcessHandle;
398  P^ := aProcessHandle;
399  if aProcessHandle<>ProcessHandle then
400    raise Exception.Create('TProcessUTF8.SetProcessHandle failed');
401end;
402
403procedure TProcessUTF8.SetThreadHandle(aThreadHandle: THandle);
404var
405  P: PHandle;
406begin
407  P := @Self.ThreadHandle;
408  P^ := aThreadHandle;
409  if aThreadHandle<>ThreadHandle then
410    raise Exception.Create('TProcessUTF8.SetThreadHandle failed');
411end;
412
413procedure TProcessUTF8.SetProcessID(aProcessID: Integer);
414var
415  P: PInteger;
416begin
417  P := @Self.ProcessID;
418  P^ := aProcessID;
419  if aProcessID<>ProcessID then
420    raise Exception.Create('TProcessUTF8.SetProcessID failed');
421end;
422
423procedure TProcessUTF8.Execute;
424Var
425  i : Integer;
426  WName,WDir,WCommandLine : UnicodeString;
427  PWName,PWDir,PWCommandLine : PWideChar;
428  FEnv: pointer;
429  FCreationFlags : Cardinal;
430  FProcessAttributes : TSecurityAttributes;
431  FThreadAttributes : TSecurityAttributes;
432  FProcessInformation : TProcessInformation;
433  FStartupInfo : STARTUPINFOW;
434  HI,HO,HE : THandle;
435  Cmd : String;
436
437begin
438  WName:='';
439  WCommandLine:='';
440  WDir:='';
441
442  if (ApplicationName{%H-}='') and (CommandLine{%H-}='') and (Executable='') then
443    Raise EProcess.Create(SNoCommandline);
444  if (ApplicationName{%H-}<>'') then
445    begin
446    WName:=UTF8Decode(ApplicationName{%H-});
447    WCommandLine:=UTF8Decode(CommandLine{%H-});
448    end
449  else If (CommandLine{%H-}<>'') then
450    WCommandLine:=UTF8Decode(CommandLine{%H-})
451  else if (Executable<>'') then
452    begin
453    Cmd:=MaybeQuoteIfNotQuoted(Executable);
454    For I:=0 to Parameters.Count-1 do
455      Cmd:=Cmd+' '+MaybeQuoteIfNotQuoted(Parameters[i]);
456    WCommandLine:=UTF8Decode(Cmd);
457    end;
458  If CurrentDirectory<>'' then
459    WDir:=UTF8Decode(CurrentDirectory);
460  if Environment.Count<>0 then
461    FEnv:=StringsToWChars(Environment)
462  else
463    FEnv:=Nil;
464  Try
465    FCreationFlags:=GetCreationFlags(Self);
466    InitProcessAttributes(FProcessAttributes);
467    InitThreadAttributes(FThreadAttributes);
468    InitStartupInfo(Self,FStartupInfo);
469    If poUsePipes in Options then
470      CreatePipes(HI{%H-},HO{%H-},HE{%H-},FStartupInfo,Not(poStdErrToOutPut in Options), PipeBufferSize);
471    Try
472      // Beware: CreateProcess can alter the strings
473      // Beware: nil is not the same as a pointer to a #0
474      PWName:=WStrAsUniquePWideChar(WName);
475      PWCommandLine:=WStrAsUniquePWideChar(WCommandLine);
476      PWDir:=WStrAsUniquePWideChar(WDir);
477
478      If Not CreateProcessW (PWName,PWCommandLine,@FProcessAttributes,@FThreadAttributes,
479                   InheritHandles,FCreationFlags,FEnv,PWDir,FStartupInfo,
480                   fProcessInformation{%H-}) then
481        Raise EProcess.CreateFmt(SErrCannotExecute,[CommandLine{%H-},GetLastError]);
482      SetProcessHandle(FProcessInformation.hProcess);
483      SetThreadHandle(FProcessInformation.hThread);
484      SetProcessID(FProcessINformation.dwProcessID);
485    Finally
486      if POUsePipes in Options then
487        begin
488        FileClose(FStartupInfo.hStdInput);
489        FileClose(FStartupInfo.hStdOutput);
490        if Not (poStdErrToOutPut in Options) then
491          FileClose(FStartupInfo.hStdError);
492        CreateStreams(HI,HO,HE);
493        end;
494    end;
495    FRunning:=True;
496  Finally
497    If FEnv<>Nil then
498      FreeMem(FEnv);
499  end;
500  if not (csDesigning in ComponentState) and // This would hang the IDE !
501     (poWaitOnExit in Options) and
502      not (poRunSuspended in Options) then
503    WaitOnExit;
504end;
505{$ENDIF}
506
507procedure TProcessUTF8.ParseCmdLine(const CmdLine: string; ReadBackslash: boolean);
508var
509  List: TStringList;
510begin
511  List:=TStringList.Create;
512  try
513    SplitCmdLineParams(CmdLine, List, ReadBackslash);
514    if List.Count>0 then begin
515      Executable:=List[0];
516      List.Delete(0);
517    end else begin
518      Executable:='';
519    end;
520    Parameters.Assign(List);
521  finally
522    List.Free;
523  end;
524end;
525
526end.
527