SyntaxBomb - Indie Coders

Languages & Coding => BlitzMax / BlitzMax NG => Topic started by: ErikT on July 08, 2017, 20:23:57

Title: Check running processes?
Post by: ErikT on July 08, 2017, 20:23:57
Hey fellas,

I'd like to run an external exe from blitzmax and keep the blitzmax program running for as long as that external exe process is running. Then, when bmx detects that the external program has ended, it should shut down as well. I've been using system_ <program.exe> earlier but the problem with that is that it freezes blitzmax program flow until the external process has ended, and I need to be able to do some other stuff while it's running.

Digging around I found this code snippet from Blitzsupport...

http://www.syntaxbomb.com/index.php/topic,2671.0.html

... and it looks very close to what I need. As I understand from the comments below I can replace ShellExecuteA with ShellExecuteEx to be able to check the running status of external processes, but I have no idea how to adapt it. If anyone could help me figure it out, I'd be really grateful :)
Title: Re: Check running processes?
Post by: markcwm on July 09, 2017, 00:47:39
I'm not going to code it for you but you need to create a struct using a type from https://msdn.microsoft.com/en-us/library/windows/desktop/bb759784(v=vs.85).aspx

Then pass an object pointer of that to your wrapped ShellExecuteEx function https://msdn.microsoft.com/en-us/library/windows/desktop/bb762154(v=vs.85).aspx

And see what happens.
Title: Re: Check running processes?
Post by: ErikT on July 09, 2017, 15:15:09
Thanks for the pointers!

I'll admit straight away I'm not an accomplished programmer by any stretch and C#/C++ stuff is pretty alien to me.

So I need to convert this thing here:

typedef struct _SHELLEXECUTEINFO {
  DWORD     cbSize;
  ULONG     fMask;
  HWND      hwnd;
  LPCTSTR   lpVerb;
  LPCTSTR   lpFile;
  LPCTSTR   lpParameters;
  LPCTSTR   lpDirectory;
  int       nShow;
  HINSTANCE hInstApp;
  LPVOID    lpIDList;
  LPCTSTR   lpClass;
  HKEY      hkeyClass;
  DWORD     dwHotKey;
  union {
    HANDLE hIcon;
    HANDLE hMonitor;
  } DUMMYUNIONNAME;
  HANDLE    hProcess;
} SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;



... into something like... this...?

Type ShellExecuteInfo
Field cbSize:Int
Field fMask:Int
Field hwnd: '(handle?)
Field lpVerb:String
Field lpFile:String
Field lpParameters:String
Field lpDirectory:String
Field nShow:Int
Field hInstApp:'(handle? memory address? wha?)
Field lpIDList:Byte Ptr '?
Field lpClass:String
Field hkeyClass: '(registry handle?)
Field dwHotKey:Int

Field hIcon: '(handle?)
Field hMonitor: '?
Field hProcess: '?
End Type


However I have no idea how to manage things like handles, HKEY and the like in blitzmax code. Even DWORDs and ULONGs... I guess they might be declared as Int's and that'll work out but who knows?

Union types... can I just ignore that bit and plop hIcon and hMonitor in as regular fields?
Title: Re: Check running processes?
Post by: col on July 09, 2017, 18:06:06
Hiya

Is there any reason not to use the cross platform pub.freeprocess module that comes with the 'Max installs?
You can start/create the process, read its std/err console outputs and check its status to know if it has ended.
Title: Re: Check running processes?
Post by: ErikT on July 09, 2017, 18:41:22
Only that I didn't know about it.

Thanks a lot :D I'll check it out now.
Title: Re: Check running processes?
Post by: ErikT on July 09, 2017, 20:54:01
Works great, thanks a bunch :)
Title: Re: Check running processes?
Post by: markcwm on July 10, 2017, 01:37:54
In the bag, col. 8)

I thought I'd go into ShellExecuteEx a bit more if you're interested. Microsoft have their own window data types https://msdn.microsoft.com/en-gb/library/windows/desktop/aa383751(v=vs.85).aspx and common/primitive data types https://msdn.microsoft.com/en-us/library/cc230309.aspx

Code (blitzmax) Select
Type ShellExecuteInfo
Field cbSize:Int ' DWORD = double word, 32-bit unsigned int
Field fMask:Int ' ULONG = 32-bit unsigned int (longs in bmx are 64-bit)
Field hwnd:Int ' HWND = handle to a window, 32-bit address
' (or if you're in 64-bit NG I think you'd use Byte Ptr)
Field lpVerb:Byte Ptr ' LPCTSTR should be Byte Ptr not String
' 8-bit or 16-bit chars depending if UNICODE is defined, ANSI is default
Field lpFile:Byte Ptr ' I think you define it with
' ModuleInfo "CC_OPTS: -DUNICODE"
Field lpParameters:Byte Ptr ' then to read it use
' Local param:String=String.FromCString(lpParameters)
Field lpDirectory:Byte Ptr ' LPCTSTR = long pointer to const T‌CHAR string
Field nShow:Int
Field hInstApp:Int ' HINSTANCE = handle to instance, 32-bit address
Field lpIDList:Byte Ptr ' LPVOID = pointer to an ITEMIDLIST struct
' which contains an 'item identifier list'
Field lpClass:Byte Ptr ' LPCTSTR
Field hkeyClass:Int ' HKEY = handle to registry key, 32-bit address
Field dwHotKey:Int ' DWORD
Field hIcon-hMonitor:Int ' HANDLE = handle to object, 32-bit address
' unions have only one member at a time
'Field hMonitor:Int ' it is as big as it's largest member
' (blitzmax doesn't have them so you just hack it)
Field hProcess:Int ' HANDLE
End Type

Type ITEMIDLIST
Field mkid:SHITEMID ' this may be an enum, I'm not sure
End Type

Type SHITEMID
Field cb:Short ' USHORT = 16-bit unsigned int
Field abID:Byte[1] ' BYTE[1] = 8-bit unsigned array
End Type

Title: Re: Check running processes?
Post by: col on July 10, 2017, 10:38:10
Anytime guys  8)

The shell execute stuff works great too - I used to use it, but its only good for a windows unit  :D
Title: Re: Check running processes?
Post by: ErikT on July 10, 2017, 11:59:05
Ooh very interesting, thanks for sharing!

Any specific reason you use an array here?
Field abID:Byte[1]
Title: Re: Check running processes?
Post by: col on July 10, 2017, 12:43:17
As ErikT queries about the array... this code will break as a 'Max array is a different beast to a c array. The array definition in the c code is there as a hack to allow a dynamic c array in the struct, very unorthodox but it works  :o
If you're going to play with this kind of code then understanding the underlying mechanisms is of paramount importance to get it right and prevent bugs - especially when used within a garbage collected runtime - as 'Max has.

The LPCTSTR interpretation is correct, however a pointer is a pointer - a pointers 'value' will be either 32bit or 64bit.

When you use types such as ShellExecuteInfo you are setting up a block of memory to hold data. You fill that block of data with 'numbers' and pass a pointer to it as a parameter in a function call to an appropriate Win32API function call. Note that Win32 can be confusing here as it has nothing to do with the 'bitness' of the CPU - ie it is just a name. The function code that gets called - ie the receiving code, in this case the code written by Microsoft, will know how to interpret that block of memory - as a ShellExecuteInfo struct. There is no marshalling or type safety when writing code to communicate outside of the 'Max environment so these structs/types have to be accurate and byte perfect.

As you correctly note the lpVerb, lpFile, lpParameters, lpDirectory, lpIDList and lpClass members should be pointers, the same as the original C struct has, and a 'Max string is NOT a pointer to a string - its a pointer to a 'Max object which will have a member that will point to the string data plus other things. Also as you hinted there are multiple ways to represent a string in C, and the win32 api uses either a single byte per character or 2 bytes per character - also known in the Windows world, and other languages including 'Max as 'single character ansi' or 'wide character'. In 'Max you can create a pointer to a 'Max string by using either...
Local str:String = "Something"
Local strPtr:Byte Ptr = str.ToCString()


and this will create a null terminated single byte per character string and store address of that string in strPtr, or you can use
Local str:String = "Something"
Local strPtr:Short Ptr = str.ToWString()


to create a null terminated 'wide character' version of the string - Yes the null terminator is also a wide version too.

VERY IMPORTANT to note is that those string methods create a NEW string in a different memory location to the original string - if you don't free that memory then you'll be leaking memory.

To free the string pointer after you're done with it use
MemFree(strPtr)

Now  ;D the sharper folks reading this may ask the question of how can a function possibly know which of the 2 character formats it should use, for eg the function ShellExecuteEx takes a ShellExecuteInfo instance as a parameter, so how does the ShellExecuteEx function know if the string pointers point to a single byte or wide character string, and the answer is that there are in fact 2 variations of the ShellExecuteEx function: ShellExecuteExA and ShellExecuteExW. Using #defines and macros in the c win32 api the ShellExecuteEx function will be 'aliased' to one of the *A or *W versions depending on the development environment. To be honest you'll find that the majority of other areas of the Win32 API leans heavily towards using wide characters.

Anyway... have fun  8)

EDIT: Oh I jut noticed too... there's mention of the HANDLE type in a previous post... be aware that a HANDLE is a pointer and not an Int - use a Byte Ptr to prevent those nasty bugs and inconspicuous errors. Hopefully you'd get a crash at a function call that uses that data as you don't want to spend hours looking for intermittent bugs ???
Title: Re: Check running processes?
Post by: markcwm on July 11, 2017, 08:33:45
Thanks col! The string stuff was really clear. 8)

So all the handles ie. hwnd, hInstApp, hkeyClass, hIcon+hMonitor, hProcess should be Byte Ptr?

How do we tell if it's pointers are 32-bit or 64-bit? I don't think we need to know though, do we?

In Blitzmax we just need to extern it as ShellExecuteExA or W?

How would we declare abID:Byte[1] a valid array, use a memory block?
Title: Re: Check running processes?
Post by: col on July 11, 2017, 13:45:27
QuoteHow do we tell if it's pointers are 32-bit or 64-bit? I don't think we need to know though, do we?
As far as pointers go that's all taken care of during the compilation phase, so you don't need to worry about it 8)

Something very important too is the function calling convention for 32bit apps. For 64bit apps the calling convention is *usually* the same across the whole 64bit world but you may find some subtle differences - I've not actually had a problem in the wild with this for 64bit apps but its good to be aware that there are different calling conventions for 64bit too, it just seems that all compiler vendors and OS developers are using the same single calling convention for 64bit - which is nice  ;)
For 32bit apps however things have narrowed down to 2 different calling conventions 'stdcall' and 'cdecl'. To keep things short the calling convention defines whether the 'called function' or the 'calling code' cleans up the stack when the function returns. If you get this wrong the very least is that you get incorrect return values and your app misbehaves, the worse is a full-on stack crash application train wreck  :o

'Max allows you to define the calling convention by using "Win32" to define a function as 'stdcall' or using "C" to define the function to use the 'cdecl' convention. You place that annotation after the function parameters closing parenthesis such as

Function ShellExecuteEx(pShellExecuteInfo:Byte Ptr)"Win32"



A further question for the alert readers would be 'If the 'called' function is supposed to clean up the stack ( the "Win32" or 'stdcall' calling convention ) then how does it know how much stack to clean up?' The answer here is that the size in bytes is a part of the function name - I know I know... you're probably thinking 'oh my god... there's more things to know about this stuff' but honestly things don't get any more complicated after this... honestly :P
'Max allows you to 'alias' the function to point to a function of another name. You can do that for extern functions using assignment and putting the complete external function definition with quotes and use the @ symbols to define how many bytes ( the size of all parameters combined ) are being put on the stack and being passed into the function itself. In the case of the ShellExecuteEx function, it has a single parameter that's a pointer, so for a 32bit app the size in bytes is 4, remember that you don't need this for 64bit apps as they don't use the 'stdcall' calling convention. We end up with

Function ShellExecuteEx(pShellExecuteInfo:Byte Ptr)"Win32" = "ShellExecuteExA@4" ' to .ToCString() in the ShellExectuteInfo string members


or


Function ShellExecuteEx(pShellExecuteInfo:Byte Ptr)"Win32" = "ShellExecuteExW@4" ' to .ToWString() in the ShellExectuteInfo string members


QuoteIn Blitzmax we just need to extern it as ShellExecuteExA or W?
In 'Max you can expose and use which ever ShellExecuteEx function that you want to use. You can have both available too it you're cute with your code design. Just that you need to make sure if you use the *W version that any strings point to a '*.ToWString()', and if you decide to use the *A version then all strings must point to a '*.ToCString()'.

A finished definition of the ShellExecuteEx function would be something like

Strict

Extern
Function ShellExecuteEx(pShellExecuteInfo:Byte Ptr)"Win32" = "ShellExecuteExW@4"
EndExtern


QuoteHow would we declare abID:Byte[1] a valid array, use a memory block?
You wouldn't normally fill this in yourself. You would use other APIs and interfaces to create these objects for you. When it comes to retrieving data that is given to you in one of the objects it will always be via a pointer and more than likely from an enumeration list object interface. You could get the size of the data via the first parameter and then create a pointer via MemAlloc using the size parameter, copy the data from the returned pointer to your MemAlloced pointer. From there you can get at the data but that is hard work. Again you normally use the shell API interfaces to help deal with all of the enumeration and allocation/deallocation. This is an area where you definitely need to read the manual  ;D




Title: Re: Check running processes?
Post by: fielder on July 11, 2017, 20:21:04
very nice!
can someone post an updated code ?
Title: Re: Check running processes?
Post by: col on July 11, 2017, 22:43:18
Quotecan someone post an updated code ?

Yeah sure.
Save this source code as 'ThisSource.bmx', then run it. It will use ShellExecuteEx to open 'notepad.exe' and load the source code directly into it  ;)
If you wanted to know about the state of the process etc, then you would set the flag parameter accordingly to return the process handle in the hProcess parameter, you can then check using other Win32API functions ( GetExitCodeProcess ) to know the running state etc.

EDIT: I'd just like to point out a little tid-bit regarding calling conventions, and that is that 'Max defaults to using the "C" ( 'cdecl' ) calling convention. Because it defaults to using that one then there's no need to specify it, and you only really need to specify "Win32" ( the 'stdcall' calling convention ) where you need it - mostly only when writing code for Windows stuff, including DLL functions. Be aware though that not all functions will be 'stdcall', for eg the VLC dll uses the 'cdecl' calling convention, perhaps because VLC is cross platform. To know for sure which one should be used you should always consult the documentation and/or check any c/c++ SDK for verification. Have fun  8)


Strict

' an example to open Notepad.exe using ShellExecuteEx

' Using Extern"Win32" means that we don't have to put the "Win32" after each function definition, this whole Extern block will
' automatically be declared as using the "Win32" calling convention for functions and type methods. Its handy if you have a ton
' of types or functions that you want to expose in the 'Max environment

Extern"Win32"
Function GetLastError:Int() ' no need to alias functions that have no parameters, and you can use "GetLastError@0" if you really want to
Function ShellExecuteEx:Int(pShellExecuteInfo:Byte Ptr) = "ShellExecuteExW@4"
EndExtern


' You could create 2 of these too... ShellExecuteInfoW and/or ShellExexcuteInfoA to use with the correspoding ShellExecuteEx function
' to allow a choice of using either. The one below can be used with *W as the strings will be pointers to WString ( Short Ptr )

' Using Byte Ptr for the handles makes this compatible with 64bit apps. Using an Int will only work on 32bit apps
' In legacy 'Max you will need to cast from Int to Byte Ptr. The *best* way on how to handle this whole scenario is open to debate.

Type ShellExecuteInfo
Field cbSize:Int
Field fMask:Int
Field hWnd:Byte Ptr
Field lpVerb:Short Ptr
Field lpFile:Short Ptr
Field lpParameters:Short Ptr
Field lpDirectory:Short Ptr
Field nShow:Int
Field hInstApp:Byte Ptr
Field lpIDList:Byte Ptr
Field lpClass:Short Ptr
Field hKey:Byte Ptr
Field dwHotKey:Int
Field hIcon_hMonitor:Byte Ptr
Field hProcess:Byte Ptr
EndType

Local appName:String = "ThisSource.bmx"

Local sei:ShellExecuteInfo = New ShellExecuteInfo
sei.cbSize = SizeOf(ShellExecuteInfo)
sei.fMask = 0 ' default value
sei.hWnd = Null ' no need for a parent window
sei.lpVerb = "open".ToWString() ' as we are going to open 'notepad' and pass a file via a parameter we'll use 'open' here
sei.lpFile = "notepad.exe".ToWString() ' this depends on the file associations if you put a document file here
sei.lpParameters = (CurrentDir() + "/" + appName).ToWString() ' we'll pass this source code to notepad as a parameter
sei.lpDirectory = Null ' no need to change the working directory for this example
sei.nShow = 10 ' 10 is the value of SW_SHOWDEFAULT - lookup ShellExecute for these values
sei.hInstApp = Null ' this is a strange one according to the docs. 16bit compatiblity and only holds a 32bit value.
sei.lpIDList = Null ' this is ignored in this example
sei.lpClass = Null ' this is ignored in this example
sei.hKey = Null ' this is ignored in this example
sei.dwHotKey = 0 ' this is ignored in this example
sei.hProcess = Null ' a process handle can be returned in this parameter under some circumstances.

Local returnValue:Int = ShellExecuteEx(sei)

' clean up the string pointers
MemFree(sei.lpVerb)
MemFree(sei.lpFile)
MemFree(sei.lpParameters)

' show a message is the function call failed
If returnValue <> True DebugLog "ShellExecuteEx failed with error code: " + GetLastError()
Title: Re: Check running processes?
Post by: markcwm on July 13, 2017, 02:01:15
Brilliant! Thanks col. There's nothing like a working example to clear things up. 8)
Title: Re: Check running processes?
Post by: ErikT on July 13, 2017, 11:15:19
Wow! Very informative, thanks :)