Dialer: tracking calls with Asterisk/AMI

Long story short: I was asked to write a real time dialer callable from a web page that takes the number from the parameters received, make the call, and play a pre-recorded message when the called picks up. This post is not about the dialer itself, it’s about tracking the call originated by it, detect the “answer” event, detect the “hangup” event and save a CDR.

The approach I chose involves AMI to generate and track the calls. It offers a really nice interface based on a text protocol and the most important thing, the API can be mapped in almost any programming language like C# (Asterisk.NET).

Some of the protocol internals

  1. The connection is made via TCP/IP sockets using user/password authentication.
  2. Once logged in, you receive every event on the system, even if that event doesn’t belong to you. This could be a real mess because it introduces a lot of noise into the dialer and the software must support some kind of filtering mechanism.
  3. Every event comes with certain data, including a field named Uniqueid which relates the events to a transaction.
  4. The order the events were received not necessarily match the order they were generated.

The native way to generate a call is through the OriginateAction but the problem is that OriginateAction doesn’t reply with the uniqueID of the transaction, it just tells you whether the call was successfully queued or not. Then, you start receiving the events with the uniqueIDs but you have no way to know if that call belongs to you, to another dialer instance, to a human making a call, etc.

Searching the web I found what I thought was the solution: use asynchronous origination.  The usual OriginateAction is a blocking call that returns only if success or failure. With async origination the answer to the request is delivered through the OriginateResponse event and with the parameters you receive, the so wanted uniqueID. I built the dialer on top of this “fix” just to later discover that if the call fails, the uniqueID is empty. Once again, you have no way to know which call failed and for what reason.

I was so pissed off because something as basic as this wasn’t correctly available, or at least, there is no documentation to expose an standard solution. I started considering parallel approaches to abandon AMI but reading the input parameters for OriginateAction a dirty workaround came to my mind.

OriginateAction takes as input a field named CallerID, this field is always available and always filled with information. I used this field to uniquely identify the call whether it succeeded or not. In my particular case, the calls goes to a GSM network so overwriting the callerID has no effect and the call follows its normal destination.

Bellow is part of the dialer class. If you can read C#, it would explain itself.

[csharp]

public class Dialer
{
ManagerConnection _managerConnection;
Timer _timer;
public delegate void CallFinalizedCallback(Dialer sender);
string _uniqueID = string.Empty;
bool _wasOriginated = false;
object _locker = new object();

List<HangupEvent> _hangupList = new List<HangupEvent>();

CallFinalizedCallback _callFinalized;
public CallFinalizedCallback CallFinalized
{
get {
return this._callFinalized;
}
set {
_callFinalized = value;
}
}

CallInfo _callInfo;
public CallInfo CallInfo
{
get
{
return this._callInfo;
}
}

public Dialer()
{
_managerConnection = new ManagerConnection(Configuration.ManagerHost,
Configuration.ManagerPort,
Configuration.ManagerUserName,
Configuration.ManagerPassword);

//_timer = new Timer(Handle_timeoutCallback, Configuration.CallNotificationTimeout);

AddEventHandlers();
}

void Handle_managerConnectionOriginateResponse (object sender, OriginateResponseEvent e)

{
if (!e.CallerIdNum.Equals(_callInfo.CallId.ToString()))
return;

Logger.Print(string.Format(“Call to {0}-{1} was originated”, _callInfo.CallId, e.Exten));

bool succeeded = e.Response.ToLower().Equals(“success”);

if (succeeded)
UpdateDialoutQueue(CallStatus.Established);
else
UpdateDialoutQueue(CallStatus.FinishedWithError);

lock(_locker)
{
_wasOriginated = true;
_uniqueID = _callInfo.CallId.ToString();
}

foreach(HangupEvent hEvent in _hangupList)
{
if (hEvent.CallerIdNum.Equals(e.CallerIdNum)) // the call failed for some reason
{
Handle_managerConnectionHangup(this, hEvent);
break;
}
}
_hangupList.Clear();

}

public Dialer(CallInfo callInfo, CallFinalizedCallback callback) : this()
{
_callInfo = callInfo;

if (callback == null)
throw new ArgumentException(“CallFinalizedCallback cannot be null”);

_callFinalized = callback;
}

void AddEventHandlers()
{
_managerConnection.Hangup += Handle_managerConnectionHangup;
_managerConnection.OriginateResponse += Handle_managerConnectionOriginateResponse;
}

public void DisconnectFromAMI()
{
_managerConnection.Hangup -= Handle_managerConnectionHangup;
_managerConnection.OriginateResponse -= Handle_managerConnectionOriginateResponse;
}

void Handle_timeoutCallback()
{
try
{
Logger.Print(string.Format(“Call {0}-{1} forced to finalize after timeout”, _callInfo.CallId, _callInfo.Extension));

UpdateDialoutQueue(CallStatus.Timedout);
//_timer.CancelAlarm();

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_timeoutCallback: {0}”, ex.Message));
DisconnectFromAMI();
}
}

void Handle_managerConnectionHangup(object sender, HangupEvent e)
{
try
{
lock(_locker)
if (!_wasOriginated || !_uniqueID.Equals(e.CallerIdNum))
{
_hangupList.Add(e); // register this event in case it was fired before the OriginateAction event
return;
}

UpdateDialoutQueue(CallStatus.Finished, e.Cause, e.CauseTxt);
//_timer.CancelAlarm();

Logger.Print(string.Format(“Call {0}-{1} was hung up”, _callInfo.CallId, e.Channel));

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_managerConnectionHangup: {0}”, ex.Message));
DisconnectFromAMI();
}
}

public void Call()
{
Call(_callInfo);
}

public void Call(CallInfo callInfo)
{
try
{
OriginateAction originate = new OriginateAction();

originate.Channel = callInfo.Channel;
originate.Priority = callInfo.Priority;
originate.Context = callInfo.Context;
originate.Exten = callInfo.Extension;
originate.CallerId = callInfo.CallId.ToString(); // <——–
originate.Timeout = 500000;
originate.Async = true;

_managerConnection.Login();

MarkCallAsProcessed();

//_timer.SetAlarm();

UpdateDialoutQueue(CallStatus.Trying);
ManagerResponse r = _managerConnection.SendAction(originate, originate.Timeout);
}
catch(Exception ex)
{
Logger.Error(string.Format(“Call: {0}”, ex.Message));

//_timer.CancelAlarm();

if (ex is Asterisk.NET.Manager.TimeoutException)
{
UpdateDialoutQueue(CallStatus.Timedout);
DisconnectFromAMI();
}
else
UpdateDialoutQueue(CallStatus.FinishedWithError);
}
}
}

[/csharp]

 

Socket descriptor leak

TCP circuit

Today I noticed that the Openser plugin that I wrote some time ago was causing a major situation on a time-critic application running on an I5OS from IBM iSeries (formerly known as AS400). This application sporadically segfaults after what we thought was a random condition but after some log mining, we found out that the process was being halted because there was too many opened sockets for the process ID that it possesses.

The normal time span for each transaction never exceeds 2 seconds in our typical scenario. This assumption led us to think that there was and bug in the listener thread, some sort of infinite loop breaking the program but, after reading the socket client source file I figured out that was actually a bug in my code.

Since I’m not a TCP expert, I will try to explain the basics of the close procedure of TCP connections to better illustrate the problem.

  1. After the connection was established there is no distinction between server and client. Any of the two parties can close the connection whenever they want.
  2. Consider the two parties as A and B. When B tries to close the connection it sends the FIN packet (TCP packet with the flag FIN activated) to A and enters to FIN-WAIT-1 state.
  3. A replies with an ACK and when B receives this answers, it enters to FIN-WAIT-2. Now B expects the FIN from A.
  4. A sends FIN to finally confirm the disconnection and after the final ACK from B, the socket is closed.

The problem?

Under certain conditions, I wasn’t closing the sockets explicitly leaving it in an inconsistent state: FIN-WAIT-2 on the i5 side, and CLOSE_WAIT on the Linux side. There is no standard timeout to reuse sockets in FIN-WAIT-2, i5OS uses 10 minutes and this value is far too high to recycle the unused sockets. When the counting reached 2000, the program inevitably crashes.

The fix

[c]

BOOL send_message(struct _cnxaaa_net_config *net_config, const str *content, str *reply_buffer, unsigned int timeout)
{
int sockfd, n;
struct sockaddr_in serv_addr;
struct hostent *server;
struct timeval tv;

tv.tv_sec = 0;
tv.tv_usec = timeout;

sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd < 0)
{
_CNXAAA_LOG_ERR(“Error opening socket”);
return FALSE;
}

if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval)) != 0)
{
_CNXAAA_LOG_ERR(“Error assigning socket option”);
return FALSE;
}

server = gethostbyname(net_config->ip);
if (server == NULL)
{
_CNXAAA_LOG_ERR(“No such host”);
return FALSE;
}

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);

serv_addr.sin_port = htons(net_config->port);

if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0)
{
_CNXAAA_FLOG_ERR(“Error connecting: %s”, strerror( errno ));
close(sockfd); // I wasn’t closing here
return FALSE;
}

n = write(sockfd, content->s, content->len);

if (n < 0)
{
_CNXAAA_FLOG_ERR(“CNXAAA ERROR | Error writing to socket: %s”, strerror( errno ));
close(sockfd); // I wasn’t closing here
return FALSE;
}

n = recv(sockfd, (void *) reply_buffer->s, reply_buffer->len, 0);

if (n < 0)
{
_CNXAAA_FLOG_ERR(“CNXAAA ERROR | Error reading from socket: %s”, strerror( errno ));
close(sockfd); // I wasn’t closing here
return FALSE;
}

reply_buffer->s[n + 1] = ‘’;
reply_buffer->len = n;

close(sockfd);

return TRUE;
}

[/c]

Script to upload users to Moodle

In my spare time, I am a maintainer of moodle powered platforms hosted by a company that offers e-learning services and this post is about uploading new students to that platform 😀

This task generally requires a CSV file filled with information about the student. The mandatory fields are username, first name, last name and email but this is just an example and some other fields may be required. The tutor or the course owner generally provides an excel file with the necessary data but since an username is needed to login and the students may not have an email account or is not a company’s policy to use personal email addresses for login, we have take alternative measures to fit the requirements.

Here are enumerated some rules imposed for standardization:

1. Username must be the first letter of the forename followed by the first surname, no spaces in between.
2. If no email account is provided, the system will provide one for the user, even if the account doesn’t exists yet.

This is how the file looks like:
[bash]
1 Pedro Pedroso pepe@gmail.com Cobranzas
2 Juan Perez jp123@gmail.com Cobranzas
3 Juan Gonzalez gonzo1900@hotmail.com Cobranzas
4 Un boludo No tiene Por ahi

[/bash]

And this is the output from the script:
[bash]
username, password, firstname, lastname, email
ppedroso, elpassword*, Pedro, Pedroso, pepe@gmail.com
jperez, elpassword*, Juan, Perez, jp123@gmail.com
jgonzalez, elpassword*, Juan, Gonzalez, gonzo1900@hotmail.com
uboludo, elpassword*, Un, boludo, unboludo@example.com
[/bash]

And finally, the script written in Perl. It explains itself very well I think, there is no need for line to line comment 😀

[perl]
use strict;
use warnings;

open(FILE, “< $ARGV[0]"); print "username, password, firstname, lastname, emailn"; for ()
{
if ($_=~m/([a-z|A-Z]+)s([a-z|A-Z]+)/g)
{
my $name = $1;
my $lastname = $2;
$name=~m/^w/g;
my $username = lc(“$&$lastname”);
my $mail;

if (!($_=~m/[w|_|-]+@w+.(com|net)/g))
{
$mail = lc($name.$lastname).’@example.com’;
}
else
{
$mail = $&;
}

print “$username, elpassword*, $name, $lastname, $mailn”;
}
else
{
die ($_);
}
}
[/perl]