Class: Ruber::ExternalProgramPlugin

Inherits:
GuiPlugin show all
Defined in:
lib/ruber/external_program_plugin.rb

Overview

Base class for plugins whose main task is to run an external program and maybe (not necessarily) to display the output in a tool widget.

Basically, this class is a wrapper for KDE::Process providing a more integrated API for the most common functions. In particular, it provides the following functionality:

  • automatic creation of the process
  • automatic cleanup at shutdown
  • automatic managing of buffered output
  • nicer API to read from standard output and standard error
  • simplified API to start the process
  • automatic display of standard output and standard error in a tool widget (if required)
  • display of the command line and of the process status in a tool widget (if required)

To start the process, you simply call the run_process method, passing it the name of the program to run, the working directory and the arguments.

When the process emits the readyReadStandardOutput() or the readyReadStandardError(), the contents of the appropriate stream are read and split into lines. The resulting array are passed to the process_standard_output or process_standard_error method. Derived classes can override these methods to do what they want with the output.

In general, you can’t be sure whether the data contained read from standard output or error contains complete lines. To deal with this issue, whenob reading from a stream, only full lines (that is, lines which end in “\n”) are passed to process_standard_output or process_standard_error. If the last line isn’t complete, it is stored in a buffer. The next time characters are read from the process, if they came from the same channel, the buffer is added at the beginning of the new stream; if they came from the other channel, they’re passed to the appropriate process_standard_* method. This behaviour can be changed by passing the appropriate arguments to the constructor.

Some methods in this class have the ability to insert data in a OutputWidget. They will do so if the output_widget</tt> instance variable (which is created in the constructor, if it doesn't already exist) is not *nil*. Note that those methods don't check if <tt>output_widget actually is an OutputWidget or not. If it is something else, you’ll get errors. In all the following documentation it is assumed that:

  • the expression the output widget refers to the object the @output_widget instance variable refers to, unless it’s nil
  • everything concerning the output widget will be ignored (without giving any error) if the output widget doesn’t exist

=Signals =process_finished(int code, QString reason) Signal emitted when the process finishes. _code_ is the exit code of the process, while _reason_ is a string which can have one of the values "killed", "crash" or be empty. If it's empty, it means that the program finished normally; "killed" means that it was killed by the user and "crash" means that the program crashed. =process_started() Signal emitted when the process has started =process_failed_to_start() Signal emitted if the process couldn't be started (for example, because the given program doesn't exist) =Slots

  • slot_process_finished(int, QProcess::ExitStatus)
  • stop_process()

Direct Known Subclasses

FindInFiles::FindInFilesPlugin, RubyRunner::RubyRunnerPlugin

Constant Summary

Constants inherited from Plugin

LICENSES

Instance Attribute Summary (collapse)

Attributes included from PluginLike

plugin_description

Instance Method Summary (collapse)

Methods inherited from GuiPlugin

#action_collection, #execute_action, #register_action_handler, #setup_actions, #unload

Methods inherited from Plugin

#about_data

Methods included from PluginLike

#add_extensions_to_project, #add_options_to_project, #add_widgets_to_project, #create_tool_widget, #delayed_initialize, #initialize_plugin, #load_settings, #plugin_name, #query_close, #register_options, #register_with_project, #remove_extensions_from_project, #remove_from_project, #remove_options_from_project, #remove_widgets_from_project, #restore_session, #save_settings, #session_data, #setup_action, #setup_actions, #unload, #update_project

Signal Summary

Constructor Details

- (ExternalProgramPlugin) initialize(pdf, line_buffered = true)

Creates a new ExternalProgramPlugin.

pdf is the plugin info object for the plugin. If line_buffered is false, buffering won’t be used (all the characters read from the process will be passed to process_standard_output or process_standard_error even if they don’t end in a newline).

Note: the process’ channel mode will be set to Qt::Process::SeparateChannels. You can set it to any value you want later



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ruber/external_program_plugin.rb', line 108

def initialize pdf, line_buffered = true
  super pdf
  @buffer = nil
  @buffer_content_channel = nil
  @line_buffered = line_buffered
  @output_widget = nil unless defined? @output_widget
  @process = KDE::Process.new self
  @process.process_channel_mode = Qt::Process::SeparateChannels
  @process.output_channel_mode = KDE::Process::SeparateChannels
  @process.connect SIGNAL(:readyReadStandardOutput) do
    do_stdout @process.read_all_standard_output.to_s
  end
  @process.connect SIGNAL(:readyReadStandardError) do
    do_stderr @process.read_all_standard_error.to_s
  end
  connect @process, SIGNAL('finished(int, QProcess::ExitStatus)'), self, SLOT('slot_process_finished(int, QProcess::ExitStatus)')
  connect self, SIGNAL('process_finished(int, QString)'), self, SLOT('display_exit_message(int, QString)')
  @process.connect SIGNAL('error(QProcess::ProcessError)') do |e|
    failed_to_start if e == Qt::Process::FailedToStart
  end
  connect @process, SIGNAL('started()'), self, SIGNAL('process_started()')
end

Instance Attribute Details

- (Object) process (readonly)

The KDE::Process used by the plugin



95
96
97
# File 'lib/ruber/external_program_plugin.rb', line 95

def process
  @process
end

Instance Method Details

- (Object) display_exit_message(code, reason) (private)

Displays a message in the output widget describing the exit status of the program.

The message (output type message) has the format program_name message, where message is:

  • exited normally if reason is an empty string
  • was killed if reason is ‘killed’
  • exited with code code if reason is ‘crash’

This method is meant to be connected to the process_finished(int, QString) signal. Its arguments match those of the signal.

If you want to change the message, override this method. If you don’t want any message and the @output_widget instance variable is not nil, you can either disconnect this slot from the process_finished(int, QString) signal or override this method without calling super



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/ruber/external_program_plugin.rb', line 379

def display_exit_message code, reason
  return unless @output_widget
  mod = @output_widget.model
  rc = mod.row_count
  mod.insert_row rc
  idx = mod.index rc, 0
  type = :message
  text = "Process "
  case reason
  when ''
    if code == 0 then text << 'exited normally'
    else 
      text << "exited with code #{code}"
      type = :message_bad
    end
  when 'killed' then text << 'killed'
  when 'crash' 
    text << "crashed with code #{code}"
    type = :message_bad
  end
  mod.set_data idx, Qt::Variant.new(text)
  @output_widget.set_output_type idx, type
end

Slot Signature:

display_exit_message(int, QString)

- (Object) do_stderr(str) (private)

Pre-processes the string str then passes the resulting array to process_standard_error.

Pre-processing the string means:

  • splitting it into lines
  • emptying the buffer (by passing its contents to process_standard_output if it refers to standard output or prepending it to str if it refers to standard error)
  • putting the last line in the buffer if it doesn’t end in a newline.

If the plugin is not buffered, then only the first step is done.

Note: if str is empty, nothing will happen. If it consists only of newlines, it will cause the buffer to be emptied (as described above) and nothing else. Note: consecutive newlines will be treated as a single newline



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/ruber/external_program_plugin.rb', line 234

def do_stderr str
  return if str.empty?
  if @line_buffered and @buffer
    buffer = @buffer
    channel = @buffer_content_channel
    @buffer = nil
    @buffer_content_channel = nil
    if channel == :stderr
      str = buffer + str
      return do_stderr str
    else process_standard_output [buffer]
    end
  end
  lines = str.split_lines
  if @line_buffered and !str.end_with? "\n"
    @buffer = lines.pop
    @buffer_content_channel = :stderr
  end
  return if lines.empty?
  process_standard_error lines 
end

- (Object) do_stdout(str) (private)

Pre-processes the string str then passes the resulting array to process_standard_output.

Pre-processing the string means:

  • splitting it into lines
  • emptying the buffer (by passing its contents to process_standard_error if it refers to standard error or prepending it to str if it refers to standard output)
  • putting the last line in the buffer if it doesn’t end in a newline.

If the plugin is not buffered, then only the first step is done.

Note: if str is empty, nothing will happen. If it consists only of newlines, it will cause the buffer to be emptied (as described above) and nothing else. Note: consecutive newlines will be treated as a single newline



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/ruber/external_program_plugin.rb', line 196

def do_stdout str
  return if str.empty?
  if @line_buffered and @buffer
    buffer = @buffer
    channel = @buffer_content_channel
    @buffer = nil
    @buffer_content_channel = nil
    if channel == :stdout
      str = buffer + str
      return do_stdout str
    else process_standard_error [buffer]
    end
  end
  lines = str.split_lines
  if @line_buffered and !str.end_with? "\n"
    @buffer = lines.pop
    @buffer_content_channel = :stdout
  end
  return if lines.empty?
  process_standard_output lines 
end

- (Object) failed_to_start (private)

Method called if the program fails to start (in response, but not connected to, the process error(QProcess::ProcessError) signal if the argument is Qt::Process::FailedToStart).

It emits the process_failed_to_start() signal and displays an appropriate message to the output widget (using the :error1 output type).

If @output_widget is not nil but you don’t want the message to be generated, you’ll need to override this method. If you do so, don’t forget to emit the failed_to_start() signal.



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/ruber/external_program_plugin.rb', line 326

def failed_to_start
  emit process_failed_to_start 
  if @output_widget
    mod = @output_widget.model
    rc = mod.row_count
    mod.insert_row rc
    cmd = @process.program
    cmd_line = cmd.join " "
    prog = cmd.shift
    idx = mod.index(rc, 0)
    mod.set_data idx, Qt::Variant.new("#{prog} failed to start. The command line was #{cmd_line}")
    @output_widget.set_output_type idx, :error1
  end
end

- (nil) process_standard_error(lines) (private)

Does something with the text written to standard error by the program.

The base class implementation of this method inserts the text at the end of the output widget in the first column (one item per line) and sets the output type to :output. Nothing is done if the output widget doesn’t exist. Subclasses can reimplement this method to do something else (in this case, you don’t usually want to call super)

lines is an array where each entry corresponds to a line of output from the program. If buffering is on, each entry is a complete line (or should be considered such). If buffering is off, you’ll have to take care of newlines by yourself.

Parameters:

  • lines (Array<String>)

    the lines on standard error

Returns:

  • (nil)


301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/ruber/external_program_plugin.rb', line 301

def process_standard_error lines
  return unless @output_widget
  mod = @output_widget.model
  rc = mod.row_count
  mod.insert_rows rc, lines.size
  lines.each_with_index do |l, i|
    idx = mod.index(i + rc, 0)
    mod.set_data idx, Qt::Variant.new(l)
    @output_widget.set_output_type idx, :error
  end
  nil
end

- (nil) process_standard_output(lines) (private)

Does something with the text written to standard output by the program.

The base class implementation of this method inserts the text at the end of the output widget in the first column (one item per line) and sets the output type to :output. Nothing is done if the output widget doesn’t exist. Subclasses can reimplement this method to do something else (in this case, you don’t usually want to call super)

lines is an array where each entry corresponds to a line of output from the program. If buffering is on, each entry is a complete line (or should be considered such). If buffering is off, you’ll have to take care of newlines by yourself.

Parameters:

Returns:

  • (nil)


272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/ruber/external_program_plugin.rb', line 272

def process_standard_output lines
  return unless @output_widget
  mod = @output_widget.model
  rc = mod.row_count
  mod.insert_rows rc, lines.size
  lines.each_with_index do |l, i|
    idx = mod.index(i + rc, 0)
    mod.set_data idx, Qt::Variant.new(l)
    @output_widget.set_output_type idx, :output
  end
  nil
end

- (Object) run_process(prog, dir, args, title = '')

Starts the program.

prog is the name of the program (you don’t need to specify the absolute path if it’s in PATH). args is an array containing the arguments. dir is the working directory.

title is the string to display in the output widget. If it is an empty string, the name of the program followed by its arguments will be used. If it is nil or false, the title won’t be set.



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ruber/external_program_plugin.rb', line 142

def run_process prog, dir, args, title = ''
  @buffer = nil
  @buffer_content_channel = nil
  @process.clear_program
  @process.working_directory = dir
  program = [prog] + args
  @process.program = program
  if @output_widget and title
    title = program.join ' ' if title.empty?
    @output_widget.title = title
  end
  @process.start
end

- (Object) shutdown

Prepares the plugin to be unloaded by killing the process (no signal will be emitted from the process or the plugin from now on).

If you reimplement this method, don’t forget to call super. If you don’t you might cause a crash when Ruber is closed



172
173
174
175
176
# File 'lib/ruber/external_program_plugin.rb', line 172

def shutdown
  @process.block_signals true
  @process.kill
  super
end

- (Object) slot_process_finished(code, status) (private)

Slot called in response to the process’ finished(int, QProcess::ExitStatus) signal.

It emits the process_finished(int, QString) signal



347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/ruber/external_program_plugin.rb', line 347

def slot_process_finished code, status
  str = @process.read_all_standard_output.to_s
  str << "\n" unless str.end_with?("\n")
  do_stdout str
  str = @process.read_all_standard_error.to_s
  str << "\n" unless str.end_with?("\n")
  do_stderr str
  reason = ''
  if status == Qt::Process::CrashExit
    reason = code == 0 ? 'killed' : 'crash'
  end
  emit process_finished code, reason
end

Slot Signature:

slot_process_finished(int, QProcess::ExitStatus)

- (Object) stop_process

Stops the process.

It’s a shortcut for process.kill



161
162
163
# File 'lib/ruber/external_program_plugin.rb', line 161

def stop_process
  @process.kill
end

Slot Signature:

stop_process()

Signal Details

- process_finished(int arg1, QString arg2)

- process_started

- process_failed_to_start