While working on the new v2 release of pycall, I was doing some research on the internal limitations of Asterisk call files, and thought I'd share some interesting (technical) bits of information here.

All information below has been gathered from the latest Asterisk release (v1.6.2.7). If you don't do any programming, you may want to skip this article, as it is a bit geeky.

Brief Overview of Call File Code

First of all, it is important to note that Asterisk call files only work when the Asterisk module pbx_spool.so is loaded.

The pbx_spool.so module (the Asterisk spooling daemon), is a process which runs and analyzes the Asterisk spooling directory (usually /var/spool/asterisk/outgoing/) for new call files. If a call file is found, then the spooling daemon will process the call file (extracting the directives), then executing the actions specified.

So, since we now know how the spooling daemon works, let's take a look at the source code (in C), and figure out what actually goes on.

First of all, download the Asterisk source code if you want to follow along: asterisk v1.6.2.7 (tarball). Once you extract the code, open up the file asterisk-1.6.2.7/pbx/pbx_spool.c in your favorite editor. This file contains all of the Asterisk code used to parse, launch, and control call files.

The first thing you'll notice (being a sensitive best-practices programmer!) is that there are a ton of magic numbers being thrown around in here. But let's just ignore that for now (I'm sure the Asterisk guys are working on it) :)

The two sections of the code which we're going to look at today are the outgoing struct, and the apply_outgoing function. These contain the bulk of the call file logic, and will help us learn a bit about call file internals.

Look them over briefly. In the next section, we'll dive right in.

The outgoing Struct

Let's start out by analyzing the outgoing struct, shown below (note: I've re-done the formatting and comments so that it displays in proper 80-column width):

struct outgoing {
    int retries;        // current number of retries
    int maxretries;     // maximum number of retries permitted
    int retrytime;      // how long to wait between retries (in seconds)
    int waittime;       // how long to wait for an answer
    long callingpid;    // PID which is currently calling
    int format;         // formats (codecs) for this call
 
    AST_DECLARE_STRING_FIELDS (
        AST_STRING_FIELD(fn);       // file name of the call file
        AST_STRING_FIELD(tech);     // which channel technology to use for
                                    // outgoing call
        AST_STRING_FIELD(dest);     // which device / line to use for outgoing
                                    // call
        AST_STRING_FIELD(app);      // if application: Application name
        AST_STRING_FIELD(data);     // if application: Application data
        AST_STRING_FIELD(exten);    // if extension / context / priority:
                                    // extension in dialplan
        AST_STRING_FIELD(context);  // if extension / context / priority:
                                    // dialplan context
        AST_STRING_FIELD(cid_num);  // callerID information: number
        AST_STRING_FIELD(cid_name); // callerID information: name
        AST_STRING_FIELD(account);  // account code
    );
 
    int priority;               // if extension / context / priority: priority
    struct ast_variable *vars;  // variables and functions
    int maxlen;                 // maximum length of call
    struct ast_flags options;   // options
};

This struct holds the directives specified in each call file that the spooling daemon reads. This means that if your call file looks like:

Channel: Local/18002223333@from-internal
Application: Playback
Data: hello-world

Then the struct will set: tech = "Local", dest = "18002223333@from-internal", app = "Playback", and data = "hello-world".

Internally, Asterisk uses this struct throughout the pbx_spool.c module, as a way to store individual call file states and track statuses.

How The Spooling Daemon Works

Now, when users create a call file, the spooling daemon will process that file. But how does it do it? Now that we've seen struct outgoing, let's look at the apply_outgoing function which parses call files, and populates an outgoing struct while verifying that all lines are syntactically correct.

This will give us insight into which directives are allowed in call files (and which variations raise errors).

Here is the apply_outgoing function cleaned up for clarity:

static int apply_outgoing(struct outgoing *o, char *fn, FILE *f) {
    /*
     * Parse the `outgoing` struct, clean up any lingering whitespace, and
     * verify that all directives are syntactically correct. Logs errors as it
     * finds them.
     */
 
    char buf[256];
    char *c, *c2;
    int lineno = 0;
    struct ast_variable *var, *last = o->vars;
 
    while (last && last->next) {
        last = last->next;
    }
 
    while(fgets(buf, sizeof(buf), f)) {
        lineno++;
 
        // Trim comments.
        c = buf;
        while ((c = strchr(c, '#'))) {
            if ((c == buf) || (*(c-1) == ' ') || (*(c-1) == '\t'))
                *c = '\0';
            else
                c++;
        }
 
        c = buf;
        while ((c = strchr(c, ';'))) {
            if ((c > buf) && (c[-1] == '\\')) {
                memmove(c - 1, c, strlen(c) + 1);
                c++;
            } else {
                *c = '\0';
                break;
            }
        }
 
        // Trim trailing white space.
        while(!ast_strlen_zero(buf) && buf[strlen(buf) - 1] < 33)
            buf[strlen(buf) - 1] = '\0';
 
        if (!ast_strlen_zero(buf)) {
 
            // Split the directive into two parts. Command and value.
            c = strchr(buf, ':');
            if (c) {
                *c = '\0';
                c++;
                while ((*c) && (*c < 33))
                    c++;
 
#if 0
                printf("'%s' is '%s' at line %d\n", buf, c, lineno);
#endif
 
                // Analyze the command, and populate the outgoing struct.
                if (!strcasecmp(buf, "channel")) {
                    if ((c2 = strchr(c, '/'))) {
                        *c2 = '\0';
                        c2++;
                        ast_string_field_set(o, tech, c);
                        ast_string_field_set(o, dest, c2);
                    } else {
                        ast_log(LOG_NOTICE, "Channel should be in form "
                            "Tech/Dest at line %d of %s\n", lineno, fn);
                    }
                } else if (!strcasecmp(buf, "callerid")) {
                    char cid_name[80] = {0}, cid_num[80] = {0};
                    ast_callerid_split(c, cid_name, sizeof(cid_name),
                        cid_num, sizeof(cid_num));
                    ast_string_field_set(o, cid_num, cid_num);
                    ast_string_field_set(o, cid_name, cid_name);
                } else if (!strcasecmp(buf, "application")) {
                    ast_string_field_set(o, app, c);
                } else if (!strcasecmp(buf, "data")) {
                    ast_string_field_set(o, data, c);
                } else if (!strcasecmp(buf, "maxretries")) {
                    if (sscanf(c, "%30d", &o->maxretries) != 1) {
                        ast_log(LOG_WARNING, "Invalid max retries at line %d "
                            "of %s\n", lineno, fn);
                        o->maxretries = 0;
                    }
                } else if (!strcasecmp(buf, "codecs")) {
                    ast_parse_allow_disallow(NULL, &o->format, c, 1);
                } else if (!strcasecmp(buf, "context")) {
                    ast_string_field_set(o, context, c);
                } else if (!strcasecmp(buf, "extension")) {
 
                    ast_string_field_set(o, exten, c);
                } else if (!strcasecmp(buf, "priority")) {
                    if ((sscanf(c, "%30d", &o->priority) != 1) ||
                        (o->priority < 1)) {
                        ast_log(LOG_WARNING, "Invalid priority at line %d of "
                            "%s\n", lineno, fn);
                        o->priority = 1;
                    }
                } else if (!strcasecmp(buf, "retrytime")) {
                    if ((sscanf(c, "%30d", &o->retrytime) != 1) ||
                        (o->retrytime < 1)) {
                        ast_log(LOG_WARNING, "Invalid retrytime at line %d of "
                            "%s\n", lineno, fn);
                        o->retrytime = 300;
                    }
                } else if (!strcasecmp(buf, "waittime")) {
                    if ((sscanf(c, "%30d", &o->waittime) != 1) ||
                        (o->waittime < 1)) {
                        ast_log(LOG_WARNING, "Invalid waittime at line %d of "
                            "%s\n", lineno, fn);
                        o->waittime = 45;
                    }
                } else if (!strcasecmp(buf, "retry")) {
                    o->retries++;
                } else if (!strcasecmp(buf, "startretry")) {
                    if (sscanf(c, "%30ld", &o->callingpid) != 1) {
                        ast_log(LOG_WARNING, "Unable to retrieve calling "
                            "PID!\n");
                        o->callingpid = 0;
                    }
                } else if (!strcasecmp(buf, "endretry") || !strcasecmp(buf,
                    "abortretry")) {
                    o->callingpid = 0;
                    o->retries++;
                } else if (!strcasecmp(buf, "delayedretry")) {
                } else if (!strcasecmp(buf, "setvar") || !strcasecmp(buf,
                    "set")) {
                    c2 = c;
                    strsep(&c2, "=");
                    if (c2) {
                        var = ast_variable_new(c, c2, fn);
                        if (var) {
                            /*
                             * Always insert at the end, because some people
                             * want to treat the spool file as a script
                             */
                            if (last) {
                                last->next = var;
                            } else {
                                o->vars = var;
                            }
                            last = var;
                        }
                    } else
                        ast_log(LOG_WARNING, "Malformed \"%s\" argument. "
                            "Should be \"%s: variable=value\"\n", buf, buf);
                } else if (!strcasecmp(buf, "account")) {
                    ast_string_field_set(o, account, c);
                } else if (!strcasecmp(buf, "alwaysdelete")) {
                    ast_set2_flag(&o->options, ast_true(c),
                        SPOOL_FLAG_ALWAYS_DELETE);
                } else if (!strcasecmp(buf, "archive")) {
                    ast_set2_flag(&o->options, ast_true(c),
                        SPOOL_FLAG_ARCHIVE);
                } else {
                    ast_log(LOG_WARNING, "Unknown keyword '%s' at line %d of "
                        "%s\n", buf, lineno, fn);
                }
            } else
                ast_log(LOG_NOTICE, "Syntax error at line %d of %s\n", lineno,
                    fn);
        }
    }
    ast_string_field_set(o, fn, fn);
    if (ast_strlen_zero(o->tech) || ast_strlen_zero(o->dest) ||
        (ast_strlen_zero(o->app) && ast_strlen_zero(o->exten))) {
        ast_log(LOG_WARNING, "At least one of app or extension must be "
            "specified, along with tech and dest in file %s\n", fn);
        return -1;
    }
    return 0;
}

The first thing it seems to do (after storing some Asterisk variables for later usage) is begin parsing the call file, 256 bytes at a time. One thing we immediately learn from this, is that any given line in your call file cannot contain more than 256 bytes (characters). If it does, then it will not be parsed correctly.

The next thing that apply_outgoing does is remove any comments and trailing whitespace. This is pretty standard stuff. I am a bit surprised, however, that the Asterisk developers didn't use the utility function ast_trim_blanks (in include/asterisk/strings.h), as re-writing this sort of stuff greatly increases the size of the codebase, making it harder to maintain.

Next, Asterisk attempts to detect the command and arguments for each call file directive. Since all call file directives are of the form command: arguments, Asterisk splits the line at ':', then tries to detect which command is being called. This is exactly what we would expect to happen.

In the process of splitting the lines into command and argument pairs, Asterisk parses out the arguments as well, and populates the outgoing struct as expected. Along the way, if Asterisk finds any problems with the syntax, it'll log the errors to the Asterisk log (usually /var/log/asterisk/full).

Lastly, after parsing all options, Asterisk verifies to make sure that the minimum directives have been specified.

If everything worked OK, and the call file can be spooled, then apply_outgoing will return 0, otherwise, it'll return -1.

What Did We Learn?

We've analyzed the core components that make call files work, and we've learned a few things.

  • We have a complete understanding of which call file directives exist (as shown in the code above). This is a big benefit, as the documentation on voip-info.org, and various other Asterisk documentation websites seems to be incomplete. The exact call files directives allowed are:

    • channel
    • callerid
    • application
    • data
    • maxretries
    • codecs
    • context
    • extension
    • priority
    • retrytime
    • waittime
    • retry
    • startretry
    • endretry
    • delayedretry
    • setvar
    • account
    • alwaysdelete
    • archive

    Note, some of these directives should not be directly entered into your call files (as Asterisk will automatically add them as necessary), but of course, if you're reading this you're probably the type of person who likes messing with that sort of stuff, so have at it. :>

  • We've learned how to put any amount of directives we want onto a single line. Obviously this is totally useless, but it is nice to know that we CAN if we want to!

    We're able to do this because the spooling daemon reads 256 bytes at a time for parsing. So we could write a call file that looks something like:

    Channel: blah <pad to 256 bytes>NextDirective: blah<pad to 256 bytes> ...

Conclusion

I'm hoping to do a few more articles that demistify other parts of Asterisk in depth (with code and examples). This is my first, and I know it doesn't touch on the complex topics like threading and synchronous / asynchronous channels, but I assure you I'll write some future articles which cover those parts in extreme depth.

If you want to keep up with my random other thoughts follow me on twitter.