| Add new files - warvox - VoIP based wardialing tool, forked from rapid7/warvox. | |
| Log | |
| Files | |
| Refs | |
| README | |
| --- | |
| commit aee6346ab4227e65a5cc0cf26636bd465e72a5cd | |
| parent ef6496ad1da956df421e15ce6c2eadc06044f3d7 | |
| Author: HD Moore <[email protected]> | |
| 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(-) | |
| --- | |
| diff --git a/app/assets/stylesheets/formtastic-overrides.css b/app/assets/style… | |
| @@ -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; | |
| +} | |
| diff --git a/app/controllers/calls_controller.rb b/app/controllers/calls_contro… | |
| @@ -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 =… | |
| + 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… | |
| + 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_i… | |
| + ) | |
| + 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 = ?'… | |
| + ) | |
| + | |
| + 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 answere… | |
| + :Busy => Call.count(:conditions =>['job_id = ? and busy = … | |
| + :Answered => Call.count(:conditions =>['job_id = ? and answere… | |
| + } | |
| + | |
| + 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 => … | |
| + else | |
| + format.html { render :action => "new" } | |
| + format.xml { render :xml => @call.errors, :status => :unprocessable_e… | |
| + 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_e… | |
| + 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 | |
| diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controll… | |
| @@ -0,0 +1,115 @@ | |
| +class JobsController < ApplicationController | |
| + | |
| + def index | |
| + @submitted_jobs = Job.where(:status => ['submitted', 'scheduled'], :… | |
| + @active_jobs = Job.where(:status => 'running', :completed_at => nil) | |
| + @inactive_jobs = Job.where('status NOT IN (?) OR completed_at IS NULL'… | |
| + :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_en… | |
| + 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 e… | |
| + respond_to do |format| | |
| + format.html { render :action => "new" } | |
| + format.xml { render :xml => @job.errors, :status => :… | |
| + 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 !=… | |
| + | |
| + 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 => @… | |
| + else | |
| + format.html { render :action => "new" } | |
| + format.xml { render :xml => @job.errors, :status => :unprocessable_en… | |
| + 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 | |
| diff --git a/app/helpers/calls_helper.rb b/app/helpers/calls_helper.rb | |
| @@ -0,0 +1,2 @@ | |
| +module CallsHelper | |
| +end | |
| diff --git a/app/helpers/jobs_helper.rb b/app/helpers/jobs_helper.rb | |
| @@ -0,0 +1,2 @@ | |
| +module JobsHelper | |
| +end | |
| 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 <%= @di… | |
| +</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 /> | |
| 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) %> | |
| 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… | |
| + "/" + | |
| + 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… | |
| + | |
| + <% if(job.analysis_completed_at) %> | |
| + <a class="btn btn-mini" href="<%= analyze_call_path(@p… | |
| + <a class="btn btn-mini" href="<%= reanalyze_call_path(… | |
| + <% else %> | |
| + <a class="btn btn-mini" href="<%= analyze_call_path(@p… | |
| + <% end %> | |
| + | |
| + <a class="btn btn-mini" href="<%= call_path(@project,job) %>" data… | |
| + </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> Sta… | |
| 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) %> | |
| 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) %> | |
| 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 %> | |
| 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 %> | |
| 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-confi… | |
| + </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 %>… | |
| + </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-… | |
| + </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> Sta… | |
| + | |
| +<% 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 %… | |
| + <td><%= link_to( truncate(job.project.name, :length => 25).html_safe, proj… | |
| + </tr> | |
| +<% end %> | |
| +</table> | |
| +<%= raw(will_paginate @inactive_jobs) %> | |
| +<br /> | |
| + | |
| +<script language="javascript"> | |
| + setTimeout("location.reload(true);", 20000); | |
| +</script> | |
| + | |
| +<% end %> | |
| 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 … | |
| + <%= f.text_area :range, :size => "35x5" %> | |
| + </p> | |
| + | |
| + <p> | |
| + <%= f.label :range_file, 'Or upload a file containing the target ranges' %… | |
| + <%= 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)' … | |
| + <%= f.text_field :cid_mask, :value => '1-123-456-XXXX' %> | |
| + </p> | |
| + | |
| + <p> | |
| + <%= f.submit "Create" %> | |
| + </p> | |
| +<% end %> | |
| + | |
| +<%= link_to 'Back', jobs_path %> | |
| diff --git a/app/views/jobs/new_dialer.html.erb b/app/views/jobs/new_dialer.htm… | |
| @@ -0,0 +1,21 @@ | |
| +<h1 class='title'>Dialer Job</h1> | |
| + | |
| +<%= semantic_form_for(@job, :url => dialer_job_path, :html => { :multipart => … | |
| + <%= 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-30… | |
| + :input_html => { :rows => 3, :cols => 80, :autofocus => true } | |
| + %> | |
| + <%= f.input :range_file, :as => :file, :label => 'Or upload a file con… | |
| + <%= f.input :seconds, :as => :number, :label => 'Seconds of audio to … | |
| + <%= f.input :lines, :as => :number, :label => 'Maximum number of outg… | |
| + <%= f.input :cid_mask, :as => :string, :label => 'The source Caller I… | |
| + | |
| + <%= f.action :submit, :label => 'Create', :button_html => { :class => … | |
| + | |
| + <a class="btn btn-link" href="<%= jobs_path %>" rel="tooltip" title="R… | |
| +<% end %> | |
| + | |
| +<%= set_focus('job_range') %> | |
| 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 %> | |
| 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 %> | |
| 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.n… | |
| + 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 => … | |
| + | |
| +args = Marshal.load(@job.args) rescue {} | |
| + | |
| + | |
| +WarVOX::Log.debug("Worker #{@job.id} #{@job.task} is running #{@job.task} with… | |
| + | |
| +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 … | |
| +end | |
| + | |
| [email protected]_progress(100) | |
| + | |
| +rescue ::SignalException, ::SystemExit | |
| + raise $! | |
| +rescue ::Exception => e | |
| + WarVOX::Log.warn("Worker #{@job.id} #{@job.task} threw an exception: #… | |
| + Job.update_all({ :error => "Exception: #{e.class} #{e}", :status => 'e… | |
| +end | |
| 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},… | |
| + | |
| + # 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 … | |
| + 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} comp… | |
| + | |
| + @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, … | |
| +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 ? A… | |
| + else | |
| + stale = Job.where("completed_at IS NULL AND locked_by LIKE ?",… | |
| + 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} jo… | |
| + Job.update_all({ :locked_by => nil, :status => 'abandoned' }, … | |
| + 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).limi… | |
| + return unless j | |
| + | |
| + # Try to get a lock on this job | |
| + Job.update_all({:locked_by => @cookie, :locked_at => Time.now.… | |
| + | |
| + # See if we actually got the lock | |
| + j = Job.where(:id => j.id, :status => 'scheduled', :locked_by… | |
| + | |
| + # 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(0x… | |
| +@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 |