newspaint

Documenting Problems That Were Difficult To Find The Answer To

Monthly Archives: August 2013

Node.JS Utility To Launch Processes Remotely Using ssh2 Library

This Node.JS utility was written with the sole purpose of running many processes on remote servers and collating the output (stdout and stderr) line by line such that no two lines would get mixed.

It uses the ssh2 library available for Node.JS.

Note that this opens a single ssh connection to each target server and multiplexes the file transfers and command executions through channels within this single ssh connection. By default OpenSSH allowed 10 simultaneous channels in a ssh connection but versions 5.1 and subsequent introduced a MaxSessions parameter which allows this to be greatly increased (I’ve had it in the thousands).

// node.js SSH process launcher and line logger

var fs = require('fs');
var util = require('util');
var ssh2 = require('/usr/local/node/node_modules/ssh2');

/***
  help_and_exit() - print out help for command line, then exit
***/
function help_and_exit() {
    var helpstr = (
        "Usage:\n" +
        "    node launcher.js -s <ip> [-s <ip> ...] \\\n" +
        "        {-i <identity_key> or -w <password>}\\\n" +
        "        [-u <user{root}>] \\\n" +
        "        [-p <port{22}>] \\\n" +
        "        [-c <command_line>] \\\n" +
        "        [-n <num_of_commands>] \\\n" +
        "        [-g <num_of_seconds>] \\\n" +
        "        [-f <file> -t <target_file>] [-f <file> -t <target_file> ...] \\\n" +
        "        [-x <connect_timeout_seconds>] \\\n" +
        "        [-k <keyword_file>] \\\n" +
        "        [-r] \\\n" +
        "        [-d]\n" +
        "\n" +
        "Notes:\n" +
        "  -g <sec> - spread start of n commands over this many seconds\n" +
        "  -f <file> -t <target_file> - uploads specified in source/dest pairs\n" +
        "  -k <file> - keyword file consists of CSV lines, one line per command\n" +
        "  -c - string may use \${1} \${2} ... \${99} to replace from fields in keyword file\n" +
        "       \${time} - timestamp in milliseconds\n" +
        "       \${index} - unique number per command executed\n" +
        "  -r - randomise starting line from keyword file\n" +
        "\n"
    );

    console.log( "%s", helpstr );
    process.exit( 1 );
}

/***
  process_command_line() - handle command line arguments
***/
function process_command_line() {
    if ( process.argv.length < 3 ) {
        help_and_exit();
    }

    var servers = new Array();
    var identity_file = null;
    var user = "root";
    var password = null;
    var port = 22;
    var command = null;
    var command_count = 1;
    var command_spread = 1; // milliseconds to wait between launches
    var command_spread_period = null;
    var files = new Array();
    var debugging = false;
    var timeout_connect = null;
    var keyword_file = null;
    var keyword_line_randomise = false;

    var idx;
    for ( idx = 2; idx < process.argv.length; idx++ ) {
        if ( process.argv[idx] === "-s" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -s option" );
                help_and_exit();
            }
            servers.push( process.argv[idx] );
            continue;
        }

        if ( process.argv[idx] === "-i" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -i option" );
                help_and_exit();
            }
            if ( ! fs.existsSync( process.argv[idx] ) ) {
                console.error( "Error, identity file \"%s\" does not exist", process.argv[idx] );
                help_and_exit();
            }
            identity_file = process.argv[idx];
            continue;
        }

        if ( process.argv[idx] === "-u" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -u option" );
                help_and_exit();
            }
            user = process.argv[idx];
            continue;
        }

        if ( process.argv[idx] === "-w" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -w option" );
                help_and_exit();
            }
            password = process.argv[idx];
            continue;
        }

        if ( process.argv[idx] === "-p" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -p option" );
                help_and_exit();
            }
            if (
                ( parseInt( process.argv[idx] ) < 1 ) ||
                ( parseInt( process.argv[idx] ) > 65535 )
            ) {
                console.error( "Invalid port number supplied to -p option" );
                help_and_exit();
            }
            port = parseInt( process.argv[idx] );
            continue;
        }

        if ( process.argv[idx] === "-c" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -c option" );
                help_and_exit();
            }
            command = process.argv[idx];
            continue;
        }

        if ( process.argv[idx] === "-n" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -n option" );
                help_and_exit();
            }
            if ( parseInt( process.argv[idx] ) < 1 ) {
                console.error( "Invalid command count supplied to -n option" );
                help_and_exit();
            }
            command_count = parseInt( process.argv[idx] );
            continue;
        }

        if ( process.argv[idx] === "-g" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -g option" );
                help_and_exit();
            }
            if ( parseInt( process.argv[idx] ) < 1 ) {
                console.error( "Invalid number of seconds supplied to -g option" );
                help_and_exit();
            }
            command_spread_period = parseInt( process.argv[idx] );
            continue;
        }

        if ( process.argv[idx] === "-f" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -f option" );
                help_and_exit();
            }
            if ( ! fs.existsSync( process.argv[idx] ) ) {
                console.error( "Error, source file \"%s\" does not exist", process.argv[idx] );
                help_and_exit();
            }
            var source_file = process.argv[idx];

            idx++;
            if ( process.argv[idx] !== "-t" ) {
                console.error( "Error, must provide -t option following -f option" );
                help_and_exit();
            }

            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -t option" );
                help_and_exit();
            }
            var target_file = process.argv[idx];

            var collection = { "source": source_file, "target": target_file };
            files.push( collection );

            continue;
        }

        if ( process.argv[idx] === "-d" ) {
            debugging = true;
            continue;
        }

        if ( process.argv[idx] === "-x" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -x option" );
                help_and_exit();
            }
            if ( parseInt( process.argv[idx] ) < 1 ) {
                console.error( "Invalid timeout supplied to -x option" );
                help_and_exit();
            }
            timeout_connect = parseInt( process.argv[idx] );
            continue;
        }

        if ( process.argv[idx] === "-k" ) {
            idx++;
            if ( idx >= process.argv.length ) {
                console.error( "Error, must provide argument to -k option" );
                help_and_exit();
            }
            if ( ! fs.existsSync( process.argv[idx] ) ) {
                console.error( "Error, keyword file \"%s\" does not exist", process.argv[idx] );
                help_and_exit();
            }
            keyword_file = process.argv[idx];
            continue;
        }

        if ( process.argv[idx] === "-r" ) {
            keyword_line_randomise = true;
            continue;
        }
    }

    if ( command_spread_period !== null ) {
        command_spread = parseInt( ( command_spread_period * 1000 ) / command_count );
    }

    if ( servers.length < 1 ) {
        console.error( "Must specify a minimum of one server" );
        help_and_exit();
    }

    if ( ( identity_file === null ) && ( password === null ) ) {
        console.error( "Must specify an identity file or login password" );
        help_and_exit();
    }

    var serverstates = { };
    for ( idx = 0; idx < servers.length; idx++ ) {
        serverstates[ servers[idx] ] = "initial";
    }

    var keyword_lines = null;
    if ( keyword_file ) {
        keyword_lines = [ ];

        // load whole file into memory (simplest)
        var keyword_buffer = fs.readFileSync( keyword_file, { encoding: 'utf8' } );
        // split into array of lines and words
        var regexpLine = new RegExp( "([^\r\n]+)[\r\n]+", "g" );
        while ( 1 ) {
            var mymatch = regexpLine.exec( keyword_buffer );
            if ( mymatch === null )
                break;

            var line = mymatch[1];

            if ( debugging ) {
                console.error( "  - processing line \"%s\"", line );
            }

            // process the line of text
            var words = [ ];
            while ( 1 ) {
                var next = line.indexOf( "," );
                if ( next < 0 ) {
                    words.push( line );
                    break;
                }

                words.push( line.substring( 0, next ) );
                line = line.substring( next + 1 );
            }

            keyword_lines.push( words );
        }
    }

    var keyword_line_number = 0;
    if ( keyword_line_randomise ) {
        keyword_line_number = Math.floor(Math.random() * keyword_lines.length);
    }

    var options = {
        "servers": serverstates,
        "identity": identity_file,
        "user": user,
        "password": password,
        "port": port,
        "command": command,
        "command_count": command_count,
        "command_spread": command_spread,
        "files": files,
        "debug": debugging,
        "timeout": timeout_connect,
        "keyword_file": keyword_file,
        "keyword_lines": keyword_lines,
        "keyword_line_number": keyword_line_number,
        "conns": { }
    };

    return( options );
}

/***
  log_server_state() - print out the states of all the servers
***/
function log_server_state( serverlist ) {
    var idx;
    for ( var key in serverlist ) {
        console.error( "    %s: %s", key, serverlist[key] );
    }
}

/***
  die_if_not_connected() - scan serverlist and die if any not connected
***/
function die_if_not_connected( serverlist ) {
    var idx;
    var connected = true;
    var disconnected_list = new Array();
    for ( var key in serverlist ) {
        if (
            ( serverlist[key] === "initial" ) ||
            ( serverlist[key] === "connected" ) ||
            ( serverlist[key] === "error" )
        ) {
            connected = false;
            disconnected_list.push( key );
        }
    }

    if ( connected )
        return;

    console.error( "ERROR: timeout, server%s failed to connect", ( disconnected_list.length === 1 ? "" : "s" ) );

    log_server_state( serverlist );

    process.exit( 2 );
}

/***
  interpret_command() - replace variables in command string with literals
***/
function interpret_command( options, index ) {
    var cmd = options.command;

    if ( options.keyword_lines ) {
        if ( options.keyword_line_number >= options.keyword_lines.length )
            options.keyword_line_number = 0;

        var words = options.keyword_lines[options.keyword_line_number];

        options.keyword_line_number++;

        var find_substitution = new RegExp( "[$][{]([0-9]+)[}]" );
        while ( 1 ) {
            var result = find_substitution.exec( cmd );
            if ( ! result )
                break;

            var start = result.index;
            var end = start + result[0].length;

            var wanted_idx = result[1] - 1;
            var substitution;
            if ( wanted_idx >= words.length ) {
                substitution = "";
            } else {
                substitution = words[wanted_idx];
            }

            cmd = cmd.substring( 0, start ) + substitution + cmd.substring( end );
        }

        var find_time = new RegExp( "[$][{](time)[}]" );
        while ( 1 ) {
            var result = find_time.exec( cmd );
            if ( ! result )
                break;

            var start = result.index;
            var end = start + result[0].length;

            var substitution = (new Date()).getTime();

            cmd = cmd.substring( 0, start ) + substitution + cmd.substring( end );
        }

        var find_index = new RegExp( "[$][{](index)[}]" );
        while ( 1 ) {
            var result = find_index.exec( cmd );
            if ( ! result )
                break;

            var start = result.index;
            var end = start + result[0].length;

            var substitution = index;

            cmd = cmd.substring( 0, start ) + substitution + cmd.substring( end );
        }
    }

    return( cmd );
}

/***
  ip2num() - convert dotted IP address to integer
***/
function ip2num( ip ) {
    var elements = ip.split( '.' );
    var result = (
        parseInt( elements[0] ) * 256 * 256 * 256 +
        parseInt( elements[1] ) * 256 * 256 +
        parseInt( elements[2] ) * 256 +
        parseInt( elements[3] )
    );

    return( result );
}

/***
   getconnidx() - get index of this connection in array of connections
***/
function getconnidx( options, conn ) {
    var idx = 0;
    for ( var tmpip in options.conns ) {
        if ( conn == options.conns[tmpip] ) {
            return idx;
        }
        idx++;
    }

    return( undefined );
}

/***
  do_exec() - execute command via established SSH connection
***/
function do_exec( conn, options, ip, instance_num ) {
    var uniqueindex = getconnidx( options, conn ) * options.command_count + instance_num;

    var cmdline = interpret_command( options, uniqueindex );

    if ( options.debug ) {
        console.error( "  - command line is \"%s\"", cmdline );
    }

    conn.exec(
        cmdline,
        function ( err, stream ) {
            if ( err ) {
                console.error( "[%s]/%d Error executing \"%s\": %s", ip, instance_num, cmdline, err );
                options.servers[ip] = "error";
                options.command_state[ip][instance_num] = "error";
                check_states( options );
                return;
            } else {
                options.command_state[ip][instance_num] = "exec";
            }

            var buffer_stdout = "";
            var buffer_stderr = "";

            var regexpLine = new RegExp( "([^\r\n]+)[\r\n]+" );

            stream.on(
                'data',
                function (data, extended) {
                    if ( extended === 'stderr' ) {
                        buffer_stderr += data;
                    } else {
                        buffer_stdout += data;
                    }

                    if ( options.debug ) {
                        console.error( "[%s]/%d Data received = %d", ip, instance_num, data.length );
                    }

                    while ( 1 ) {
                        var mymatch = regexpLine.exec( buffer_stderr );
                        if ( mymatch == null )
                            break;

                        var start = mymatch.index;
                        var end = start + mymatch[0].length;
                        var line = mymatch[1];

                        buffer_stderr = buffer_stderr.substring( end );

                        console.log( "[%s]/%d stderr: %s", ip, instance_num, line );
                    }

                    while ( 1 ) {
                        var mymatch = regexpLine.exec( buffer_stdout );
                        if ( mymatch == null )
                            break;

                        var start = mymatch.index;
                        var end = start + mymatch[0].length;
                        var line = mymatch[1];

                        buffer_stdout = buffer_stdout.substring( end );

                        console.log( "[%s]/%d stdout: %s", ip, instance_num, line );
                    }
                }
            );

            stream.on(
                'end',
                function () {
                    if ( options.debug ) {
                        console.error( "[%s]/%d stream end", ip, instance_num );
                    }
                    options.command_state[ip][instance_num] = "completed";
                    check_states( options );
                }
            );

            stream.on(
                'close',
                function () {
                    if ( options.debug ) {
                        console.error( "[%s]/%d stream closed", ip, instance_num );
                    }
                }
            );
            stream.on(
                'exit',
                function (retval, signalName, didCoreDump, description) {
                    if ( retval != null ) {
                        if ( options.debug ) {
                            console.error( "[%s]/%d stream exit = %d", ip, instance_num, retval );
                        }
                        stream.end();
                    } else {
                        if ( options.debug ) {
                            console.error( "[%s]/%d stream exit signal %s%s [%s]", ip, instance_num, signalName, didCoreDump ? " (dumped core)" : "", description );
                        }
                    }
                }
            );
        }
    );
}

/***
  check_states() - state machine engine, checking states, calling next action when ready
***/
function check_states( options ) {
    var all_done = true;

    if ( options.debug ) {
        for ( var sip in options.servers ) {
            console.error( "  %s: %s", sip, options.servers[sip] );
        }
    }

    if ( options.command_state ) {
        for ( var ip in options.command_state ) {
            if (
                ( options.servers[ip] === "closed" ) ||
                ( options.servers[ip] === "error" )
            ) {
                continue;
            }

            if ( options.debug ) {
                var strstate = "";
                var statectr = 0;
                for ( statectr = 0; statectr < options.command_state[ip].length; statectr++ ) {
                    if ( strstate.length != 0 ) {
                        strstate += ", ";
                    }
                    strstate += "\n  " + statectr + ":" + options.command_state[ip][statectr];
                }
                console.error( "[%s] command_state = %s", ip, strstate );
            }

            var all_completed = true;
            var instance = 0;
            for ( inst = 0; inst < options.command_state[ip].length; inst++ ) {
                var state = options.command_state[ip][inst];
                if (
                    ( state === "error" ) ||
                    ( state === "completed" )
                ) {
                    ;
                } else {
                    all_completed = false;
                }
            }

            if ( all_completed ) {
                options.conns[ip].end();
                options.servers[ip] = "closed";
            }
        }
    }

    for ( var ip in options.servers ) {
        if (
            ( options.servers[ip] === "error" ) ||
            ( options.servers[ip] === "closed" )
        ) {
            ;
        } else {
            all_done = false;
        }
    }

    if ( all_done ) {
        if ( options.debug ) {
            console.error( "- all completed" );
        }
        process.exit( 0 );
    }

    var all_awaiting_cmd = true;
    var any_closed = false;
    var any_awaiting = false;

    for ( var ip in options.servers ) {
        if (
            ( options.servers[ip] === "awaiting_cmd" )
        ) {
            any_awaiting = true;
        } else {
            all_awaiting_cmd = false;
            if (
                ( options.servers[ip] === "closed" ) ||
                ( options.servers[ip] === "error" )
            ) {
                // connection closed before "awaiting_command"
                any_closed = true;
            }
        }
    }

    if ( all_awaiting_cmd ) {
        options["command_state"] = { };
        for ( var ip in options.servers ) {
            options.servers[ip] = "executing";

            if ( options.command != null ) {
                var conn = options.conns[ip];
                if ( ! conn ) {
                    console.error( "Programmer error, conn should exist" );
                    process.exit( 98 );
                }

                // execute command number of times
                var cmdidx;
                options.command_state[ip] = new Array( options.command_count );
                for ( cmdidx = 0; cmdidx < options.command_count; cmdidx++ ) {
                    options.command_state[ip][cmdidx] = "initial";
                }
                var timeoffset = 0;
                for ( cmdidx = 0; cmdidx < options.command_count; cmdidx++ ) {
                    setTimeout(
                        function (conn, options, ip, cmdidx) {
                            do_exec( conn, options, ip, cmdidx );
                        },
                        timeoffset,
                        conn, options, ip, cmdidx
                    );
                    timeoffset += options.command_spread;
                }
            } else {
                options.servers[ip] = "closed";
                check_states( options );
            }
        }
    } else if ( any_closed && any_awaiting ) {
        console.error( "Error, connection has been prematurely closed" );
        process.exit( 2 );
    }
}

/***
  do_sftp() - perform file upload via established SSH connection
***/
function do_sftp( conn, options, ip, filesremaining, callback ) {
    if ( options.debug ) {
        console.error( "[%s] doing sftp %d", ip, filesremaining.length );
    }

    if ( filesremaining.length == 0 ) {
        callback();
        return;
    }

    var nextfile = filesremaining.shift();
    if ( options.debug ) {
        console.error( "[%s] next file is %s", ip, nextfile.source );
    }

    conn.sftp(
        function (err, sftp) {
            if ( err ) {
                console.error( "[%s] Error, problem starting SFTP: %s", ip, err );
                process.exit( 3 );
            }

            if ( options.debug ) {
                console.error( "[%s] SFTP started", ip );
            }

            // start upload
            var readStream = fs.createReadStream( nextfile.source );
            var writeStream = sftp.createWriteStream(
                nextfile.target,
                { flags: 'w', encoding: null, mode: 0666 }
            );

            writeStream.on(
                'close',
                function () {
                    if ( options.debug ) {
                        console.error( "[%s] end transfer of \"%s\"", ip, nextfile.source );
                    }
                    sftp.end();
                    do_sftp( conn, options, ip, filesremaining, callback );
                }
            );

            readStream.pipe( writeStream );
        }
    );
}

/***
  do_ready_action_awaiting_cmd() - change state to waiting for cmd and check state machine
***/
function do_ready_action_awaiting_cmd( conn, options, ip ) {
    options.servers[ip] = "awaiting_cmd";
    check_states( options );
}

/***
 do_ready_action_upload_files() - upload files if required
***/
function do_ready_action_upload_files( conn, options, ip ) {
    if ( options.files.length > 0 ) {
        var filesremaining = options.files.slice( 0 );

        do_sftp(
            conn, options, ip, filesremaining,
            function () {
                do_ready_action_awaiting_cmd( conn, options, ip );
            }
        );
    } else {
        // no files to transfer, change state
        do_ready_action_awaiting_cmd( conn, options, ip );
    }
}

/***
  connect_to_server() - set up a connection to a server
***/
function connect_to_server( options, ip ) {
    var conn = new ssh2();

    conn.setMaxListeners(0);

    conn.on(
        'connect',
        function () {
            if ( options.debug ) {
                console.error( "[%s] connect", ip );
            }
            options.servers[ip] = "connected";
        }
    );

    conn.on(
        'ready',
        function () {
            if ( options.debug ) {
                console.error( "[%s] ready", ip );
            }
            options.servers[ip] = "ready";
            options.conns[ip] = conn;

            do_ready_action_upload_files(
                conn, options, ip
            );
        }
    );

    conn.on(
        'error',
        function (err) {
            if ( options.debug ) {
                console.error( "[%s] error: %s", ip, err );
            }
            options.servers[ip] = "error";
            check_states( options );
        }
    );

    conn.on(
        'end',
        function () {
            if ( options.debug ) {
                console.error( "[%s] end", ip );
            }
            options.servers[ip] = "closed";
            check_states( options );
        }
    );

    conn.on(
        'close',
        function (had_error) {
            if ( options.debug ) {
                console.error( "[%s] end: %s", ip, had_error );
            }
            options.servers[ip] = "closed";
            check_states( options );
        }
    );

    var connectoptions = {
        "host": ip,
        "port": options.port,
        "username": options.user
    };

    if ( options.identity !== null ) {
        connectoptions["privateKey"] = fs.readFileSync( options.identity );
    } else if ( options.password !== null ) {
        connectoptions["password"] = options.password;
    } else {
        console.error( "No authentication option for SSH provided" );
        process.exit( 1 );
    }

    conn.connect( connectoptions );
}

/***
  connect_to_servers() - connect to all the servers
***/
function connect_to_servers( options ) {
    var idx;
    for ( var ip in options.servers ) {
        connect_to_server( options, ip );
    }
}

/***
  main() - process command line, then connect to servers
***/
function main() {
    var options = process_command_line();

    // establish ssh connections to all servers, wait until this is achieved
    if ( options.debug ) {
        console.error( "options = %s", util.inspect( options ) );
    }

    // timeout to blow up if connections haven't been established
    if ( options.timeout ) {
        setTimeout(
            function () {
                die_if_not_connected( options.servers );
            },
            options.timeout * 1000
        );
    }

    // establish connection to servers
    connect_to_servers( options );
}

main();

Example Usage

One could have a local jar file they wish to run remotely on four servers. And they want to run 20 instances of this on each server but with the instances started spread out over 60 seconds. In total this means that 80 instances of the program will be started spread out over 60 seconds.

The jar file exists in ~/myjar.jar on the local server and before executing this jar file it must be uploaded to /tmp/ on the remote servers. Command execution won’t begin until the file has been successfully transferred to all the remote servers.

/usr/local/node/bin/node sshlauncher.js \
  -s test_server1.my.net \
  -s test_server2.my.net \
  -s test_server3.my.net \
  -s test_server4.my.net \
  -u testuser \
  -w testpassword \
  -x 15 \
  -g 60 \
  -n 20 \
  -f ~/myjar.jar -t /tmp/myjar.jar \
  -c 'java -jar /tmp/myjar.jar'

Debugging mode can be enabled with a -d flag so you can see when connections take place and file transfers, etc.

You may wish to provide a text comma-separated value file so that you can provide unique or different parameters to your executed command instances (e.g. for testing login processes to a site).

If you wanted to launch 4 instances of a program with unique usernames you could have a file named ~/keywords.txt containing:

user1,password1
user2,password2
user3,password3
user4,password4

and a command line of:

/usr/local/node/bin/node sshlauncher.js \
  -s test_server1.my.net \
  -s test_server2.my.net \
  -s test_server3.my.net \
  -s test_server4.my.net \
  -u testuser \
  -w testpassword \
  -x 15 \
  -g 60 \
  -n 20 \
  -f ~/myjar.jar -t /tmp/myjar.jar \
  -c 'java -jar /tmp/myjar.jar ${1} ${2}' \
  -k ~/keywords.txt

If your CSV (comma-separated value) keyword file is long enough (say, a file full of dictionary words) you can provide the -r flag to start reading from a random line in the file (e.g. when trying to test a cached server and you want to feed it unique words to circumvent caching).

TAO IDL Compiler Error About Reserved Words

I was getting the following error when compiling an IDL (interface definition language) file using tao_idl version 2.2.0:

Illegal syntax or missing declarator in parameter declaration

I couldn’t work out what the problem was until I read this blog post in which somebody had discovered the compiler complaining about the word “port” clashing with a reserved word.

In my case I had an IDL file try and declare a structure member named “alias“.

The solution was to append all instances of the variable name “alias” with an underscore, e.g. “_alias“.

Installing Perl Padre IDE Into Ubuntu 12.04 Precise

Running cpan Padre was giving me all sorts of troubles.

I had to follow the advice in this blog post when I got the error message Undefined subroutine &ExtUtils::ParseXS::errors – which was to apt-get install libwx-perl and edit /usr/local/lib/perl5/5.12.4/ExtUtils/xsubpp and change the errors() call to report_error_count() (as specified in this forum post).

Padre kept failing to install when it couldn’t pass tests depending on Debug::Client. To install that I had to install some dependent libraries:

apt-get install libncurses5-dev
apt-get install libreadline-dev
cpan Term::ReadLine::Gnu
cpan Debug::Client
cpan Padre

Basic Authentication in Perl by Inheriting from LWP::UserAgent or WWW::Mechanize

We can easily provide HTTP basic authentication in the LWP::UserAgent and WWW::Mechanize modules by subclassing them and overriding the get_basic_credentials() method. We also provide a set_credentials() function for providing the username and password.

Note that this is a very simple override – because the get_basic_credentials() routine may discriminate between different Authentication Realms as well as URLs. The provided function in this article merely assumes you want the username/password to be provided for every webpage accessed.

package MyAgent;

use vars qw(@ISA);
@ISA = qw(LWP::UserAgent);

my ( $username, $password ) = ( undef, undef );

# method to set username and password for authentication
sub set_credentials {
  my $self = shift;
  ( $username, $password ) = @_;
}

# this routine gets called when your browser
#   would otherwise be asked to provide a
#   username and password
sub get_basic_credentials {
  my ($self, $realm, $uri) = @_;
  print( STDERR "  - providing auth to realm \"$realm\"\n" );
  return( $username, $password );
}
package MyMech;

use vars qw(@ISA);
@ISA = qw(WWW::Mechanize);

my ( $username, $password ) = ( undef, undef );

# method to set username and password for authentication
sub set_credentials {
  ( $username, $password ) = @_;
}

# this routine gets called when your browser
#   would otherwise be asked to provide a
#   username and password
sub get_basic_credentials {
  my ($self, $realm, $uri) = @_;
  print( STDERR "  - providing auth to realm \"$realm\"\n" );
  return( $username, $password );
}

Example Usage

You can use your new class where you would have otherwise used LWP::UserAgent (or WWW::Mechanize), e.g.:

package main;

my $mech = MyMech->new();
$mech->set_credentials( "administrator", "incharge" );
$mech->get( $url );
print( $mech->status . "\n" );
print( $mech->success );

Summarising Local Changes Against A CVS Repository

Say you have a large project checked out from CVS with many subdirectories. And you want to know what files you have changed that need checking in.

You could use the cvs status command but the output can get unwieldy on the screen. The following script presents a coloured text summary.

#!/usr/bin/perl -w # -*-CPerl-*-

use constant DEBUG => 0;

use strict;

my $cvs = "cvs";

# using ANSI?
my $ansicolor = 1;
eval "use Term::ANSIColor;";
if ( $@ ) {
    $ansicolor = undef;
}

# can we detect column width?
my $columns = undef;
if ( ! $columns ) {
    eval {
        my ( $fin, $line );
        if ( open( $fin, "resize |" ) ) {
            while ( defined( $line = <$fin> ) ) {
                if ( $line =~ m/COLUMNS=(\d+)/ ) {
                    $columns = $1;
                    last;
                }
            }
        }
        close( $fin );
    };
    if ( DEBUG() && $@ ) {
        my $err = $@; warn( $err );
    }
}

if ( ! $columns ) {
    eval {
        my ( $fin, $line );
        if ( open( $fin, "stty -a |" ) ) {
	    while ( defined( $line = <$fin> ) ) {
	        if ( $line =~ m/columns\s+(\d+)/ ) {
	            $columns = $1;
		    last;
	        }
            }
	}
	close( $fin );
    };
    if ( DEBUG() && $@ ) {
        my $err = $@; warn( $err );
    }
}
#$columns-- if ( $columns && ( $columns > 1 ) );
if ( DEBUG() ) {
    print( "Using columns of \"$columns\"\n" ) if ( $columns );
    print( "Could not determine column width\n" ) if ( ! $columns );
}

# autoflush on
select( (select(STDERR), $|=1)[0] );
select( (select(STDOUT), $|=1)[0] );

# start the cvs status command
my $arg = join( " ", map { "\"$_\"" } @ARGV );

if ( ! open( FIN, "$cvs status $arg 2>&1 |" ) ) {
    die( "Could not start cvs: $!" );
}

# cycle through each line of text returned by cvs status
my $lastdir = undef;
my $lastlength = 0;
while ( defined( my $line = <FIN> ) ) {
    if ( $line =~ m/^(cvs \S+: )(Examining )(.*)/s ) {
        my $dispdir;

        my $dir = $3;
        if ( ( $columns ) && ( $columns <= length( $1 . $2 . $3 ) ) ) {
            $dir = substr( $3, 0, ( $columns - length( $1 . $2 ) ) - 1 );
        }

        if ( $ansicolor ) {
            eval '$lastdir = $1 .color("yellow") .$2 .$3 .color("reset");';
            eval '$dispdir = $1 .color("yellow") .$2 .$dir .color("reset");';
        } else {
            $lastdir = $1 . $2 . $3;
            $dispdir = $1 . $2 . $dir;
        }

        my $length12dir = length( $1 . $2 . $dir );

        $lastdir =~ s/[\r\n]+//gs;
        $dispdir =~ s/[\r\n]+//gs;

        print( ( ' ' x $lastlength ) . "\r" );
        $lastlength = $length12dir;
        print( $dispdir . "\r" );
    }

    if ( $line =~ m/^(File: )(.*?)(\s+)(Status: )(.*)/s ) {
        my $fileline = undef;
        if ( $ansicolor ) {
            eval '$fileline = $1 . color("cyan") . $2 . color("reset") . $3 . $4 . color("magenta") . $5 . color("reset")';
        } else {
            $fileline = $1 . $2. $3. $4. $5;
        }

        if ( $line !~ m/Status: Up-to-date/ ) {
            if ( $lastdir ) {
                print( ( ' ' x length($lastdir) ) . "\r" );
                print( $lastdir . "\n" );
                $lastdir = undef;
            }

            print( $fileline );
        }
    }
}

# clean up screen
if ( $lastdir ) {
    print( ' ' x length($lastdir) . "\r" );
}

# close cvs
close( FIN );

It first attempts to discover the terminal width so that directory names don’t spill off the edge when displaying what folder is currently being analysed.

Example output would be:

host@server:/tmp/cvs# cvschange.pl
cvs status: Examining mytest
File: verifymsg         Status: Locally Modified
cvs status: Examining nagiosconf
File: nsca.pl           Status: Locally Modified

This script was written in 2006.

Quickly Comparing Working Copies With CVS, SVN, and Git Repositories

A frequent problem I had was wanting to run a graphical (UI) diff between my working copy of a file and the latest (or any particular) revision in the source repository.

Of course one can run the common commands cvs diff (and similar for other repository types) but this gives a textual diff which can be less helpful when a source file is long.

So I present here the source code for the Perl scripts I use before every check-in I make.

Note that I have used tkdiff as the graphical diff utility in these scripts. I find this diff tool very lightweight and it is free. I also have variants of these utilities that use Beyond Compare which is a heavier but very powerful diff utility and one which I thoroughly recommend purchasing by any serious developer.

CVS

#!/usr/bin/perl -w
# -*-CPerl-*-

use Getopt::Long;

use strict;

# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $cvs = "cvs";
my $cp = "cp";
my $patch = "patch";

my %tempfile = ();
my $result;

$tempfile{patch} = "temp.patch.$$.temp";

sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );

# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );

my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
    print( STDERR <<EOF );
cvs-tkdiff - compare a file against another version in a CVS repository

Usage:
  cvs-tkdiff [-r <revision>] [-r <revision] <filename>

Description:
  If no revision is specified then <filename> is compared against the
HEAD in the repository.

  If a single revision is provided (e.g. "cvs-tkdiff -r1.1 a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.

  If two revisions are specified (e.g. "cvs-tkdiff -r1.1 -r1.3 a.txt")
then the files from the two specified commits will be compared to each
other.
EOF
    exit( 1 );
}

my @filediff = (); # will contain the two files to diff against

if ( @revisions == 2 ) {
    # compare revisions mode
    my ( $rev_a, $rev_b ) = @revisions;
    $tempfile{rev_a} = "temp.file.$$.ver.$rev_a.temp";
    $tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";

    eval {
        patch_copy( $cmpfile, $tempfile{rev_a},
                    $rev_a, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
    # compare against CVS version mode

    # does $cmpfile exist?
    if ( ! -r $cmpfile ) {
        print( STDERR "File \"$cmpfile\" is not readable" );
        exit( 1 );
    }

    # get current repository version of file
    my $rev_b = undef;
    if ( @revisions == 1 ) {
        $rev_b = $revisions[0];
    } else {
        my $cvsstatus = `$cvs status "$cmpfile"`;
        my $repositoryversion = undef;
        if ( $cvsstatus =~ m/Repository\srevision:\s+([a-zA-Z_0-9.-]+)/i ) {
            $rev_b = $1;
        } else {
            die( "Could not obtain current repository version" );
        }
    }

    $tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };

    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_b}, $cmpfile );
}

# run diff
system( $diff, @filediff );

# clean up
clean_tempfiles( \%tempfile );

exit( 0 );

###############################################################################
# subroutines
#

sub clean_tempfiles( $ ) {
    my ( $tempfileref ) = @_;

    foreach my $key ( keys %{$tempfileref} ) {
        unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
    }
}

sub patch_copy( $ $ $ $ ) {
    my ( $fileorig, $filedest, $revision, $patchfile ) = @_;

    # copy original file to destination
    my $result = `$cp "$fileorig" "$filedest"`;
    die ( "Could not copy original file" ) if ( ! -r $filedest );

    # create patch file
    $result = `$cvs diff -r $revision -u "$cmpfile" >"$patchfile"`;
    if ( ! -r $patchfile ) {
        die( "Could not create patch file revision $revision from cvs" );
    }

    $result = `$patch -R -p0 $filedest $patchfile`;
}

SVN

#!/usr/bin/perl -w
# -*-CPerl-*-

use File::Copy;
use Getopt::Long;

use strict;

# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $svn = "svn";
my $patch = "patch";

my %tempfile = ();
my $result;

$tempfile{patch} = "temp.patch.$$.temp";

sub normalise_eol( $ $ );
sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );

# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );

my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
    print( STDERR <<EOF );
svn-tkdiff - compare a file against another version in a SVN repository

Usage:
  svn-tkdiff [-r <revision>] [-r <revision] <filename>

Description:
  If no revision is specified then <filename> is compared against the
HEAD in the repository.

  If a single revision is provided (e.g. "svn-tkdiff -r3266 a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.

  If two revisions are specified (e.g. "svn-tkdiff -r3266 -r5230 a.txt")
then the files from the two specified commits will be compared to each
other.
EOF
    exit( 1 );
}

my @filediff = (); # will contain the two files to diff against

if ( @revisions == 2 ) {
    # compare revisions mode
    my ( $rev_a, $rev_b ) = @revisions;
    $tempfile{rev_a} = "temp.file.$$.ver.$rev_a.temp";
    $tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";

    eval {
        patch_copy( $cmpfile, $tempfile{rev_a},
                    $rev_a, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
    # compare against SVN version mode

    # does $cmpfile exist?
    if ( ! -r $cmpfile ) {
        print( STDERR "File \"$cmpfile\" is not readable" );
        exit( 1 );
    }

    # get current repository version of file
    my $rev_b = "HEAD";
    if ( @revisions == 1 ) {
        $rev_b = $revisions[0];
    } else {
        my $svnstatus = `$svn status -v "$cmpfile"`;
        if ( $svnstatus =~ m/^.{7}\s*(\d+)\s*(\d+)/ ) {
            $rev_b = $2;
        } else {
            die( "Could not obtain current repository version" );
        }
    }

    $tempfile{rev_b} = "temp.file.$$.ver.$rev_b.temp";
    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };

    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_b}, $cmpfile );
}

# run diff
system( $diff, @filediff );

# clean up
clean_tempfiles( \%tempfile );

exit( 0 );

###############################################################################
# subroutines
#

sub clean_tempfiles( $ ) {
    my ( $tempfileref ) = @_;

    foreach my $key ( keys %{$tempfileref} ) {
        unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
    }
}

sub patch_copy( $ $ $ $ ) {
    my ( $fileorig, $filedest, $revision, $patchfile ) = @_;

    # copy original file to destination
    my $result = File::Copy::copy( "$fileorig", "$filedest" );
    die ( "Could not copy original file" ) if ( ! -r $filedest );

    # create patch file
    $result = `$svn diff -r $revision -x -u "$cmpfile" >"$patchfile"`;
    if ( ! -r $patchfile ) {
        die( "Could not create patch file revision $revision from svn" );
    }

    # make line endings common
    normalise_eol( $filedest, "\n" );
    normalise_eol( $patchfile, "\n" );

    $result = `$patch -R -p0 $filedest -i $patchfile`;
}

sub normalise_eol( $ $ ) {
    my ( $filename, $eol ) = @_;

    my $fin = undef;
    if ( ! open( $fin, "<$filename" ) ) {
        die( "Failed to open file \"$filename\" for reading: $!" );
    }

    my $fout = undef;
    if ( ! open( $fout, ">$filename.$$.tmp" ) ) {
        die( "Failed to open file \"$filename.$$.tmp\" for writing: $!" );
    }

    my $line;
    while ( defined( $line = <$fin> ) ) {
        $line =~ s/(\r\n|\n\r|\n|\r)/$eol/sg;
        print( $fout $line );
    }
    close( $fout );
    close( $fin );

    rename( "$filename.$$.tmp", $filename );
}

Git

#!/usr/bin/perl -w
# -*-CPerl-*-

use Getopt::Long;

use strict;

# configure your utilities here
# ... in particular your favourite graphical diff application
my $diff = "tkdiff";
my $git = "GIT_PAGER=cat git";
my $cp = "cp";
my $patch = "patch";

my %tempfile = ();
my $result;

$tempfile{patch} = "temp.patch.$$.temp";

sub patch_copy( $ $ $ $ );
sub clean_tempfiles( $ );

# process options
my @revisions = ();
Getopt::Long::Configure("bundling");
GetOptions( "revision|rev|r=s" => \@revisions );

my $cmpfile = $ARGV[0];
if ( ! $cmpfile )
{
    print( STDERR <<EOF );
git-tkdiff - compare a file against another version in a git repository

Usage:
  git-tkdiff [-r <revision>] [-r <revision] <filename>

Description:
  If no revision is specified then <filename> is compared against the
HEAD in the local repository.

  If a single revision is provided (e.g. "git-tkdiff -r0f1bef a.txt")
then the file ("a.txt") in the local working directory will be compared
against the version in the specified commit.

  If two revisions are specified (e.g. "git-tkdiff -r0f1b -rba88 a.txt")
then the files from the two specified commits will be compared to each
other.

  If you want to compare your local copy against the most current version
in the remote repository then specify "origin/master" as the revision, e.g.
    git-tkdiff -rorigin/master a.txt
EOF
    exit( 1 );
}

my @filediff = (); # will contain the two files to diff against

sub safe_fname {
    my $orig = $_[0];
    $orig =~ s/[^a-zA-Z0-9_-]/_/g;
    return( $orig );
}

if ( @revisions == 2 ) {
    # compare revisions mode
    my ( $rev_a, $rev_b ) = @revisions;
    my ( $srev_a, $srev_b ) = map { safe_fname($_) } @revisions;

    $tempfile{rev_a} = "temp.file.$$.ver.$srev_a.temp";
    $tempfile{rev_b} = "temp.file.$$.ver.$srev_b.temp";

    eval {
        patch_copy( $cmpfile, $tempfile{rev_a},
                    $rev_a, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };
    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_a}, $tempfile{rev_b} );
} else {
    # compare against git HEAD version mode

    # does $cmpfile exist?
    if ( ! -r $cmpfile ) {
        print( STDERR "File \"$cmpfile\" is not readable" );
        exit( 1 );
    }

    # get current repository version of file
    my $rev_b = undef;
    if ( @revisions == 1 ) {
        $rev_b = $revisions[0];
    } else {
        my $gitlatestlog = `$git log --full-index --summary -1 "$cmpfile"`;
        my $repositoryversion = undef;
        if ( $gitlatestlog =~ m/^commit\s+([a-fA-F0-9]{40})/m ) {
            $rev_b = $1;
        } else {
            die( "Could not obtain current repository version" );
        }
    }

    my $srev_b = safe_fname( $rev_b );
    $tempfile{rev_b} = "temp.file.$$.ver.$srev_b.temp";
    eval {
        patch_copy( $cmpfile, $tempfile{rev_b},
                    $rev_b, $tempfile{patch} );
    };

    if ( $@ ) {
        my $error = $@;
        clean_tempfiles( \%tempfile );
        die( "Could not retrieve repository version: $error" );
    }

    @filediff = ( $tempfile{rev_b}, $cmpfile );
}

# run diff
system( $diff, @filediff );

# clean up
clean_tempfiles( \%tempfile );

exit( 0 );

###############################################################################
# subroutines
#

sub clean_tempfiles( $ ) {
    my ( $tempfileref ) = @_;

    foreach my $key ( keys %{$tempfileref} ) {
        unlink( $tempfileref->{$key} ) if ( -f $tempfileref->{$key} );
    }
}

sub patch_copy( $ $ $ $ ) {
    my ( $fileorig, $filedest, $revision, $patchfile ) = @_;

    # copy original file to destination
    my $result = `$cp "$fileorig" "$filedest"`;
    die ( "Could not copy original file" ) if ( ! -r $filedest );

    # create patch file
    $result = `$git diff -r $revision -u -- "$cmpfile" >"$patchfile"`;
    if ( ! -r $patchfile ) {
        die( "Could not create patch file revision $revision from cvs" );
    }

    $result = `$patch -R -p0 $filedest $patchfile`;
}

History

I wrote cvs-tkdiff.pl in 2006 and have subsequently written the variants – git-tkdiff.pl was written in 2009.

Looking Inside A Java Jar File

Here are some simple scripts for inspecting a .jar file.

Listing All Classes and Methods in a Jar

I have found this very useful when given a .jar without any other documentation and have been expected to use it to interface to a proprietary system. Being given the classes and methods along with the method signatures (including expected parameter types) has been a life-saver.

#!/bin/bash

JAR_TO_INTERROGATE=$1
if [ -z "$JAR_TO_INTERROGATE" ]; then
  echo "Please provide JAR file name to interrogate"
  exit 1
fi

CLASSES_OF_JAR=$(jar tf "$JAR_TO_INTERROGATE" |grep class |sed 's/.class//g')

javap -classpath "$JAR_TO_INTERROGATE" -s $CLASSES_OF_JAR

This utility can be used as follows:

user@host:~$ interrogate_jar.sh junit-4.10.jar
Compiled from "ActiveTestSuite.java"
class junit.extensions.ActiveTestSuite$1 extends java.lang.Thread{
final junit.framework.Test val$test;
  Signature: Ljunit/framework/Test;
final junit.framework.TestResult val$result;
  Signature: Ljunit/framework/TestResult;
final junit.extensions.ActiveTestSuite this$0;
  Signature: Ljunit/extensions/ActiveTestSuite;
junit.extensions.ActiveTestSuite$1(junit.extensions.ActiveTestSuite, junit.framework.Test, junit.framework.TestResult);
  Signature: (Ljunit/extensions/ActiveTestSuite;Ljunit/framework/Test;Ljunit/framework/TestResult;)V
public void run();
  Signature: ()V
}

Compiled from "ActiveTestSuite.java"
public class junit.extensions.ActiveTestSuite extends junit.framework.TestSuite{
public junit.extensions.ActiveTestSuite();
  Signature: ()V
public junit.extensions.ActiveTestSuite(java.lang.Class);
  Signature: (Ljava/lang/Class;)V
public junit.extensions.ActiveTestSuite(java.lang.String);
  Signature: (Ljava/lang/String;)V
public junit.extensions.ActiveTestSuite(java.lang.Class, java.lang.String);
  Signature: (Ljava/lang/Class;Ljava/lang/String;)V
public void run(junit.framework.TestResult);
  Signature: (Ljunit/framework/TestResult;)V
public void runTest(junit.framework.Test, junit.framework.TestResult);
  Signature: (Ljunit/framework/Test;Ljunit/framework/TestResult;)V
synchronized void waitUntilFinished();
  Signature: ()V
public synchronized void runFinished();
  Signature: ()V
}
  ...

Showing the Java Disassembly for all Methods in a Jar

#!/bin/bash

JAR_TO_INTERROGATE=$1
if [ -z "$JAR_TO_INTERROGATE" ]; then
  echo "Please provide JAR file name to interrogate"
  exit 1
fi

CLASSES_OF_JAR=$(jar tf "$JAR_TO_INTERROGATE" |grep class |sed 's/.class//g')

javap -classpath "$JAR_TO_INTERROGATE" -c $CLASSES_OF_JAR

This utility can be used as follows:

user@host:~$ disassemble_jar.sh junit-4.10.jar
Compiled from "ActiveTestSuite.java"
class junit.extensions.ActiveTestSuite$1 extends java.lang.Thread{
final junit.framework.Test val$test;

final junit.framework.TestResult val$result;

final junit.extensions.ActiveTestSuite this$0;

junit.extensions.ActiveTestSuite$1(junit.extensions.ActiveTestSuite, junit.framework.Test, junit.framework.TestResult);
  Code:
   0:	aload_0
   1:	aload_1
   2:	putfield	#1; //Field this$0:Ljunit/extensions/ActiveTestSuite;
   5:	aload_0
   6:	aload_2
   7:	putfield	#2; //Field val$test:Ljunit/framework/Test;
   10:	aload_0
   11:	aload_3
   12:	putfield	#3; //Field val$result:Ljunit/framework/TestResult;
   15:	aload_0
  ...

Prevent GIMP Toolbox and Layer Windows Appearing on All Desktops on XFCE

This article documents the solution found in this forum post.

The Problem

I was using Ubuntu (Xubuntu) 12.04 (precise) with XFCE as my window manager. When I started the graphical editor GIMP it would show three windows – a main (image) window, a toolbox window, and a layer window.

When switching to another desktop the toolbox and layer windows would persist while the main (image) window would stay on only that desktop to which it was assigned.

Solution

The solution to keep the toolbox and layer windows on only one desktop at a time?

From the main (image) window menu select Edit->Preferences…

Then choose “Window Management” from the list of categories on the left-hand side of the Preferences menu.

Select the Window Management category in the Preferences window

Select the Window Management category in the Preferences window

You’ll note that the options “Hint for the toolbox” and “Hint for other docks” are both set to “Utility window”. Select that option and change it to “Normal window” for both.

Select Normal Window for the hints

Select Normal Window for the hints

And your settings should look like this:

Both hint options should be set to Normal window

Both hint options should be set to Normal window

Click OK and you will be told you have to restart GIMP for the settings to take effect. So restart GIMP.

JMeter Could Not Initialize MenuFactory Class

I was getting the following messages when starting JMeter:

Uncaught Exception java.lang.NoSuchMethodError: kg.apc.jmeter.gui.GuiBuilderHelper.strechButtonToComponent(Ljavax/swing/JComponent;Ljavax/swing/JButton;)V. See log file for details.
Uncaught Exception java.lang.NoClassDefFoundError: Could not initialize class org.apache.jmeter.gui.util.MenuFactory. See log file for details.

In addition my Edit menu was greyed out in the JMeter window:

JMeter Edit Menu Greyed Out

JMeter Edit Menu Greyed Out

The problem was that I had been placing a custom written Java sampler into the lib/ext/ subdirectory and that plugin was including a conflicting .jar in its .jar (you can put .jars inside .jars).

The solution? Remove the following file from the .jar plugin I created:

  • JMeterPlugins.jar

The Perl For Variable Modification Gotcha

What does this code do?

#!/usr/bin/perl -w

use strict;

my @arr = ( 1, 2, 3, 4, 5, 6, 7, 8, 9 );

for my $val ( @arr ) {
    $val++;
}

The answer? It increments every value inside @arr!

Not what I expected – as I assumed that by assigning the value into a specified variable (especially one declared with my) all that would happen would be that the local variable $val gets incremented and have no side-effects on the array @arr.

This is a quirk of Perl whereby the variable is actually masquerading as the actual element within the array. You can try this code for yourself!