Add new files - warvox - VoIP based wardialing tool, forked from rapid7/warvox.
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
       ---
 (DIR) commit aee6346ab4227e65a5cc0cf26636bd465e72a5cd
 (DIR) parent ef6496ad1da956df421e15ce6c2eadc06044f3d7
 (HTM) Author: HD Moore <hd_moore@rapid7.com>
       Date:   Tue,  1 Jan 2013 21:07:48 -0600
       
       Add new files
       
       Diffstat:
         A app/assets/stylesheets/formtastic-… |      46 +++++++++++++++++++++++++++++++
         A app/controllers/calls_controller.rb |     179 +++++++++++++++++++++++++++++++
         A app/controllers/jobs_controller.rb  |     115 +++++++++++++++++++++++++++++++
         A app/helpers/calls_helper.rb         |       2 ++
         A app/helpers/jobs_helper.rb          |       2 ++
         A app/views/calls/analyze.html.erb    |      27 +++++++++++++++++++++++++++
         A app/views/calls/edit.html.erb       |      48 +++++++++++++++++++++++++++++++
         A app/views/calls/index.html.erb      |      57 +++++++++++++++++++++++++++++++
         A app/views/calls/new.html.erb        |      43 ++++++++++++++++++++++++++++++
         A app/views/calls/show.html.erb       |      48 +++++++++++++++++++++++++++++++
         A app/views/calls/view.html.erb       |      58 ++++++++++++++++++++++++++++++
         A app/views/jobs/edit.html.erb        |      24 ++++++++++++++++++++++++
         A app/views/jobs/index.html.erb       |     104 +++++++++++++++++++++++++++++++
         A app/views/jobs/new.html.erb         |      35 +++++++++++++++++++++++++++++++
         A app/views/jobs/new_dialer.html.erb  |      21 +++++++++++++++++++++
         A app/views/jobs/run.html.erb         |       5 +++++
         A app/views/jobs/show.html.erb        |      39 +++++++++++++++++++++++++++++++
         A bin/worker.rb                       |      91 +++++++++++++++++++++++++++++++
         A bin/worker_manager.rb               |     194 ++++++++++++++++++++++++++++++
       
       19 files changed, 1138 insertions(+), 0 deletions(-)
       ---
 (DIR) diff --git a/app/assets/stylesheets/formtastic-overrides.css b/app/assets/stylesheets/formtastic-overrides.css
       @@ -0,0 +1,46 @@
       +.help-block {
       +        font-size: 13px;
       +        font-style: italic;
       +}
       +
       +.control-label {
       +        font-weight: bold;
       +        font-size: 15px;
       +}
       +
       +.formtastic .stringish input
       +{
       +  width: auto;
       +}
       +
       +.formtastic .numeric input {
       +        width: 5em;
       +}
       +
       +.formtastic .stringish input#project_name
       +{
       +  width: 500px;
       +}
       +
       +.formtastic .text textarea.project_description {
       +  width: 500px;
       +}
       +
       +.formtastic .text textarea.project_includes {
       +  width: 200px;
       +}
       +
       +.formtastic input {
       +        padding: 4px;
       +}
       +
       +.formtastic textarea {
       +        padding: 4px;
       +}
       +
       +.formtastic input.btn {
       +        padding-left: 10px;
       +        padding-right: 10px;
       +        padding-top: 5px;
       +        padding-bottom: 5px;
       +}
 (DIR) diff --git a/app/controllers/calls_controller.rb b/app/controllers/calls_controller.rb
       @@ -0,0 +1,179 @@
       +class CallsController < ApplicationController
       +
       +  # GET /calls
       +  # GET /calls.xml
       +  def index
       +    @jobs = Job.where(:status => 'answered').paginate(
       +                :page => params[:page],
       +                :order => 'id DESC',
       +                :per_page => 30
       +
       +        )
       +
       +    respond_to do |format|
       +      format.html # index.html.erb
       +      format.xml  { render :xml => @calls }
       +    end
       +  end
       +
       +  # GET /calls/1/reanalyze
       +  def reanalyze
       +          Call.update_all(['processed = ?', false], ['job_id = ?', params[:id]])
       +        j = Job.find(params[:id])
       +        j.processed = false
       +        j.save
       +
       +        redirect_to :action => 'analyze'
       +  end
       +
       +  # GET /calls/1/process
       +  # GET /calls/1/process.xml
       +  def analyze
       +          @job_id = params[:id]
       +        @job    = Job.find(@job_id)
       +
       +        if(@job.processed)
       +                redirect_to :controller => 'analyze', :action => 'view', :id => @job_id
       +                return
       +        end
       +
       +        @dial_data_total = Call.count(
       +                :conditions => [ 'job_id = ? and answered = ?', @job_id, true ]
       +        )
       +
       +        @dial_data_done = Call.count(
       +                :conditions => [ 'job_id = ? and processed = ?', @job_id, true ]
       +        )
       +
       +        ltypes = Call.find( :all, :select => 'DISTINCT line_type', :conditions => ["job_id = ?", @job_id] ).map{|r| r.line_type}
       +        res_types = {}
       +
       +        ltypes.each do |k|
       +                next if not k
       +                res_types[k.capitalize.to_sym] = Call.count(
       +                        :conditions => ['job_id = ? and line_type = ?', @job_id, k]
       +                )
       +        end
       +
       +        @lines_by_type = res_types
       +
       +        @dial_data_todo = Call.where(:job_id => @job_id).paginate(
       +                :page => params[:page],
       +                :order => 'number ASC',
       +                :per_page => 50,
       +                :conditions => [ 'answered = ? and processed = ? and busy = ?', true, false, false ]
       +        )
       +
       +        if @dial_data_todo.length > 0
       +        res = @job.schedule(:analysis)
       +                unless res
       +                        flash[:error] = "Unable to launch analysis job"
       +                end
       +        end
       +  end
       +
       +  # GET /calls/1/view
       +  # GET /calls/1/view.xml
       +  def view
       +    @calls = Call.where(:job_id => params[:id]).paginate(
       +                :page => params[:page],
       +                :order => 'number ASC',
       +                :per_page => 30
       +        )
       +
       +        unless @calls and @calls.length > 0
       +                redirect_to :action => :index
       +                return
       +        end
       +        @call_results = {
       +                :Timeout  => Call.count(:conditions =>['job_id = ? and answered = ?', params[:id], false]),
       +                :Busy     => Call.count(:conditions =>['job_id = ? and busy = ?', params[:id], true]),
       +                :Answered => Call.count(:conditions =>['job_id = ? and answered = ?', params[:id], true]),
       +        }
       +
       +        respond_to do |format|
       +      format.html # index.html.erb
       +      format.xml  { render :xml => @calls }
       +    end
       +  end
       +
       +  # GET /calls/1
       +  # GET /calls/1.xml
       +  def show
       +    @call = Call.find(params[:id])
       +
       +        unless @call
       +                redirect_to :action => :index
       +                return
       +        end
       +
       +    respond_to do |format|
       +      format.html # show.html.erb
       +      format.xml  { render :xml => @call }
       +    end
       +  end
       +
       +  # GET /calls/new
       +  # GET /calls/new.xml
       +  def new
       +    @call = Call.new
       +
       +    respond_to do |format|
       +      format.html # new.html.erb
       +      format.xml  { render :xml => @call }
       +    end
       +  end
       +
       +  # GET /calls/1/edit
       +  def edit
       +    @call = Call.find(params[:id])
       +  end
       +
       +  # POST /calls
       +  # POST /calls.xml
       +  def create
       +    @call = Call.new(params[:call])
       +
       +    respond_to do |format|
       +      if @call.save
       +        flash[:notice] = 'Call was successfully created.'
       +        format.html { redirect_to(@call) }
       +        format.xml  { render :xml => @call, :status => :created, :location => @call }
       +      else
       +        format.html { render :action => "new" }
       +        format.xml  { render :xml => @call.errors, :status => :unprocessable_entity }
       +      end
       +    end
       +  end
       +
       +  # PUT /calls/1
       +  # PUT /calls/1.xml
       +  def update
       +    @call = Call.find(params[:id])
       +
       +    respond_to do |format|
       +      if @call.update_attributes(params[:call])
       +        flash[:notice] = 'Call was successfully updated.'
       +        format.html { redirect_to(@call) }
       +        format.xml  { head :ok }
       +      else
       +        format.html { render :action => "edit" }
       +        format.xml  { render :xml => @call.errors, :status => :unprocessable_entity }
       +      end
       +    end
       +  end
       +
       +  # DELETE /calls/1
       +  # DELETE /calls/1.xml
       +  def destroy
       +
       +    @job = Job.find(params[:id])
       +        @job.destroy
       +
       +    respond_to do |format|
       +      format.html { redirect_to :action => 'index' }
       +      format.xml  { head :ok }
       +    end
       +  end
       +
       +end
 (DIR) diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb
       @@ -0,0 +1,115 @@
       +class JobsController < ApplicationController
       +
       +  def index
       +          @submitted_jobs = Job.where(:status => ['submitted', 'scheduled'], :completed_at => nil)
       +        @active_jobs    = Job.where(:status => 'running', :completed_at => nil)
       +        @inactive_jobs = Job.where('status NOT IN (?) OR completed_at IS NULL', ['submitted', 'scheduled', 'running']).paginate(
       +                :page => params[:page],
       +                :order => 'id DESC',
       +                :per_page => 30
       +        )
       +
       +    respond_to do |format|
       +      format.html # index.html.erb
       +      format.xml  { render :xml => @active_jobs + @submitted_jobs }
       +    end
       +  end
       +
       +
       +  def new_dialer
       +    @job = Job.new
       +    if @project
       +            @job.project = @project
       +    else
       +            @job.project = Project.last
       +    end
       +
       +    respond_to do |format|
       +      format.html # new.html.erb
       +      format.xml  { render :xml => @job }
       +    end
       +  end
       +
       +  def dialer
       +    @job = Job.new(params[:job])
       +    @job.created_by = current_user.login
       +    @job.task = 'dialer'
       +        @job.range.gsub!(/[^0-9X:,\n]/, '')
       +        @job.cid_mask.gsub!(/[^0-9X]/, '') if @job.cid_mask != "SELF"
       +
       +        if @job.range_file.to_s != ""
       +                @job.range = @job.range_file.read.gsub(/[^0-9X:,\n]/, '')
       +        end
       +
       +    respond_to do |format|
       +      if @job.schedule
       +        flash[:notice] = 'Job was successfully created.'
       +            format.html { redirect_to :action => :index }
       +        format.xml  { render :xml => @job, :status => :created }
       +      else
       +        format.html { render :action => "new_dialer" }
       +        format.xml  { render :xml => @job.errors, :status => :unprocessable_entity }
       +      end
       +    end
       +  end
       +
       +  def stop
       +    @job = Job.find(params[:id])
       +        @job.stop
       +        flash[:notice] = "Job has been cancelled"
       +    redirect_to :action => 'index'
       +  end
       +
       +  def create
       +
       +        @job = Job.new(params[:job])
       +
       +    if(Provider.find_all_by_enabled(true).length == 0)
       +                @job.errors.add(:base, "No providers have been configured or enabled, this job cannot be run")
       +                respond_to do |format|
       +                        format.html { render :action => "new" }
       +                        format.xml  { render :xml => @job.errors, :status => :unprocessable_entity }
       +                end
       +                return
       +        end
       +
       +        @job.status       = 'submitted'
       +        @job.progress     = 0
       +        @job.started_at   = nil
       +        @job.completed_at = nil
       +        @job.range        = @job_range.gsub(/[^0-9X:,\n]/m, '')
       +        @job.cid_mask     = @cid_mask.gsub(/[^0-9X]/m, '') if @job.cid_mask != "SELF"
       +
       +        if(@job.range_file.to_s != "")
       +                @job.range = @job.range_file.read.gsub(/[^0-9X:,\n]/m, '')
       +        end
       +
       +    respond_to do |format|
       +      if @job.save
       +        flash[:notice] = 'Job was successfully created.'
       +
       +                res = @job.schedule(:dialer)
       +                unless res
       +                        flash[:error] = "Unable to launch dialer job"
       +                end
       +
       +            format.html { redirect_to :action => 'index' }
       +        format.xml  { render :xml => @job, :status => :created, :location => @job }
       +      else
       +        format.html { render :action => "new" }
       +        format.xml  { render :xml => @job.errors, :status => :unprocessable_entity }
       +      end
       +    end
       +  end
       +
       +  def destroy
       +    @job = Job.find(params[:id])
       +    @job.destroy
       +
       +    respond_to do |format|
       +      format.html { redirect_to(jobs_url) }
       +      format.xml  { head :ok }
       +    end
       +  end
       +
       +end
 (DIR) diff --git a/app/helpers/calls_helper.rb b/app/helpers/calls_helper.rb
       @@ -0,0 +1,2 @@
       +module CallsHelper
       +end
 (DIR) diff --git a/app/helpers/jobs_helper.rb b/app/helpers/jobs_helper.rb
       @@ -0,0 +1,2 @@
       +module JobsHelper
       +end
 (DIR) diff --git a/app/views/calls/analyze.html.erb b/app/views/calls/analyze.html.erb
       @@ -0,0 +1,27 @@
       +<% if @dial_data_todo.length > 0 %>
       +
       +<h1 class='title'>
       +        Analyzing Audio for <%= @dial_data_total-@dial_data_done %> of <%= @dial_data_total %> Calls...
       +</h1>
       +
       +<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
       +<tr>
       +<% if @dial_data_done > 0 %>
       +        <td align='center'>
       +                <%= render :partial => 'shared/graphs/lines_by_type' %>
       +        </td>
       +<% end %>
       +</tr>
       +</table>
       +
       +<script language="javascript">
       +        setTimeout("location.reload(true);", 10000);
       +</script>
       +
       +<% else %>
       +
       +<h1 class='title'>No Completed Calls Found</h1>
       +
       +<% end %>
       +
       +<br />
 (DIR) diff --git a/app/views/calls/edit.html.erb b/app/views/calls/edit.html.erb
       @@ -0,0 +1,48 @@
       +<h1>Editing call</h1>
       +
       +<%= form_for(@call) do |f| %>
       +  <%= f.error_messages %>
       +
       +  <p>
       +    <%= f.label :number %><br />
       +    <%= f.text_field :number %>
       +  </p>
       +  <p>
       +    <%= f.label :cid %><br />
       +    <%= f.text_field :cid %>
       +  </p>
       +  <p>
       +    <%= f.label :job_id %><br />
       +    <%= f.text_field :job_id %>
       +  </p>
       +  <p>
       +    <%= f.label :provider %><br />
       +    <%= f.text_field :provider %>
       +  </p>
       +  <p>
       +    <%= f.label :completed %><br />
       +    <%= f.check_box :completed %>
       +  </p>
       +  <p>
       +    <%= f.label :busy %><br />
       +    <%= f.check_box :busy %>
       +  </p>
       +  <p>
       +    <%= f.label :seconds %><br />
       +    <%= f.text_field :seconds %>
       +  </p>
       +  <p>
       +    <%= f.label :ringtime %><br />
       +    <%= f.text_field :ringtime %>
       +  </p>
       +  <p>
       +    <%= f.label :rawfile %><br />
       +    <%= f.text_field :rawfile %>
       +  </p>
       +  <p>
       +    <%= f.submit "Update" %>
       +  </p>
       +<% end %>
       +
       +<%= link_to 'Show', @call %> |
       +<%= link_to 'Back', calls_path(@project) %>
 (DIR) diff --git a/app/views/calls/index.html.erb b/app/views/calls/index.html.erb
       @@ -0,0 +1,57 @@
       +<% if @jobs.length > 0 %>
       +<h1 class='title'>Completed Jobs</h1>
       +
       +<%= raw(will_paginate @jobs) %>
       +<table class='table table-striped table-bordered' width='90%'>
       +  <thead>
       +  <tr>
       +    <th>ID</th>
       +    <th>Range</th>
       +        <th>CallerID</th>
       +    <th>Connected</th>
       +    <th>Date</th>
       +    <th>Actions</th>
       +  </tr>
       +  </thead>
       +  <tbody>
       +
       +<% @jobs.sort{|a,b| b.id <=> a.id}.each do |job|  %>
       +  <tr>
       +    <td><%=h job.id %></td>
       +    <td><%=h job.range %></td>
       +    <td><%=h job.cid_mask %></td>
       +    <td><%=h (
       +                Call.count(:conditions => ['job_id = ? and processed = ?', job.id, true]).to_s +
       +                "/" +
       +                Call.count(:conditions => ['job_id = ?', job.id]).to_s
       +        )%></td>
       +    <td><%=h job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S") %></td>
       +
       +    <td>
       +        <a class="btn btn-mini" href="<%= view_call_path(@project,job) %>" rel="tooltip" title="View Call Connections" ><i class="icon-bar-chart"></i></a>
       +
       +                <% if(job.analysis_completed_at) %>
       +                        <a class="btn btn-mini" href="<%= analyze_call_path(@project,job) %>" rel="tooltip" title="View Call Analysis"><i class="icon-eye-open"></i></a>
       +                        <a class="btn btn-mini" href="<%= reanalyze_call_path(@project,job) %>" data-confirm="Reprocess this job?" rel="nofollow tooltip" title="Rerun Call Analysis"><i class="icon-refresh"></i></a>
       +                <% else %>
       +                        <a class="btn btn-mini" href="<%= analyze_call_path(@project,job) %>" data-confirm="Analyze this job?" rel="nofollow tooltip" title="Run Call Analysis"><i class="icon-bolt"></i></a>
       +                <% end %>
       +
       +            <a class="btn btn-mini" href="<%= call_path(@project,job) %>" data-confirm="Delete all data for this job?" data-method="delete" rel="nofollow tooltip" title="Delete Call Data"><i class="icon-trash"></i></a>
       +        </td>
       +  </tr>
       +
       +<% end %>
       +</tbody>
       +</table>
       +
       +<%= raw(will_paginate @jobs) %>
       +
       +<% else %>
       +
       +<h1 class='title'>No Completed Jobs</h1>
       +<br/>
       +
       +<% end %>
       +
       +<a class="btn" href="<%= new_dialer_job_path %>"><i class="icon-plus"></i> Start Job </a>
 (DIR) diff --git a/app/views/calls/new.html.erb b/app/views/calls/new.html.erb
       @@ -0,0 +1,43 @@
       +<h1>New call</h1>
       +
       +<%= form_for(@call) do |f| %>
       +  <%= f.error_messages %>
       +
       +  <p>
       +    <%= f.label :number %><br />
       +    <%= f.text_field :number %>
       +  </p>
       +  <p>
       +    <%= f.label :job_id %><br />
       +    <%= f.text_field :job_id %>
       +  </p>
       +  <p>
       +    <%= f.label :provider %><br />
       +    <%= f.text_field :provider %>
       +  </p>
       +  <p>
       +    <%= f.label :completed %><br />
       +    <%= f.check_box :completed %>
       +  </p>
       +  <p>
       +    <%= f.label :busy %><br />
       +    <%= f.check_box :busy %>
       +  </p>
       +  <p>
       +    <%= f.label :seconds %><br />
       +    <%= f.text_field :seconds %>
       +  </p>
       +  <p>
       +    <%= f.label :ringtime %><br />
       +    <%= f.text_field :ringtime %>
       +  </p>
       +  <p>
       +    <%= f.label :rawfile %><br />
       +    <%= f.text_field :rawfile %>
       +  </p>
       +  <p>
       +    <%= f.submit "Create" %>
       +  </p>
       +<% end %>
       +
       +<%= link_to 'Back', calls_path(@project) %>
 (DIR) diff --git a/app/views/calls/show.html.erb b/app/views/calls/show.html.erb
       @@ -0,0 +1,48 @@
       +<p>
       +  <b>Number:</b>
       +  <%=h @call.number %>
       +</p>
       +
       +<p>
       +  <b>CallerID:</b>
       +  <%=h @call.cid %>
       +</p>
       +
       +<p>
       +  <b>Dial job:</b>
       +  <%=h @call.job_id %>
       +</p>
       +
       +<p>
       +  <b>Provider:</b>
       +  <%=h @call.provider %>
       +</p>
       +
       +<p>
       +  <b>Completed:</b>
       +  <%=h @call.completed %>
       +</p>
       +
       +<p>
       +  <b>Busy:</b>
       +  <%=h @call.busy %>
       +</p>
       +
       +<p>
       +  <b>Seconds:</b>
       +  <%=h @call.seconds %>
       +</p>
       +
       +<p>
       +  <b>Ringtime:</b>
       +  <%=h @call.ringtime %>
       +</p>
       +
       +<p>
       +  <b>Rawfile:</b>
       +  <%=h @call.rawfile %>
       +</p>
       +
       +
       +<%= link_to 'Edit', edit_call_path(@project, @call) %> |
       +<%= link_to 'Back', calls_path(@project) %>
 (DIR) diff --git a/app/views/calls/view.html.erb b/app/views/calls/view.html.erb
       @@ -0,0 +1,58 @@
       +<% if @calls %>
       +
       +
       +<h1 class='title'>Dial Results for Job <%=@calls[0].job_id%></h1>
       +
       +<%= raw(will_paginate @calls) %>
       +<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
       +<tr>
       +        <td align='center'>
       +                <%= render :partial => 'shared/graphs/call_results' %>
       +        </td>
       +</tr>
       +</table>
       +
       +<br/>
       +
       +<table class='table table-striped table-bordered' width='90%' id='results'>
       +  <thead>
       +  <tr>
       +    <th>Number</th>
       +    <th>CallerID</th>
       +    <th>Provider</th>
       +    <th>Completed</th>
       +    <th>Busy</th>
       +    <th>Seconds</th>
       +        <th>Ring Time</th>
       +  </tr>
       +  </thead>
       +  <tbody>
       +<% for call in @calls.sort{|a,b| a.number <=> b.number } %>
       +  <tr>
       +    <td><%= call.number %></td>
       +    <td><%= call.cid %></td>
       +    <td><%= call.provider.name %></td>
       +    <td><%= call.completed %></td>
       +    <td><%= call.busy %></td>
       +    <td><%= call.seconds %></td>
       +    <td><%= call.ringtime.to_i %></td>
       +  </tr>
       +<% end %>
       +  </tbody>
       +</table>
       +<%= raw(will_paginate @calls) %>
       +
       +<% else %>
       +
       +<h1 class='title'>No Dial Results</h1>
       +
       +<% end %>
       +<br />
       +
       +<%= javascript_tag do %>
       +// For fixed width containers
       +$('#results').dataTable({
       +  "sDom": "<'row'<'span6'l><'span6'f>r>t<'row'<'span6'i><'span6'p>>",
       +  "sPaginationType": "bootstrap"
       +});
       +<% end %>
 (DIR) diff --git a/app/views/jobs/edit.html.erb b/app/views/jobs/edit.html.erb
       @@ -0,0 +1,24 @@
       +<h1 class='title'>Modify Job</h1>
       +
       +<%= form_for(@job) do |f| %>
       +  <%= f.error_messages %>
       +
       +  <p>
       +    <%= f.label :range %><br />
       +    <%= f.text_area :range, :size => "35x5" %>
       +  </p>
       +  <p>
       +    <%= f.label :seconds %><br />
       +    <%= f.text_field :seconds %>
       +  </p>
       +  <p>
       +    <%= f.label :lines %><br />
       +    <%= f.text_field :lines %>
       +  </p>
       +  <p>
       +    <%= f.submit "Update" %>
       +  </p>
       +<% end %>
       +
       +<%= link_to 'Show', @job %> |
       +<%= link_to 'Back', jobs_path %>
 (DIR) diff --git a/app/views/jobs/index.html.erb b/app/views/jobs/index.html.erb
       @@ -0,0 +1,104 @@
       +<% if(@submitted_jobs.length > 0) %>
       +
       +<h1 class='title'>Submitted Jobs</h1>
       +
       +<table class='table table-striped table-bordered' width='90%'>
       +  <tr>
       +    <th>ID</th>
       +    <th>Task</th>
       +    <th>Status</th>
       +    <th>Submitted Time</th>
       +    <th>Actions</th>
       +  </tr>
       +
       +<% @submitted_jobs.each do |job| %>
       +  <tr>
       +    <td><%= job.id %></td>
       +    <td><%= format_job_details(job) %></td>
       +        <td><%= format_job_status(job) %></td>
       +    <td><%= job.created_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
       +
       +    <td>
       +                <a class="btn btn-mini" href="<%= job_path(job) %>" data-confirm="Remove this job?" data-method="delete" rel="nofollow tooltip" title="Remove Job"><i class="icon-trash"></i></a>
       +        </td>
       +  </tr>
       +<% end %>
       +</table>
       +<br />
       +<% end %>
       +
       +<% if(@active_jobs.length > 0) %>
       +
       +<h1 class='title'>Active Jobs</h1>
       +
       +<table class='table table-striped table-bordered' width='90%'>
       +  <tr>
       +    <th>ID</th>
       +    <th>Task</th>
       +    <th>Progress</th>
       +    <th>Submitted Time</th>
       +    <th>Actions</th>
       +  </tr>
       +
       +<% @active_jobs.each do |job| %>
       +  <tr class='active_job_row'>
       +    <td><%= job.id %></td>
       +    <td><%= format_job_details(job) %></td>
       +    <td valign='center'>
       +                <div class="progress progress-success progress-striped">
       +                        <div class="bar" style="width: <%= job.progress %>%">
       +                                <span class='progress_pct'><%= job.progress %>%</span>
       +                        </div>
       +                </div>
       +        </td>
       +    <td><%= job.created_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
       +    <td>
       +                <a class="btn btn-mini" href="<%= stop_job_path(job) %>" data-confirm="Terminate this job?" rel="nofollow tooltip" title="Terminate Job"><i class="icon-stop"></i></a>
       +        </td>
       +  </tr>
       +<% end %>
       +</table>
       +<br />
       +
       +<% end %>
       +
       +<% if (@active_jobs.length + @submitted_jobs.length == 0) %>
       +<h1 class='title'>No Active Jobs</h1>
       +<% end %>
       +
       +<a class="btn" href="<%= new_dialer_job_path %>"><i class="icon-plus"></i> Start Job </a>
       +
       +<% if(@inactive_jobs.length > 0) %>
       +<br/><br/>
       +<h1 class='title'>Inactive Jobs</h1>
       +
       +<%= raw(will_paginate @inactive_jobs) %>
       +<table class='table table-striped table-bordered' width='90%'>
       +  <tr>
       +    <th>ID</th>
       +    <th>Task</th>
       +    <th>Status</th>
       +    <th>Created</th>
       +    <th>Completed</th>
       +    <th>Project</th>
       +  </tr>
       +
       +<% @inactive_jobs.each do |job| %>
       +  <tr class='active_job_row'>
       +    <td><%= job.id %></td>
       +    <td><%= format_job_details(job) %></td>
       +        <td><%= format_job_status(job) %></td>
       +    <td><%= job.created_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
       +    <td><%= job.completed_at ? job.completed_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") : "incomplete" %></td>
       +    <td><%= link_to( truncate(job.project.name, :length => 25).html_safe, project_path(job.project)) %></td>
       +  </tr>
       +<% end %>
       +</table>
       +<%= raw(will_paginate @inactive_jobs) %>
       +<br />
       +
       +<script language="javascript">
       +        setTimeout("location.reload(true);", 20000);
       +</script>
       +
       +<% end %>
 (DIR) diff --git a/app/views/jobs/new.html.erb b/app/views/jobs/new.html.erb
       @@ -0,0 +1,35 @@
       +<h1 class='title'>Submit a New Job</h1>
       +
       +<%= form_for(@job, :html => { :multipart => true }) do |f| %>
       +  <%= f.error_messages %>
       +  <p>
       +    <%= f.label :range, 'Specify target telephone range(s) (1-123-456-7890 or 1-123-456-XXXX or 1-123-300-1000:1-123-400-2000)' %><br />
       +    <%= f.text_area :range, :size => "35x5" %>
       +  </p>
       +
       +  <p>
       +    <%= f.label :range_file, 'Or upload a file containing the target ranges' %><br />
       +    <%= f.file_field :range_file %>
       +  </p>
       +
       +  <p>
       +    <%= f.label :seconds, 'Seconds of audio to capture' %><br />
       +    <%= f.text_field :seconds, :value => 53 %>
       +  </p>
       +
       +  <p>
       +    <%= f.label :lines, 'Maximum number of outgoing lines' %><br />
       +    <%= f.text_field :lines, :value => 10 %>
       +  </p>
       +
       +  <p>
       +    <%= f.label :lines, 'The source Caller ID range (1-555-555-55XX or SELF)' %><br />
       +    <%= f.text_field :cid_mask, :value => '1-123-456-XXXX' %>
       +  </p>
       +
       +  <p>
       +    <%= f.submit "Create" %>
       +  </p>
       +<% end %>
       +
       +<%= link_to 'Back', jobs_path %>
 (DIR) diff --git a/app/views/jobs/new_dialer.html.erb b/app/views/jobs/new_dialer.html.erb
       @@ -0,0 +1,21 @@
       +<h1 class='title'>Dialer Job</h1>
       +
       +<%= semantic_form_for(@job, :url => dialer_job_path, :html => { :multipart => true, :method => :put }) do |f| %>
       +        <%= f.input :project, :as => :select %>
       +
       +        <%= f.input :range, :as => :text,
       +                :label => 'Target telephone range(s)',
       +                :hint => 'Examples: 1-123-456-7890 / 1-123-456-XXXX / 1-123-300-1000:1-123-400-2000',
       +                :input_html => { :rows => 3, :cols => 80, :autofocus => true }
       +        %>
       +        <%= f.input :range_file, :as => :file, :label => 'Or upload a file containing the target ranges' %>
       +        <%= f.input :seconds,  :as => :number, :label => 'Seconds of audio to capture', :input_html => { :value => 53 } %>
       +        <%= f.input :lines,  :as => :number, :label => 'Maximum number of outgoing lines', :input_html => { :value => 1 } %>
       +        <%= f.input :cid_mask,  :as => :string, :label => 'The source Caller ID range (1-555-555-55XX or SELF)', :input_html => { :value => '1-123-456-XXXX' } %>
       +
       +        <%= f.action :submit, :label => 'Create', :button_html => { :class => 'btn btn-large fbtn' }  %>
       +
       +        <a class="btn btn-link" href="<%= jobs_path %>" rel="tooltip" title="Return to jobs"><i class="icon-return"></i>Cancel</a>
       +<% end %>
       +
       +<%= set_focus('job_range') %>
 (DIR) diff --git a/app/views/jobs/run.html.erb b/app/views/jobs/run.html.erb
       @@ -0,0 +1,5 @@
       +<h1 class='title'>Run Job</h1>
       +
       +Running this job...<br/>
       +
       +<%= link_to 'Back', jobs_path %>
 (DIR) diff --git a/app/views/jobs/show.html.erb b/app/views/jobs/show.html.erb
       @@ -0,0 +1,39 @@
       +<h1 class='title'>Show Job</h1>
       +<p>
       +  <b>Range:</b>
       +  <%=h @job.range %>
       +</p>
       +
       +<p>
       +  <b>Seconds:</b>
       +  <%=h @job.seconds %>
       +</p>
       +
       +<p>
       +  <b>Lines:</b>
       +  <%=h @job.lines %>
       +</p>
       +
       +<p>
       +  <b>Status:</b>
       +  <%=h @job.status %>
       +</p>
       +
       +<p>
       +  <b>Progress:</b>
       +  <%=h @job.progress %>
       +</p>
       +
       +<p>
       +  <b>Started at:</b>
       +  <%=h @job.started_at %>
       +</p>
       +
       +<p>
       +  <b>Completed at:</b>
       +  <%=h @job.completed_at %>
       +</p>
       +
       +
       +<%= link_to 'Edit', edit_job_path(@job) %> |
       +<%= link_to 'Back', jobs_path %>
 (DIR) diff --git a/bin/worker.rb b/bin/worker.rb
       @@ -0,0 +1,91 @@
       +#!/usr/bin/env ruby
       +###################
       +
       +#
       +# Load the library path
       +#
       +base = __FILE__
       +while File.symlink?(base)
       +        base = File.expand_path(File.readlink(base), File.dirname(base))
       +end
       +$:.unshift(File.join(File.expand_path(File.dirname(base)), '..', 'lib'))
       +
       +require 'warvox'
       +require 'fileutils'
       +
       +
       +ENV['RAILS_ENV'] ||= 'production'
       +$:.unshift(File.join(File.expand_path(File.dirname(base)), '..'))
       +
       +@task = nil
       +@job  = nil
       +
       +def usage
       +        $stderr.puts "Usage: #{$0} [JID]"
       +        exit(1)
       +end
       +
       +def stop
       +        if @task
       +                @task.stop() rescue nil
       +        end
       +        if @job
       +                Job.update_all({ :status => 'stopped', :completed_at => Time.now }, { :id => @job.id })
       +        end
       +        exit(0)
       +end
       +
       +#
       +# Script
       +#
       +
       +jid = ARGV.shift() || usage()
       +if (jid and jid =="-h") or (! jid)
       +        usage()
       +end
       +
       +require 'config/boot'
       +require 'config/environment'
       +
       +trap("SIGTERM") { stop() }
       +
       +jid = jid.to_i
       +
       +@job = Job.where(:id => jid).first
       +
       +unless @job
       +        $stderr.puts "Error: Specified job not found"
       +        WarVOX::Log.warn("Worker rejected invalid Job #{jid}")
       +        exit(1)
       +end
       +
       +$0 = "warvox worker: #{jid} "
       +
       +Job.update_all({ :started_at => Time.now.utc, :status => 'running'}, { :id => @job.id })
       +
       +args = Marshal.load(@job.args) rescue {}
       +
       +
       +WarVOX::Log.debug("Worker #{@job.id} #{@job.task} is running #{@job.task} with parameters #{ args.inspect }")
       +
       +begin
       +
       +case @job.task
       +when 'dialer'
       +        @task = WarVOX::Jobs::Dialer.new(@job.id, args)
       +        @task.start
       +when 'analysis'
       +        @task = WarVOX::Jobs::Analysis.new(@job.id, args)
       +        @task.start
       +else
       +        Job.update_all({ :error => 'unsupported', :status => 'error' }, { :id => @job.id })
       +end
       +
       +@job.update_progress(100)
       +
       +rescue ::SignalException, ::SystemExit
       +        raise $!
       +rescue ::Exception => e
       +        WarVOX::Log.warn("Worker #{@job.id} #{@job.task} threw an exception: #{e.class} #{e} #{e.backtrace}")
       +        Job.update_all({ :error => "Exception: #{e.class} #{e}", :status => 'error', :completed_at => Time.now.utc }, { :id => @job.id })
       +end
 (DIR) diff --git a/bin/worker_manager.rb b/bin/worker_manager.rb
       @@ -0,0 +1,194 @@
       +#!/usr/bin/env ruby
       +###################
       +
       +#
       +# Load the library path
       +#
       +base = __FILE__
       +while File.symlink?(base)
       +        base = File.expand_path(File.readlink(base), File.dirname(base))
       +end
       +$:.unshift(File.join(File.expand_path(File.dirname(base)), '..', 'lib'))
       +
       +@worker_path = File.expand_path(File.join(File.dirname(base), "worker.rb"))
       +
       +require 'warvox'
       +require 'socket'
       +
       +ENV['RAILS_ENV'] ||= 'production'
       +
       +$:.unshift(File.join(File.expand_path(File.dirname(base)), '..'))
       +require 'config/boot'
       +require 'config/environment'
       +
       +
       +@jobs = []
       +
       +def stop
       +        WarVOX::Log.info("Worker Manager is terminating due to signal")
       +
       +        unless @jobs.length > 0
       +                exit(0)
       +        end
       +
       +        # Update the database
       +        Job.update_all({ :status => "stopped", :completed_at => Time.now.utc}, { :id => @jobs.map{|j| j[:id] } })
       +
       +        # Signal running jobs to shut down
       +        @jobs.map{|j| Process.kill("TERM", j[:pid]) rescue nil }
       +
       +        # Sleep for five seconds
       +        sleep(5)
       +
       +        # Forcibly kill any remaining job processes
       +        @jobs.map{|j| Process.kill("KILL", j[:pid]) rescue nil }
       +
       +        exit(0)
       +end
       +
       +
       +def clear_zombies
       +        while ( r = Process.waitpid(-1, Process::WNOHANG) rescue nil ) do
       +        end
       +end
       +
       +def schedule_job(j)
       +        WarVOX::Log.debug("Worker Manager is launching job #{j.id}")
       +        @jobs <<  {
       +                :id  => j.id,
       +                :pid => Process.fork { exec("#{@worker_path} #{j.id}") }
       +        }
       +end
       +
       +def stop_cancelled_jobs
       +        jids = []
       +        @jobs.each do |x|
       +                jids << x[:id]
       +        end
       +
       +        return if jids.length == 0
       +        Job.where(:status => 'cancelled', :id => jids).find_each do |j|
       +                job = @jobs.select{ |o| o[:id] == j.id }.first
       +                next unless job and job[:pid]
       +                pid = job[:pid]
       +
       +                WarVOX::Log.debug("Worker Manager is killing job #{j.id} with PID #{pid}")
       +                Process.kill('TERM', pid)
       +        end
       +end
       +
       +def clear_completed_jobs
       +        dead_pids = []
       +        dead_jids = []
       +
       +        @jobs.each do |j|
       +                alive = Process.kill(0, j[:pid]) rescue nil
       +                next if alive
       +                dead_pids << j[:pid]
       +                dead_jids << j[:id]
       +        end
       +
       +        return unless dead_jids.length > 0
       +
       +        WarVOX::Log.debug("Worker Manager is clearing #{dead_pids.length} completed jobs")
       +
       +        @jobs = @jobs.reject{|x| dead_pids.include?( x[:pid] ) }
       +
       +        # Mark failed/crashed jobs as completed
       +        Job.update_all({ :completed_at => Time.now.utc }, { :id => dead_jids, :completed_at => nil })
       +end
       +
       +def clear_stale_jobs
       +        jids  = @jobs.map{|x| x[:id] }
       +        stale = nil
       +
       +        if jids.length > 0
       +                stale = Job.where("completed_at IS NULL AND locked_by LIKE ? AND id NOT IN (?)", Socket.gethostname + "^%", jids)
       +        else
       +                stale = Job.where("completed_at IS NULL AND locked_by LIKE ?", Socket.gethostname + "^%")
       +        end
       +
       +        dead = []
       +        pids = {}
       +
       +        # Extract the PID from the locked_by cookie for each job
       +        stale.each do |j|
       +                host, pid, uniq = j.locked_by.to_s.split("^", 3)
       +                next unless (pid and uniq)
       +                pids[pid] ||= []
       +                pids[pid]  << j
       +        end
       +
       +        # Identify dead processes (must be same user or root)
       +        pids.keys.each do |pid|
       +                alive =        Process.kill(0, pid.to_i) rescue nil
       +                next if alive
       +                pids[pid].each do |j|
       +                        dead << j.id
       +                end
       +        end
       +
       +        # Mark these jobs as abandoned
       +        if dead.length > 0
       +                WarVOX::Log.debug("Worker Manager is marking #{dead.length} jobs as abandoned")
       +                Job.update_all({ :locked_by => nil, :status => 'abandoned' }, { :id => dead })
       +        end
       +end
       +
       +def schedule_submitted_jobs
       +        loop do
       +                # Look for a candidate job with no current owner
       +                j  = Job.where(:status => 'submitted', :locked_by => nil).limit(1).first
       +                return unless j
       +
       +                # Try to get a lock on this job
       +                Job.update_all({:locked_by => @cookie, :locked_at => Time.now.utc, :status => 'scheduled'}, {:id => j.id, :locked_by => nil})
       +
       +                # See if we actually got the lock
       +                j  = Job.where(:id => j.id, :status => 'scheduled', :locked_by => @cookie).limit(1).first
       +
       +                # Try again if we lost the race,
       +                next unless j
       +
       +                # Hurray, we got a job, run it
       +                schedule_job(j)
       +
       +                return true
       +        end
       +end
       +
       +#
       +# Main
       +#
       +
       +trap("SIGINT")  { stop() }
       +trap("SIGTERM") { stop() }
       +
       +@cookie   = Socket.gethostname + "^" + $$.to_s + "^" + sprintf("%.8x", rand(0x100000000))
       +@max_jobs = 3
       +
       +
       +WarVOX::Log.info("Worker Manager initialized with cookie #{@cookie}")
       +
       +loop do
       +        $0 = "warvox manager: #{@jobs.length} active jobs (cookie : #{@cookie})"
       +
       +        # Clear any zombie processes
       +        clear_zombies()
       +
       +        # Clear any completed jobs
       +        clear_completed_jobs()
       +
       +        # Stop any jobs cancelled by the user
       +        stop_cancelled_jobs()
       +
       +        # Clear locks on any stale jobs from this host
       +        clear_stale_jobs()
       +
       +        while @jobs.length < @max_jobs
       +                break unless schedule_submitted_jobs
       +        end
       +
       +        # Sleep between 3-8 seconds before re-entering the loop
       +        sleep(rand(5) + 3)
       +end