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