Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
181 views
in Technique[技术] by (71.8m points)

Terminate shell pipe from interactive go cli

I have a Go program that consumes "live" input from a shell pipe, eg:

tail -f some/file | my-program

my-program is an interactive program built with rivo/tview. I want to be able to close my program with Ctrl-C and have it also terminate the tail -f that supplies input to it.

Currently I have to hit Ctrl-C twice to get back to my shell prompt. Any way I can get back to my prompt by hitting Ctrl-C once?


Adjusted my program per @torek's explanation of progress groups and observation that I can get the progress group ID using unix.Getpgid(pid):

import (
  "os"
  "golang.org/x/sys/unix"
)

func main() {
  // do stuff with piped input

  pid := os.Getpid()
  pgid, err := unix.Getpgid(pid)

  if err != nil {
    log.Fatalf("could not get process group id for pid: %v
", pid)
  }

  processGroup, err := os.FindProcess(pgid)

  if err != nil {
    log.Fatalf("could not find process for pid: %v
", pgid)
  }

  processGroup.Signal(os.Interrupt)
}

This delivers my desired behavior from my original question.

I opted to not use syscall because of the warning I found:

Deprecated: this package is locked down. Callers should use the corresponding package in the golang.org/x/sys repository instead. That is also where updates required by new systems or versions should be applied. See https://golang.org/s/go1.4-syscall for more information.

I plan to update my program to detect whether or not it was given a pipe using the strategy outlined in this article, so when a pipe is detected, I'll do the above process group signaling on interrupt.

Any issues with that?


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

We'll assume a Unix-like system, using a shell that understands and engages in job control (and they all do now). When you run a command, the shell creates something called a process group or "pgroup" to hold each of the processes that make up the command. If the command is a pipeline (as this one is), each process in the pipeline gets the same pgroup-ID (see setpgid).

If the command is run in the forgeground (without &), the controlling terminal has this particular pgid assigned to it. Pressing one of the signal-generating keys, such as CTRL-C or CTRL-, sends the corresponding signal (SIGINT and SIGQUIT in these cases) to the pgroup, using an internal killpg or equivalent. This sends the signal to every member of the pgroup.

(Backgrounding a process is simply *cough* a matter of taking back the pgid on the controlling tty, then restarting the processes in the pipeline. To make that happen is not so simple, though, as indicated by the "restarting" here.)

The likely source of the problem here is that an interactive program will place the controlling terminal into cbreak or raw mode and disable some or all signalling from keyboard keys, so that, for instance, CTRL-C no longer causes the kernel's tty module to send a signal at all. Instead, if you see a key that should cause suspension (CTRL-Z) or termination, the program has to do its own suspending or terminating. Programmers sometimes assume that this consists of simply suspending or terminating—but since the entire pipeline never got the signal in question, that's not the case, unless the entire shell pipeline consisted solely of the interactive program.

The fix is to have the program send the signal to its own pgroup, after doing any necessary cleanup (temporarily or permanently) of the controlling terminal.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...