PROCEDURE RunExeAndWait * procedure to fire up an external program and suspend all VFP activities until it closes * Written by: Albert Gostick * Last Updated: Nov 24, 2004 * Parameters: * tcExeName: application (exe or com etc) to fire up (full path and file name (no parameters though); note that * the string does NOT need to be wrapped in quotes if the string contains any spaces (and in testing, it * cannot in fact be wrapped in quotes or it throws error #123) * tcCommandLine: full command line including parameters with a space between exe and parameters; full path to * exe should be wrapped in quotes if there is a space anywhere in the file path (or else the parser cannot * determine where the .exe filename ends and the parameters start; note that if parameters are to be passed, * then the .exe filepath has to be at the start of the string also * tcStartupDir: startup directory for the process (optional); if not passed, defaults to SYS(5) + CURDIR() * tnPriority: priority for the app to start; values should be 32 (Normal), 128 (High), 64 (Idle/Low) or 1600 * (real-time, whatever that is); optional, defaults to 32 * * Examples: * RunExeAndWait('C:\Windows\NotePad.exe') * RunExeAndWait('C:\Program Files\Adobe\Acrobat 6.0\Acrobat\Acrobat.exe') * RunExeAndWait('C:\Program Files\Adobe\Acrobat 6.0\Acrobat\Acrobat.exe', ; * '"C:\Program Files\Adobe\Acrobat 6.0\Acrobat\Acrobat.exe" c:\Test\MyReport.pdf') LPARAMETERS tcExeName, tcCommandLine, tcStartupDir, tnPriority LOCAL llOldAutoYield, llReturn, laDLLArray[1], lnNumDLLs, ; llCreateProcessWasOpen, llGetLastErrorWasOpen, llCloseHandleWasOpen, llGetExitCodeWasOpen, llSleepWasOpen, ; ; lcNullChar, lcStartupInfo, lcProcessInfo, lnInheritHandles, lnExitCode, lnResult, ; lnProcessHandle, lnThreadHandle, lnProcessAttributes, nThreadAttributes, lnEnvironment * use this over and over so stuff in var STORE CHR(0) TO lcNullChar *** Parameter checks *** * all parameters should be and null-terminated if passed and empty character if not passed; note that in * testing, it does not seem that the parameters have to be null terminated but will keep the code in * tcExeName IF VARTYPE(tcExeName) # "C" STORE "" TO tcExeName ELSE STORE ALLTRIM(tcExeName) + IIF(RIGHT(tcExeName,1) == lcNullChar,"",lcNullChar) TO tcExeName ENDIF * tcCommandLine IF VARTYPE(tcCommandLine) # "C" STORE "" TO tcCommandLine ELSE STORE ALLTRIM(tcCommandLine) + IIF(RIGHT(tcCommandLine,1) == lcNullChar,"",lcNullChar) TO tcCommandLine ENDIF * tcStartupDir IF VARTYPE(tcStartupDir) # "C" STORE SYS(5) + CURDIR() + lcNullChar TO tcStartupDir ELSE STORE ALLTRIM(tcStartupDir) + IIF(RIGHT(tcStartupDir,1) == lcNullChar,"",lcNullChar) TO tcStartupDir ENDIF * tnPriority: default this to 32 if not passed; if passed, force to 32 if incorrect IF VARTYPE(tnPriority) # "N" STORE 32 TO tnPriority ELSE IF NOT INLIST(tnPriority,32,64,128,1600) STORE 32 TO tnPriority ENDIF ENDIF *** Declare Necessary Windows Functions *** * init array in case no DLLs around and then grab DIMENSION laDLLArray[1] STORE ADLLS(laDLLArray) TO lnNumDLLs * init some vars to indicate if they were open or not before STORE .F. TO llCreateProcessWasOpen, llGetLastErrorWasOpen, llCloseHandleWasOpen, llGetExitCodeWasOpen, llSleepWasOpen * Note: the ASCANs done below are case-insensitive and only search column 1 IF ASCAN(laDLLArray,"CREATEPROCESS",1,-1,1,15) = 0 DECLARE INTEGER CreateProcess IN kernel32 ; STRING lpApplicationName, STRING lpCommandLine, INTEGER lpProcessAttributes, INTEGER lpThreadAttributes, ; INTEGER bInheritHandles, INTEGER dwCreationFlags, INTEGER lpEnvironment, STRING lpCurrentDirectory, ; STRING @lpStartupInfo, STRING @lpProcessInformation STORE .T. TO llCreateProcessWasOpen ENDIF IF ASCAN(laDLLArray,"GETLASTERROR",1,-1,1,15) = 0 DECLARE INTEGER GetLastError IN kernel32 STORE .T. TO llGetLastErrorWasOpen ENDIF IF ASCAN(laDLLArray,"CLOSEHANDLE",1,-1,1,15) = 0 DECLARE INTEGER CloseHandle IN kernel32 INTEGER hObject STORE .T. TO llCloseHandleWasOpen ENDIF IF ASCAN(laDLLArray,"GETEXITCODEPROCESS",1,-1,1,15) = 0 DECLARE INTEGER GetExitCodeProcess IN WIN32API INTEGER hProcess, INTEGER @lpExitCode STORE .T. TO llGetExitCodeWasOpen ENDIF IF ASCAN(laDLLArray,"SLEEP",1,-1,1,15) = 0 DECLARE Sleep IN kernel32 INTEGER dwMilliseconds STORE .T. TO llSleepWasOpen ENDIF * default return value (only set to .F. on error) STORE .T. TO llReturn * save autoyield setting to restore and then set for here STORE _VFP.AutoYield TO llOldAutoYield STORE .T. TO _VFP.AutoYield *** Documentation On CreateProcess() function *** * Was able to glean this documentation from the sources that follow but note that in some cases, it may be incomplete * or inaccurate as the documentation was somewhat sketchy; here were the main sources: * Microsoft Sources: * Best article: titled "CreateProcess" in MSDN library * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/createprocess.asp * This one has more detail on what should be in StartupInfo string if you want to change the appearance of the * startup process: * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarw98bk/html/makingmodifyingprocesses.asp * and then an article that explains the difference between passing arguments via Param1 and 2: * http://support.microsoft.com/default.aspx?scid=kb;en-us;175986 i.e. article Q175986 * Universal Thread examples and samples: * UT Thread: 931761 Message: 939258 (Tracy Holzer) * UT Thread: 936327 Message: 936339 (Claudio Rola) * UT Thread: 936327 Message: 936342 (Marcia Akins) * (note that these last 2 messages use function WaitForSingleObject() as part of their example but it seems like * later examples in the UT have moved away from using that to using GetExitCodeProcess() instead) * Parameters for CreateProcess(): * [1] lcApplicationName: the application file (e.g. exe) to run including the full path if it is not in the current * directory; the string should NOT be in quotes even if the file path contains spaces somewhere in the path; * optional; if not passed, the information has to be in parameter 2, lcCommandLine, instead; supposed to be * null-terminated but it did not seem to make a difference either way * [2] lcCommandLine: if the exe to be run includes any parameters, then the entire command line needs to be in this * parameter (otherwise, if there are no parameters for the exe to be run, then this parameter can be left empty); * if not empty, lcCommandLine needs to include the exe file to run and it's parameters; it is better to include * the full path or else various paths are searched (see MSDN documentation on CreateProcess() for path search * specifics); an example is "C:\Windows\Notepad.exe c:\Temp\MyFile.txt"; note that there has to be a space in * between the .exe file and the parameters and because of this, if the path to the exe file contains any spaces, * then the .exe filepath must be enclosed in it's own quotes so that the parse can determine where the exe file * name ends and the paraemters begin e.g. ["C:\Program Files\Acrobat\Acrobat.exe" c:\Temp\SomePdf.pdf] * [3] lcProcessAttributes: something to do with assigning security attributes to the process or allowing the * returned handle to be inherited by child processes; not sure what this means but we pass zero which * means they cannot be inherited (I think - null cannot be inherited so zero must be same as null) * [4] lcThreadAttributes: sounds identical to [2] but applies to threads; zero passed so handle cannot be inherited * [5] llInheritHandles: if TRUE (1?) then each inheritable handle in the calling process is inherited by the * new process; the sample given to us was set to 1 * [6] lcCreationFlags: flags used when creating the process; in our case, flag is used to set priority for the * process creation so we use passed in parameter tnPriority (usually 32, "Normal") * [7] lnEnvironment: if null, new process uses environment of the calling process (if used, seems to be a string of * environmental vars (eg. DEV=0N) that is passed to set up the environment; each set of vars is delimited by * the null character and then the entire block is terminated with a null as well * [8] lcCurrentDirectory: directory to startup the process in (I think this is the same as the "Start-In" option * on a shortcut); must be null-terminated; if not passed, the directory for the calling program is used; note that * in the sample we started with this was not used and sample wrapped its code in SET DEFAULT TO <something> before * and after calling this method: I think it would be better to specify the startup directory for the exe running * by passing a parameter instead of using SET DEFAULT * [9] lcStartupInfo: string that is filled with startup info that after the call will contain (according to the docs) * "window station, desktop, standard handles and appearance of main window for new process"; although this is * passed in to function here, it does not seem to be parsed after returning; the string "D" + 67 nulls are * passed in as the string to be filled (the "D" is ascii character 68 which "tells" the function how many bytes * are in the string that is being passed - 68) it looks like this string can be filled with values to set things * like window caption of new process, window height etc; do not have good example on how to use yet * [10] lcProcessInfo: string that is filled with "process info" which I think is handles to the new process and it * thread; it is filled with 16 nulls and if the process is created correctly, then lcProcessInfo can be * parsed to obtain the following: handle to new process [chars 1 to 4], handle to thread [chars 5 to 8], * number identifying process [chars 9 to 12 I think], number identifying new thread [chars 13 to 16 I thnk]; * these seem to be "double word" values as they have to be converted using a function to convert from base * 256 values (see function String2DWord()) * Returns: non-zero if process successfully created (TRUE) or zero if process creation failed (FALSE) * set some values for CreateProcess lnProcessAttributes = 0 lnThreadAttributes = 0 lnInheritHandles = 1 lnEnvironment = 0 lcStartupInfo = CHR(68) + REPLICATE(CHR(0),67) lcProcessInfo = REPLICATE(CHR(0),16) * create the process and capture the result STORE CreateProcess(tcExeName,tcCommandLine,lnProcessAttributes,lnThreadAttributes,lnInheritHandles, ; tnPriority,lnEnvironment,tcStartupDir,@lcStartupInfo,@lcProcessInfo) TO lnResult * if non-zero returned, process was successfully created IF lnResult = 0 STORE GetLastError() TO lnErrorNum ?? CHR(7) * Debug: could not get the 2nd line ("Please inform system administrator") to show up now matter what I did; turns * out that the null character at the end of tcExeName is causing WAIT WINDOW to stop at that point so strip it WAIT WINDOW "Error number " + LTRIM(STR(lnErrorNum)) + " occurred when trying to startup process: " + ; CHR(13) + LEFT(tcExeName,LEN(tcExeName)-1) + CHR(13) + "Please inform system administrator" * set our return value to indicate could not process STORE .F. TO llReturn ELSE * string lcProcessInfo will now contain a series of handles in it; pull out 1st 4 chars as the process * handle and then the next 4 chars as the thread handle; these are in a base 256 format that need to * be coverted back to numeric (it seems) lnProcessHandle = DWordToNum(SUBSTR(lcProcessInfo,1,4)) lnThreadHandle = DWordToNum(SUBSTR(lcProcessInfo,5,4)) * wait until the termination of the program DOEVENTS DO WHILE .T. * reset exit code each loop STORE 0 TO lnExitCode * fetch the exit code from the app GetExitCodeProcess(lnProcessHandle,@lnExitCode) * if exitcode = 259, this means app not busy so exit IF lnExitCode # 259 && not still busy EXIT ENDIF Sleep(100) && wait .1 seconds ENDDO CloseHandle(lnThreadHandle) CloseHandle(lnProcessHandle) ENDIF * restore autoyield setting if needed IF llOldAutoYield = .F. STORE .F. TO VFP.AutoYield ENDIF *** Release Win Functions Opened *** * only release if the functions were not previously opened so as to not knock out if a program * program further up the stack is relying on them being open; can just test laDLLArray() again IF NOT llCreateProcessWasOpen CLEAR DLLS CreateProcess ENDIF IF NOT llGetLastErrorWasOpen CLEAR DLLS GetLastError ENDIF IF NOT llCloseHandleWasOpen CLEAR DLLS CloseHandle ENDIF IF NOT llGetExitCodeWasOpen CLEAR DLLS GetExitCodeProcess ENDIF IF NOT llSleepWasOpen CLEAR DLLS Sleep ENDIF RETURN (llReturn) PROCEDURE DWordToNum * function to convert a DWord (double wide word, which is 4 bytes base 256) to it's numeric equivalent * Written by: Albert Gostick * Last Updated: Nov 24, 2004 PARAMETERS tcDWord LOCAL lnReturn * work from left to right with the first char being base 256 raised to zero, the 2nd char being * 256 squared and so on; used constants as probably processes faster STORE ASC(SUBSTR(tcDWord,1,1)) + ; ASC(SUBSTR(tcDWord,2,1)) * 256 + ; ASC(SUBSTR(tcDWord,3,1)) * 65536 + ; ASC(SUBSTR(tcDWord,4,1)) * 16777216 ; TO lnReturn RETURN lnReturnAlbert