The Unix du
command is a potentially long running process that produces
tabular output that practically begs for operations like drilling down and
sorting. As such, it is an excellent opportunity to demonstrate that the
Wunderbar helps in situations where command line scripts are otherwise called
for.
Which brings us to the next demo: diskusage.
Once again, we will use web sockets, but as running commands and parsing the
output line by line is such a common pattern, Wunderbar's web socket support
redefines the system
method to send out events for every line output. This
reduces the core of this script to the following code:
# extract sizes in a background process
port = _.websocket {Dir.chdir(dir) {system 'du -sb *'}}
@websocket = "ws://#{env['SERVER_NAME'] || 'localhost'}:#{port}/"
To make this work, the HTML contains a table that serves as a blank canvas, and is set up for sorting:
# initial table (names and dates without sizes)
_table_ do
_thead_ do
_tr do
_th 'Name', data_sort: 'string'
_th 'Size', data_sort: 'int'
_th 'Date', data_sort: 'date'
end
end
_tbody do
# ...
end
end
Each time a message is received from stdout
, it is parsed and compared
against every row in the table. When a matching name is found, the size
column for that row is filled in with the number (with commas inserted to
improve readability), and with the original number added as an attribute for
sorting purposes.
ws.onmessage = proc do |evt|
data = JSON.parse(evt.data)
match = data.line.match(/^(\d+)\s+(.*)/)
# update table using output from 'du' command
if data.type == 'stdout' and match
~'tbody tr'.each do
if match and ~["td:first a", self].text == match[2]
~["td", self].eq(1).attr('data-sort-value', match[1]).text =
match[1].gsub(/(\d)(?=(\d{3})+(\.|$))/, '$1,')
match = nil
end
end
end
# ...
end
All other messages are added to an initially hidden area on the page, which is scrolled to the bottom as messages come in and shown once any unexpected data is received:
# display all other messages received
if data.type != 'stdout' or match
~"#msg".append(~"<p>".text(data.line).addClass(data.type))
~"#msg".scrollTop = ~"#msg".prop("scrollHeight") - ~"#msg".height
~"#msg".show if data.type != 'stdin'
end
The directory itself is extracted from CGI/rack environment variables:
# directory is DOCUMENT_ROOT + PATH_INFO
$ROOT ||= ARGV.map {|arg| arg[/^--root=(.*)/i, 1]}.compact.first
dir = ($ROOT || env['DOCUMENT_ROOT'] || Dir.pwd)
prefix = "#{env['REQUEST_URI']}/" if not env['PATH_INFO'].to_s.end_with? '/'
if env['PATH_INFO'].to_s =~ %r{(/\w[-.\w]*)+/?}
dir = File.expand_path(env['PATH_INFO'][1..-1], dir)
end
As an alternative, the directory to be used may be specified using $ROOT
,
which can be provided via the command line or by wrappers that call this
script. In fact, at the end of this script are lines (minus the __END__
of
course) that will be added to CGI wrappers that are installed:
__END__
# Customize what directory is searched
$ROOT = ENV['DOCUMENT_ROOT']
These lines can be tailored by editing the wrapper or by specifying alternate values during the installation of the wrapper itself, for example:
ruby demo/diskusage.rb --install=/var/www --root=/home/rubys
Next up, an Ajax demo.