Class: K8::Util::ShellCommand

Inherits:
Object
  • Object
show all
Defined in:
lib/keight.rb

Overview

Invoke shell command and responses it’s output to client.

Example:

def do_download_csv()
  ## for example, run SQL and generate CSV file (for postgresql)
  sql = "select * from table1"
  cmd = "psql -AF',' -U dbuser dbname | iconv -f UTF-8 -t CP932 -c | gzip"
  shell_command = K8::Util::ShellCommand.new(cmd, input: sql) do
    ## callback after sending response body
    File.unlink(tempfile) if File.exist?(templfile)  # for example
  end
  begin
    return shell_command.start() do
      ## callback before sending response body
      @resp.headers['Content-Type']        = "text/csv;charset=Shift_JIS"
      @resp.headers['Content-Disposition'] = 'attachment;filename="file.csv"'
      @resp.headers['Content-Encoding']    = "gzip"
    end
  rescue K8::Util::ShellCommandError => ex
    logger = @req.env['rack.logger']
    logger.error(ex.message) if logger
    @resp.status = 500
    @resp.headers['Content-Type'] = "text/plain;charset=UTF-8"
    return ex.message
  end
end

Constant Summary collapse

CHUNK_SIZE =
8 * 1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command, input: nil, chunk_size: nil, &teardown) ⇒ ShellCommand

Returns a new instance of ShellCommand.



506
507
508
509
510
511
512
513
514
# File 'lib/keight.rb', line 506

def initialize(command, input: nil, chunk_size: nil, &teardown)
  #; [!j95pi] takes shell command and input string.
  @command    = command  # ex: "psql -AF',' dbname | gzip"
  @input      = input    # ex: "select * from table1"
  @chunk_size = chunk_size || CHUNK_SIZE
  @teardown   = teardown
  @process_id = nil
  @tuple      = nil
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



516
517
518
# File 'lib/keight.rb', line 516

def command
  @command
end

#inputObject (readonly)

Returns the value of attribute input.



516
517
518
# File 'lib/keight.rb', line 516

def input
  @input
end

#process_idObject (readonly)

Returns the value of attribute process_id.



516
517
518
# File 'lib/keight.rb', line 516

def process_id
  @process_id
end

Instance Method Details

#each {|chunk| ... } ⇒ Object

Yields:

  • (chunk)


554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/keight.rb', line 554

def each
  #; [!ssgmm] '#start()' should be called before '#each()'.
  @process_id  or
    raise ShellCommandError.new("Not started yet (command: #{@command.inspect}).")
  #; [!vpmbw] yields each chunk data.
  sout, serr, waiter, chunk = @tuple
  @tuple = nil
  yield chunk
  size = @chunk_size
  ex = nil
  begin
    while (chunk = sout.read(size))
      yield chunk
    end
    #; [!70xdy] logs stderr output.
    error = serr.read()
    log_error(error) if error && ! error.empty?
  rescue => ex
    raise
  ensure
    #; [!2wll8] closes stdout and stderr, even if error raised.
    sout.close()
    serr.close()
    #; [!0ebq5] calls callback specified at initializer with error object.
    @teardown.yield(ex) if @teardown
  end
  #; [!ln8we] returns self.
  self
end

#log_error(message) ⇒ Object



584
585
586
587
588
# File 'lib/keight.rb', line 584

def log_error(message)
  $stderr.write("[ERROR] ShellCommand: #{@command.inspect} #-------\n")
  $stderr.write(message); $stderr.write("\n") unless message.end_with?("\n")
  $stderr.write("--------------------\n")
end

#startObject



518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/keight.rb', line 518

def start
  #; [!66uck] not allowed to start more than once.
  @process_id.nil?  or    # TODO: close sout and serr
    raise ShellCommandError.new("Already started (comand: #{@command.inspect})")
  #; [!9seos] invokes shell command.
  require 'open3' unless defined?(::Open3)
  sin, sout, serr, waiter = ::Open3.popen3(@command)
  @process_id = waiter.pid
  size = @chunk_size
  begin
    #; [!d766y] writes input string if provided to initializer.
    sin.write(input) if input
    sin.close()
    #; [!f651x] reads first chunk data.
    #; [!cjstj] raises ShellCommandError when command prints something to stderr.
    chunk = sout.read(size)
    if chunk.nil?
      error = serr.read()
      log_error(error.to_s)
      error = "Command failed: #{@command}" if ! error || error.empty?
      raise ShellCommandError.new(error)
    end
    #; [!bt12n] saves stdout, stderr, command process, and first chunk data.
    @tuple = [sout, serr, waiter, chunk]
    #; [!kgnel] yields callback (if given) when command invoked successfully.
    yield if block_given?
  #; [!2989u] closes both stdout and stderr when error raised.
  rescue => ex
    sout.close()
    serr.close()
    raise
  end
  #; [!fp98i] returns self.
  self
end