Alexey Maslov · Aug 21, 2017

How to get a PID of Windows process called with $zf(-1,...)?

Hello everybody,

We have a piece of Caché software which calls an external utility using $zf(-1,command). It works fine under Linux, but under Windows an external process occasionally hangs (due to some internal problems out of the scope here) and need to be killed programmatically. Having PID, it's easy to kill a process. If a Caché process is called with JOB command, the caller can easily get its PID from $zchild, but alas $zf(-1) does not seem to return the similar info. Is it possible to get it somehow?

1 0 4 432


I don't know of any magic $zf/$zu function or API to do this. Looking at StackOverflow, a common approach seems to be parsing the output of "tasklist" filtered by executable name and/or window title.

Here's a classmethod to get the PIDs of all processes with a given executable name (Windows-only, of course):

Class DC.Demo.WindowsProcessList

ClassMethod GetPIDsByExecutable(pExecutable As %String = "", Output pPIDList As %List) As %Status
    Set tSC = $$$OK
    Set tFileCreated = 0
    Set pPIDList = ""
    Try {
        Set tSC = ##class(%Net.Remote.Utility).RunCommandViaZF("tasklist /V /FO CSV /NH",.tTempFileName,,,0)
        Set tFileCreated = (tTempFileName '= "")
        Set tRowSpec = "ROW(ImageName VARCHAR(255),PID INT)"
        Set tResult = ##class(%SQL.Statement).%ExecDirect(,
            "call %SQL_Util.CSV(,?,?)",
        // For debugging (if tRowSpec changes):
        // Do tResult.%Display()
        If (tResult.%SQLCODE < 0) {
            Throw ##class(%Exception.SQL).CreateFromSQLCODE(tResult.%SQLCODE, tResult.%Message)
        // Note: could instead call tResult.%NextResult() to get a result set with column names as properties.
        While tResult.%Next(.tSC) {
            If ($ZConvert(tResult.%GetData(1),"L") = $ZConvert(pExecutable,"L")) {
                Set pPIDList = pPIDList_$ListBuild(tResult.%GetData(2))
    } Catch e {
        Set tSC = e.AsStatus()
    // Cleanup
    If tFileCreated {
        Do ##class(%Library.File).Delete(tTempFileName)
    Quit tSC


Thank you, Timothy.
Definitely this is a solution, while our case is a bit more complex. It can be several running copies of "dangerous" utility started by different users, so we can't select the actual one neither by executable name nor by its window title as it is started with "-nogui" option and has got no window.

An approach how to bypass this limitation that we are going to implement looks like this:

 lock +^offPID
 set rc=$zf(-2,"drive:\path\utility --par1 --parN")
 if rc=0 {
   get PIDs of all running copies of utility.exe
   if '$data(^offPID(PID1)) {
       set ^offPID(PID1)=$h // search a new PID (let it be PID1)
       job checkJob(PID1)
 } else {
   process an error
 lock -^offPID

сheckJob(pPID) // check if pPID is running
  for {
    get PIDs of all running copies of utility.exe
    if pPID is not listed {
       kill ^offPID(pPID)
   } elseif timeout expired {
       set rc=$zf(-1,"taskkill /pid "_pPID)
       if rc=0 { ... }
       else { process an error }
       kill ^offPID(pPID)

   } else {
      hang 30 // wait...


Thank you, Robert.

In this case the PID saved in a file will be the PID of command processor (cmd) itself rather than the PID of an utility which it has invoked. If Cache process kill the cmd instance using this PID, the utility will continue its execution. Besides, there is no  parent-child relationship between the cmd and the utility, so /t switch (kill process tree) would not help.


with $ZF(-1,command)  you are at OS level.
Nothing  prevents you from extending command to a script that  writes its pid in a file.
using $JOB_".pid" or similar as file name should be enough to  identify it.
So after a moderate HANG your pid should be available.

I prefer this to using pipes or other sophisticated solutions (e.g. listening on a TCP/IP or UDP port, ...)
as you get  an embedded trace for free