1

I'm looking for a preferably cross-platform way to detect from within a Tcl script if the interpreter is running in a foreground or in a background process.

I've seen how to do it via ps (or /proc/$$/stat on Linux); is there a better way or do I have to hack something around that approach? I already have a utility library written in C so exposing the lowlevel API that ps also uses so I don't have to parse process output (or special file content) would be fine.

RJVB
  • 698
  • 8
  • 18

2 Answers2

1

There's no truly cross-platform notion of foreground, but the main platforms do have ways of doing it according to the notion they have of foreground.

Linux, macOS, and other Unix:

For determining if a process is foreground or not, you need to check if its process group ID is the terminal's controlling process group ID. For Tcl, you'd be looking to surface the getpgrp() and tcgetpgrp() system calls (both POSIX). Tcl has no built-in exposure of either, so you're talking either a compiled extension (may I recommend Critcl for this?) or calling an external program like ps. Fortunately, if you use the latter (a reasonable option if this is just an occasional operation) you can typically condition the output so that you get just the information you want and need to do next to no parsing.

# Tested on macOS, but may work on other platforms
proc isForeground {{pid 0}} {
    try {
        lassign [exec ps -p [expr {$pid ? $pid : [pid]}] -o "pgid=,tpgid="] pgid tpgid
    } on error {} {
        return -code error "no such process"
    }
    # If tpgid is zero, the process is a daemon of some kind
    expr {$pgid == $tpgid  &&  $tpgid != 0}
}

Windows

There's code to do it, and the required calls are supported by the TWAPI extension so you don't need to make your own. (WARNING! I've not tested this!)

package require twapi_ui

proc isForeground {{pid 0}} {
    set forground_pid [get_window_thread [get_foreground_window]]
    return [expr {($pid ? $pid : [pid]) == $foreground_pid}]
}
Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • Interesting. I can't actually find a _portable_ way of getting the foreground terminal process group leader for the terminal associated with an arbitrary process ID. `ps` must be doing something non-portable… which it is allowed to do, of course; it doesn't need to only use pure POSIX APIs as it is part of the OS… – Donal Fellows May 30 '20 at 12:10
  • Thanks, I'll be twiddling my thumbs on this. As to the cross-platform bit: this is in Unix-land only. I wasn't even aware you could do something similar on MS Windows - foreground vs. background in a terminal shell is something other than foreground or background in a graphical shell. – RJVB May 30 '20 at 12:37
  • Update: Doing the determination for the current process _is_ possible in a mostly portable way, as you can open `/dev/tty` to get the FD to pass to `tcgetpgrp()`. – Donal Fellows May 30 '20 at 13:07
0

Thanks to Donal I came up with the implementation below that should work on all POSIX Unix variants:

/*
    processIsForeground

    synopsis: processIsForeground

    Returns true if the process is running in the foreground or false
    if in the background.
*/
int IsProcessForegroundCmd(ClientData clientData UNUSED, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
{
    /* Check the arg count */
    if (objc != 1) {
        Tcl_WrongNumArgs(interp, 1, objv, NULL);
        return TCL_ERROR;
    }

    int fd;
    errno = 0;
    if ((fd = open("/dev/tty", O_RDONLY)) != -1) {
        const pid_t pgrp = getpgrp();
        const pid_t tcpgrp = tcgetpgrp(fd);
        if (pgrp != -1 && tcpgrp != -1) {
            Tcl_SetObjResult(interp, Tcl_NewBooleanObj(pgrp == tcpgrp));
            close(fd);
            return TCL_OK;
        }
        close(fd);
    }
    Tcl_SetErrno(errno);
    Tcl_ResetResult(interp);
    Tcl_AppendResult(interp, "processIsForeground: ", (char *)Tcl_PosixError(interp), NULL);
    return TCL_ERROR;
}

int Pextlib_Init(Tcl_Interp *interp)
{
    if (Tcl_InitStubs(interp, "8.4", 0) == NULL)
        return TCL_ERROR;
// SNIP
    Tcl_CreateObjCommand(interp, "processIsForeground", IsProcessForegroundCmd, NULL, NULL);

    if (Tcl_PkgProvide(interp, "Pextlib", "1.0") != TCL_OK)
        return TCL_ERROR;

    return TCL_OK;
}
RJVB
  • 698
  • 8
  • 18