{ ***************************************************************************** This file is part of LazUtils. See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** Initial Revision : Tue Dec 06 09:00:00 CET 2005 } unit UTF8Process; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Process, FileUtil, LazFileUtils, LazUTF8, LazUtilsStrConsts; {$IF DEFINED(MSWINDOWS) AND NOT DECLARED(poDetached)} // we need to work around the poNoConsole->poDetached change // more info: issue #32055, #35991; FPC r45228, https://forum.lazarus.freepascal.org/index.php/topic,49631.0 {$DEFINE UseTProcessW} {$ENDIF} { TProcessUTF8 } {$IFDEF UseTProcessW} {$Optimization -ORDERFIELDS } const SNoCommandLine = 'Cannot execute empty command-line'; SErrCannotExecute = 'Failed to execute %s : %d'; type TProcessUTF8 = class(TProcess) protected procedure SetProcessHandle(aProcessHandle : THandle); procedure SetThreadHandle(aThreadHandle : THandle); procedure SetProcessID(aProcessID : Integer); public procedure Execute; override; procedure ParseCmdLine(const CmdLine: string; ReadBackslash: boolean = false); end; {$ELSE} type TProcessUTF8 = class(TProcess) public procedure ParseCmdLine(const CmdLine: string; ReadBackslash: boolean = false); end; {$ENDIF} procedure RunCmdFromPath(const ProgramFilename, CmdLineParameters: string); function FindFilenameOfCmd(ProgramFilename: string): string; function GetSystemThreadCount: integer; // guess number of cores procedure Register; implementation {$IF defined(windows)} uses Windows {$IFDEF UseTProcessW} ,pipes {$ENDIF} ; {$ELSEIF defined(freebsd) or defined(darwin)} uses ctypes, sysctl; {$ELSEIF defined(linux)} {$linklib c} uses ctypes; {$ENDIF} {$IFDEF Linux} const _SC_NPROCESSORS_ONLN = 83; function sysconf(i: cint): clong; cdecl; external name 'sysconf'; {$ENDIF} function GetSystemThreadCount: integer; // returns a good default for the number of threads on this system {$IF defined(windows)} //returns total number of processors available to system including logical hyperthreaded processors var SystemInfo: SYSTEM_INFO; {$IFnDEF WinCE} i: Integer; ProcessAffinityMask, SystemAffinityMask: DWORD_PTR; Mask: DWORD; {$ENDIF} begin {$IFnDEF WinCE} if GetProcessAffinityMask(GetCurrentProcess, ProcessAffinityMask{%H-}, SystemAffinityMask{%H-}) then begin Result := 0; for i := 0 to 31 do begin Mask := DWord(1) shl i; if (ProcessAffinityMask and Mask)<>0 then inc(Result); end; exit; end; {$ENDIF} //can't get the affinity mask so we just report the total number of processors GetSystemInfo(SystemInfo{%H-}); Result := SystemInfo.dwNumberOfProcessors; end; {$ELSEIF defined(UNTESTEDsolaris)} begin t = sysconf(_SC_NPROC_ONLN); end; {$ELSEIF defined(freebsd) or defined(darwin)} var mib: array[0..1] of cint; len: cint; t: cint; begin mib[0] := CTL_HW; mib[1] := HW_NCPU; len := sizeof(t); {$if FPC_FULLVERSION >= 30101} fpsysctl(@mib, 2, @t, @len, Nil, 0); {$else} fpsysctl(pchar(@mib), 2, @t, @len, Nil, 0); {$endif} Result:=t; end; {$ELSEIF defined(linux)} begin Result:=sysconf(_SC_NPROCESSORS_ONLN); end; {$ELSE} begin Result:=1; end; {$ENDIF} function FindFilenameOfCmd(ProgramFilename: string): string; begin Result:=TrimFilename(ProgramFilename); if not FilenameIsAbsolute(Result) then begin if Pos(PathDelim,Result)>0 then begin // with sub directory => relative to current directory Result:=CleanAndExpandFilename(Result); end else begin // search in PATH Result:=FindDefaultExecutablePath(Result); end; end; if (Result<>'') and not FileExistsUTF8(Result) then Result:=''; end; // Runs a short command which should point to an executable in the environment PATH // For example: ProgramFilename='ls' CmdLineParameters='-l /home' // Will locate and execute the file '/bin/ls' // If the command isn't found, an exception will be raised procedure RunCmdFromPath(const ProgramFilename, CmdLineParameters: string); var NewProgramFilename: String; BrowserProcess: TProcessUTF8; begin NewProgramFilename:=FindFilenameOfCmd(ProgramFilename); if NewProgramFilename='' then raise EFOpenError.Create(Format(lrsProgramFileNotFound, [ProgramFilename])); if not FileIsExecutable(NewProgramFilename) then raise EFOpenError.Create(Format(lrsCanNotExecute, [NewProgramFilename])); // run BrowserProcess := TProcessUTF8.Create(nil); try BrowserProcess.InheritHandles:=false; BrowserProcess.Options := [poWaitOnExit]; // Prevent a zombie process. // Encloses the executable with "" if its name has spaces if Pos(' ',NewProgramFilename)>0 then NewProgramFilename:='"'+NewProgramFilename+'"'; {$Push} {$WARN SYMBOL_DEPRECATED OFF} BrowserProcess.CommandLine := NewProgramFilename; if CmdLineParameters<>'' then BrowserProcess.CommandLine := BrowserProcess.CommandLine + ' ' + CmdLineParameters; {$Pop} BrowserProcess.Execute; finally BrowserProcess.Free; end; end; procedure Register; begin RegisterComponents('System',[TProcessUTF8]); end; {$IFDEF UseTProcessW} Const PriorityConstants : Array [TProcessPriority] of Cardinal = (HIGH_PRIORITY_CLASS,IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS,REALTIME_PRIORITY_CLASS {$if (FPC_FULLVERSION >= 30200) and not defined(WinCE)} ,BELOW_NORMAL_PRIORITY_CLASS,ABOVE_NORMAL_PRIORITY_CLASS {$endif} ); function WStrAsUniquePWideChar(var s: UnicodeString): PWideChar; inline; begin UniqueString(s); if s<>'' then Result:=PWideChar(s) else Result:=nil; end; Function GetStartupFlags (P : TProcessUTF8): Cardinal; begin Result:=0; if poUsePipes in P.Options then Result:=Result or Startf_UseStdHandles; if suoUseShowWindow in P.StartupOptions then Result:=Result or startf_USESHOWWINDOW; if suoUSESIZE in P.StartupOptions then Result:=Result or startf_usesize; if suoUsePosition in P.StartupOptions then Result:=Result or startf_USEPOSITION; if suoUSECOUNTCHARS in P.Startupoptions then Result:=Result or startf_usecountchars; if suoUsefIllAttribute in P.StartupOptions then Result:=Result or startf_USEFILLATTRIBUTE; end; Function GetCreationFlags(P : TProcessUTF8) : Cardinal; begin Result:=CREATE_UNICODE_ENVIRONMENT; {$IF DECLARED(poDetached)} if poNoConsole in P.Options then Result:=Result or CREATE_NO_WINDOW; if poDetached in P.Options then Result:=Result or Detached_Process; {$ELSE} if poNoConsole in P.Options then Result:=Result or Detached_Process; {$ENDIF} if poNewConsole in P.Options then Result:=Result or Create_new_console; if poNewProcessGroup in P.Options then Result:=Result or CREATE_NEW_PROCESS_GROUP; If poRunSuspended in P.Options Then Result:=Result or Create_Suspended; if poDebugProcess in P.Options Then Result:=Result or DEBUG_PROCESS; if poDebugOnlyThisProcess in P.Options Then Result:=Result or DEBUG_ONLY_THIS_PROCESS; if poDefaultErrorMode in P.Options Then Result:=Result or CREATE_DEFAULT_ERROR_MODE; result:=result or PriorityConstants[P.Priority]; end; Function MaybeQuote(Const S : String) : String; begin If (Pos(' ',S)<>0) then Result:='"'+S+'"' else Result:=S; end; Function MaybeQuoteIfNotQuoted(Const S : String) : String; begin If (Pos(' ',S)<>0) and (pos('"',S)=0) then Result:='"'+S+'"' else Result:=S; end; Function StringsToWChars(List : TStrings): pointer; var EnvBlock: UnicodeString; I: Integer; begin EnvBlock := ''; For I:=0 to List.Count-1 do EnvBlock := EnvBlock + UTF8Decode(List[i]) + #0; EnvBlock := EnvBlock + #0; GetMem(Result, Length(EnvBlock)*2); CopyMemory(Result, @EnvBlock[1], Length(EnvBlock)*2); end; Procedure InitProcessAttributes(Out PA : TSecurityAttributes); begin FillChar(PA{%H-},SizeOf(PA),0); PA.nLength := SizeOf(PA); end; Procedure InitThreadAttributes(Out TA : TSecurityAttributes); begin FillChar(TA{%H-},SizeOf(TA),0); TA.nLength := SizeOf(TA); end; Procedure InitStartupInfo(P : TProcessUTF8; Out SI : STARTUPINFOW); Const SWC : Array [TShowWindowOptions] of Cardinal = (0,SW_HIDE,SW_Maximize,SW_Minimize,SW_Restore,SW_Show, SW_ShowDefault,SW_ShowMaximized,SW_ShowMinimized, SW_showMinNOActive,SW_ShowNA,SW_ShowNoActivate,SW_ShowNormal); begin FillChar(SI{%H-},SizeOf(SI),0); SI.dwFlags:=GetStartupFlags(P); if P.ShowWindow<>swoNone then SI.dwFlags:=SI.dwFlags or Startf_UseShowWindow else SI.dwFlags:=SI.dwFlags and not Startf_UseShowWindow; SI.wShowWindow:=SWC[P.ShowWindow]; if (poUsePipes in P.Options) then begin SI.dwFlags:=SI.dwFlags or Startf_UseStdHandles; end; if P.FillAttribute<>0 then begin SI.dwFlags:=SI.dwFlags or Startf_UseFillAttribute; SI.dwFillAttribute:=P.FillAttribute; end; SI.dwXCountChars:=P.WindowColumns; SI.dwYCountChars:=P.WindowRows; SI.dwYsize:=P.WindowHeight; SI.dwXsize:=P.WindowWidth; SI.dwy:=P.WindowTop; SI.dwX:=P.WindowLeft; end; { The handles that are to be passed to the child process must be inheritable. On the other hand, only non-inheritable handles allow the sending of EOF when the write-end is closed. This function is used to duplicate the child process's ends of the handles into inheritable ones, leaving the parent-side handles non-inheritable. } function DuplicateHandleFP(var handle: THandle): Boolean; var oldHandle: THandle; begin oldHandle := handle; Result := DuplicateHandle ( GetCurrentProcess(), oldHandle, GetCurrentProcess(), @handle, 0, true, DUPLICATE_SAME_ACCESS ); if Result then Result := CloseHandle(oldHandle); end; Procedure CreatePipes(Var HI,HO,HE : Thandle; Var SI : TStartupInfoW; CE : Boolean; APipeBufferSize : Cardinal); begin CreatePipeHandles(SI.hStdInput,HI, APipeBufferSize); DuplicateHandleFP(SI.hStdInput); CreatePipeHandles(HO,Si.hStdOutput, APipeBufferSize); DuplicateHandleFP( Si.hStdOutput); if CE then begin CreatePipeHandles(HE,SI.hStdError, APipeBufferSize); DuplicateHandleFP( SI.hStdError); end else begin SI.hStdError:=SI.hStdOutput; HE:=HO; end; end; { TProcessUTF8 } type PHandle = ^THandle; procedure TProcessUTF8.SetProcessHandle(aProcessHandle: THandle); var P: PHandle; begin P := @Self.ProcessHandle; P^ := aProcessHandle; if aProcessHandle<>ProcessHandle then raise Exception.Create('TProcessUTF8.SetProcessHandle failed'); end; procedure TProcessUTF8.SetThreadHandle(aThreadHandle: THandle); var P: PHandle; begin P := @Self.ThreadHandle; P^ := aThreadHandle; if aThreadHandle<>ThreadHandle then raise Exception.Create('TProcessUTF8.SetThreadHandle failed'); end; procedure TProcessUTF8.SetProcessID(aProcessID: Integer); var P: PInteger; begin P := @Self.ProcessID; P^ := aProcessID; if aProcessID<>ProcessID then raise Exception.Create('TProcessUTF8.SetProcessID failed'); end; procedure TProcessUTF8.Execute; Var i : Integer; WName,WDir,WCommandLine : UnicodeString; PWName,PWDir,PWCommandLine : PWideChar; FEnv: pointer; FCreationFlags : Cardinal; FProcessAttributes : TSecurityAttributes; FThreadAttributes : TSecurityAttributes; FProcessInformation : TProcessInformation; FStartupInfo : STARTUPINFOW; HI,HO,HE : THandle; Cmd : String; begin WName:=''; WCommandLine:=''; WDir:=''; if (ApplicationName{%H-}='') and (CommandLine{%H-}='') and (Executable='') then Raise EProcess.Create(SNoCommandline); if (ApplicationName{%H-}<>'') then begin WName:=UTF8Decode(ApplicationName{%H-}); WCommandLine:=UTF8Decode(CommandLine{%H-}); end else If (CommandLine{%H-}<>'') then WCommandLine:=UTF8Decode(CommandLine{%H-}) else if (Executable<>'') then begin Cmd:=MaybeQuoteIfNotQuoted(Executable); For I:=0 to Parameters.Count-1 do Cmd:=Cmd+' '+MaybeQuoteIfNotQuoted(Parameters[i]); WCommandLine:=UTF8Decode(Cmd); end; If CurrentDirectory<>'' then WDir:=UTF8Decode(CurrentDirectory); if Environment.Count<>0 then FEnv:=StringsToWChars(Environment) else FEnv:=Nil; Try FCreationFlags:=GetCreationFlags(Self); InitProcessAttributes(FProcessAttributes); InitThreadAttributes(FThreadAttributes); InitStartupInfo(Self,FStartupInfo); If poUsePipes in Options then CreatePipes(HI{%H-},HO{%H-},HE{%H-},FStartupInfo,Not(poStdErrToOutPut in Options), PipeBufferSize); Try // Beware: CreateProcess can alter the strings // Beware: nil is not the same as a pointer to a #0 PWName:=WStrAsUniquePWideChar(WName); PWCommandLine:=WStrAsUniquePWideChar(WCommandLine); PWDir:=WStrAsUniquePWideChar(WDir); If Not CreateProcessW (PWName,PWCommandLine,@FProcessAttributes,@FThreadAttributes, InheritHandles,FCreationFlags,FEnv,PWDir,FStartupInfo, fProcessInformation{%H-}) then Raise EProcess.CreateFmt(SErrCannotExecute,[CommandLine{%H-},GetLastError]); SetProcessHandle(FProcessInformation.hProcess); SetThreadHandle(FProcessInformation.hThread); SetProcessID(FProcessINformation.dwProcessID); Finally if POUsePipes in Options then begin FileClose(FStartupInfo.hStdInput); FileClose(FStartupInfo.hStdOutput); if Not (poStdErrToOutPut in Options) then FileClose(FStartupInfo.hStdError); CreateStreams(HI,HO,HE); end; end; FRunning:=True; Finally If FEnv<>Nil then FreeMem(FEnv); end; if not (csDesigning in ComponentState) and // This would hang the IDE ! (poWaitOnExit in Options) and not (poRunSuspended in Options) then WaitOnExit; end; {$ENDIF} procedure TProcessUTF8.ParseCmdLine(const CmdLine: string; ReadBackslash: boolean); var List: TStringList; begin List:=TStringList.Create; try SplitCmdLineParams(CmdLine, List, ReadBackslash); if List.Count>0 then begin Executable:=List[0]; List.Delete(0); end else begin Executable:=''; end; Parameters.Assign(List); finally List.Free; end; end; end.