piotrmurach / tty-command

Execute shell commands with pretty output logging and capture stdout, stderr and exit status.
https://ttytoolkit.org
MIT License
400 stars 34 forks source link

How to watch a command and then on stop return back? #55

Closed butsjoh closed 3 years ago

butsjoh commented 4 years ago

Hi,

All of the tty stuff is amazing and before i start i would like to thank you for the effort you put into it.

I have a question about a specific usage of tty command for the cli i am trying to build.

I bootstrapped my cli with teletype and I am also using thor_repl to make my cli interactive. I don't really need to run one time scripts but rather have it run in a repl kind of fasion and that is working fine. But now i am stuck on trying to get something work with tty-command. I have a command (docker-compose ps) that i want to get refreshed once in a while to see the output of it and want to stop it when i do a certain key combo.

I tried with using the 'watch' command which calls the given command in certain intervals and that works kind of ok but problematic part is that i cannot exit out of it anymore unless i hit CTRL+C but that triggers the interrupted signal which stops everything.

command(printer: :quiet).run('watch docker-compose ps', pty: true)

Now is there a way that you can think of (also maybe using other tty tools like https://github.com/piotrmurach/tty-reader) to somehow trap a key combo while that command is running and have a way to stop it and return to the cli? Or is this just impossible todo cause that command would be a subprocess that is running? Or is there a way to catch the interrupt in some way to restart the cli again? Or are there other means to run a command in a loop and stop that loop on key combo?

Would be happy to have your thought or opinions since you are more familiar with the possibilities of the tool.

Thnx in advance.

butsjoh commented 4 years ago

Ok it seems it works with the following. Leaving it for reference. So when i hit control+c when it is running it just returns back to the interactive cli prompt. Not sure though if that is expected to work like this but if you could give some insights on how it could work with other key combos that would be nice.

# frozen_string_literal: true

require_relative '../command'

module MyCli
  module Commands
    class Status < MyCli::Command
      def initialize(options)
        @options = options
      end

      def execute(input: $stdin, output: $stdout)
        command(printer: :quiet).run('watch docker-compose ps')
      rescue TTY::Command::ExitError

      end
    end
  end
end
piotrmurach commented 3 years ago

Hi Buts,

Thanks for using the TTY Toolkit!

When the command run returns non-zero exit it will raise the TTY::Command::ExitError. This is a safety measure built-in into tty-command as you don't want any script to progress any further. Alternatively, you can use run! and write logic to handle failure scenario:

result = command(printer: :quiet).run('watch docker-compose ps')
if result.failure?
  # handle or skip
end

Back to your original question. When you run any command via run or run! methods, it will be executed in a child process. In your case, the 'watch docker-compose ps runs in a child process. The way to interact with this process is either through standard input/output or by sending a signal. From the shell, the way a user can terminate watch command is with Ctrl+C. If you wanted to use some other combination of keys to stop this child process, you could do so but you would find it a bit more involved and more brittle.

First of all, any Unix user expects to be able to interrupt a long-running process with Ctrl+C. Secondly, even if you used the tty-reader to handle, let's say Ctrl+k keys, then you would need to somehow terminate the long-running watch process yourself. In Ruby, the way to do this is by killing the child process. An example of this approach would be:

reader = TTY::Reader.new
reader.on(:keyctrl_k) do
  parent_id = Process.pid 
  # find a child process that inherits from parent_id
  # you would probably need to run some form of ps & grep
  child_id = run("...")
  Process.kill('SIGHUP', child_pid)
  Process.detach(child_pid)
end

There may be other ways to handle what you want but that would be my approach given current implementation. However, I'm not sure I would actually go down this route given that Ctrl+C is a widely recognised way to stop long-running processes and it makes your code fairly straightforward. I may consider adding a way to somehow retrieve a child process id when the command is run but it's not supported at the moment. I hope this helps.