class QB::IPC::STDIO::Server

Server functionality to make the master QB process' STDIO streams available to external processes, specifically Ansible modules.

Ansible's handling of STDIO in modules is really not suitable for our use case - we want to see what modules and other external process commands are doing in real time, much like invoking them in a Bash script.

This thing is far from perfect, but it's been incredibly helpful for a simple solution.

Basically, {OutService} instances are created for `STDOUT` and `STDERR`, which each create a {UNIXServer} on a local socket file and spawn a {Thread} to listen to it. The socket's path is then made available to the Ansible child process via ENV vars, and that process in turn carries those ENV vars to it's module child processes, who can then use an instance of the corresponding {QB::IPC::STDIO::Client} class to connect to those sockets and write output that is passed through to the master QB process' output streams.

The protocol is simply text line-based, and modules - or any other process - written in other languages can easily connect and write as well.

@note

This feature only works for `localhost`. I have no idea what it will do
in other cases. It doesn't seem like it should break anything, but remotely
executing modules definitely won't be able to connect to the sockets on
the host.

@todo

There is also a {InService} for `STDIN`, but it's is pretty experimental /
broken at this point. That would be nice to fix in the future so that
programs that make use of user interaction work seamlessly through QB.

This will probably require using pseudo-TTY streams or whatever mess.

Attributes

socket_dir[R]

Where the UNIX socket files get put.

@return [Pathname]

Public Class Methods

clean_up_for(object_id:, services:, socket_dir: logger.debug "Cleaning up...", object_id: object_id, socket_dir: socket_dir) click to toggle source

Clean up resources for an instance. Broken out because I was trying to make it run as a finalizer to remove the directory in all cases, but that does not seem to be triggering. Whatever man…

@param [Fixnum] object_id:

The instance's `#object_id`, just for logging purposes.

@param [Array<Service>]

The instance's services, which we will {Service#close!}.

@param [Pathname] socket_dir:

The tmpdir created for the sockets, which we will remove.

@return [nil]

# File lib/qb/ipc/stdio/server.rb, line 106
def self.clean_up_for object_id:, services:, socket_dir:
  logger.debug "Cleaning up...",
    object_id: object_id,
    socket_dir: socket_dir
  
  services.each do |service|
    logger.catch.warn(
      "Unable to close service",
      service: service,
    ) { service.close! }
  end
      
  FileUtils.rm_rf( socket_dir ) if socket_dir.exist?
  
  logger.debug "Clean!",
    object_id: object_id,
    socket_dir: socket_dir
  
  nil
end
finalizer_for(**kwds) click to toggle source

Make a {Proc} to use for finalization.

Needs to be done outside instance scope to doesn't close over the instance.

@param **kwds

Passed to {.clean_up_for}.

@return [Proc<() => nil>]

@todo Document return value.
# File lib/qb/ipc/stdio/server.rb, line 139
def self.finalizer_for **kwds
  -> {
    logger.debug "Finalizing...", **kwds
    clean_up_for **kwds
    logger.debug "Finalized", **kwds
  }
end
new() click to toggle source

Instantiate a new `QB::IPC::STDIO::Server`.

# File lib/qb/ipc/stdio/server.rb, line 163
def initialize
  @socket_dir = Dir.mktmpdir( 'qb-ipc-stdio' ).to_pn
  
  @in_service   = QB::IPC::STDIO::Server::InService.new \
                    name: :in,
                    socket_dir: socket_dir,
                    src: $stdin
                    
  @out_service  = QB::IPC::STDIO::Server::OutService.new \
                    name: :out,
                    socket_dir: socket_dir,
                    dest: $stdout
                    
  @err_service  = QB::IPC::STDIO::Server::OutService.new \
                    name: :err,
                    socket_dir: socket_dir,
                    dest: $stderr
  
  @log_service  = QB::IPC::STDIO::Server::LogService.new \
                    name: :log,
                    socket_dir: socket_dir
                    
  ObjectSpace.define_finalizer \
    self,
    self.class.finalizer_for(
      object_id: object_id,
      services: services,
      socket_dir: socket_dir
    )
end

Public Instance Methods

services() click to toggle source

@return [Array<(InService, OutService, OutService)>]

Array of in, out and err services.
# File lib/qb/ipc/stdio/server.rb, line 201
def services
  [ @in_service, @out_service, @err_service, @log_service ]
end
start!() click to toggle source

Start all the {#services} by calling {Service#open!} on them.

@return [self]

# File lib/qb/ipc/stdio/server.rb, line 210
def start!
  services.each &:open!
  self
end
stop!() click to toggle source

Stop all {#services} by calling {Service#close!} on them and clean up the resources.

@return [self]]

# File lib/qb/ipc/stdio/server.rb, line 221
def stop!
  self.class.clean_up_for \
    object_id: object_id,
    services: services,
    socket_dir: socket_dir
  self
end