<commit_before>var util = require('util'),
    path = require('path'),
    j = require('path').join,
    i = function(obj, depth, showAll) { return util.inspect(obj, showAll, typeof depth === 'number' ? depth : 1); },
    async = require(j(process.env.LK_SCRIPTS_ROOT, 'node_modules/async'));

function uuid() { // helper
    var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }).toUpperCase();
    return id;
}

var debugLevel = 1;
var debugThreshold = 1;
var logSessionLifetime = true;

function log(/*level, msg, arguments*/) {
    // the smaller logLevel the more important the message
    var args = Array.prototype.slice.call(arguments);
    var logLevel = typeof args[0] === 'number' ? args.shift() : 1;
    if (logLevel > debugThreshold) return;
    console.log.apply(console, args);
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// session actions, when messages come in they specify in
// their "action" parameter what they want to do. This is
// the table that defines what functions are behind those
// actions.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
var sessionActions = {

    reportServices: function(sessionServer, connection, msg) {
        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {services: Object.keys(sessionServer.getActions())}
        });
    },

    registerClient: function(sessionServer, connection, msg) {
        // Adds a lively2lively session object for the client identified with
        // msg.data.id (or reuses the old session if client is already registered).
        // Note that there is a difference between the session object created
        // here and the (HTTP/websocket) connection object used to send/receive
        // messages: The session can persist even though the underlaying
        // connection may change or be closed. Session objects have the structure: {
        //     timeOfCreation: NUMBER,    -- timestamp of session creation, reported by client
        //     timeOfRegistration: NUMBER -- timestamp of session registration, attached by server
        //     lastActivity: NUMBER,      -- timestamp of last reported activity
        //     user: STRING,              -- name of the session, not unique
        //     worldURL: STRING           -- the URL of the connected "world"
        //     id: STRING                 -- unique, identifies the client, looks like 'client-session:E25F3CCC-F82D-41F4-B3E7-CDA398BD7D2D'
        //     remoteAddress: STRING      -- ip
        // }
        var sessions = sessionServer.getLocalSessions()[sessionServer.id()],
            session = sessions[msg.sender],
            isNewSession = false;

        if (!session) {
            isNewSession = true;
            session = sessions[msg.sender] = {}
        }

        util._extend(session, msg.data);
        util._extend(session, {
            remoteAddress: connection.remoteAddress,
            timeOfRegistration: new Date().getTime()
        });

        connection.id = msg.data.id;
        connection.on('close', function() {
            // remove the session registration after the connection closes
            // give it some time because the connection might be just flaky and
            // the client might come back
            if (!sessions[msg.sender]) return; // unregistered
            if (!sessionServer.inactiveSessionRemovalTime) return;
            setTimeout(function() {
                var newConnection = sessionServer.websocketServer.getConnection(msg.sender);
                if (!newConnection) {
                    log(2, '%s removes local session of %s', sessionServer, msg.sender);
                    delete sessions[msg.sender];
                    if (logSessionLifetime) {
                        console.log('l2l deregistration "%s" %s %s %s',
                            session.user, session.worldURL, session.id, session.remoteAddress);
                    }
                }
            }, sessionServer.inactiveSessionRemovalTime);
        });

        if (logSessionLifetime && isNewSession) {
            console.log('l2l registration "%s" %s %s %s',
                session.user, session.worldURL, session.id, session.remoteAddress);
        }

        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {success: true, tracker: {id: sessionServer.id()}}
        });
    },

    unregisterClient: function(sessionServer, connection, msg) {
        sessionServer.removeLocalSessionOf(msg.sender);
        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId, data: {success: true}});
        connection.close();
    },

    initServerToServerConnect: function(sessionServer, connection, msg) {
        var url = msg.data.url;
        sessionServer.serverToServerConnect(url, function(err, remoteClient) {
            if (err) console.error(err);
            connection.send({action: 'initServerToServerConnectResult', inResponseTo: msg.messageId, data: {success: !err}});
        });
    },

    initServerToServerDisconnect: function(sessionServer, connection, msg) {
        var remoteURLs = Object.keys(sessionServer.serverToServerConnections);
        sessionServer.removeServerToServerConnections();
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true, message: 'Connections to ' + remoteURLs.join(', ') + ' closed'}});
    },

    getSessions: function(sessionServer, connection, msg) {
        // send the sessions accessible from this tracker. includes local sessions,
        // reported sessions
        sessionServer.getSessionList(msg.data.options, function(sessions) {
            connection.send({
                action: msg.action,
                inResponseTo: msg.messageId,
                data: sessions
            });
        })
    },

    reportSessions: function(sessionServer, connection, msg) {
        // another tracker is reporting its sessions to sessionServer.
        // sessionServer is acting as a "lively2lively central"
        if (!msg.data || !msg.data.trackerId) {
            console.error('%s got reportSession request without id: ', sessionServer, msg);
            return;
        }
        var id = connection.id = msg.data.trackerId;
        connection.on('close', function() {
            if (!sessionServer.inactiveSessionRemovalTime) return;
            setTimeout(function() {
                var newConnection = sessionServer.websocketServer.getConnection(msg.sender);
                if (!newConnection) {
                    log(2, '%s removes reported session of %s', sessionServer, id);
                    delete sessionServer.trackerData[id];
                }
            }, sessionServer.inactiveSessionRemovalTime);
        });
        sessionServer.trackerData[id] = {sessions: msg.data[id]};
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true, message: 'Sessions added to ' + sessionServer}});
    },

    reportActivity: function(sessionServer, connection, msg) {
        // lively session sends infos about last user activity from time to time
         var sessions = sessionServer.getLocalSessions()[sessionServer.id()],
            session = sessions[msg.sender] = sessions[msg.sender] || {};
        session.lastActivity = msg.data.lastActivity;
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true}});
    }

}

var services = require("./LivelyServices").services;
util._extend(services, sessionActions);

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// SessionTracker
// The main server component for managing sessions. A
// session represents a onlinr connection of a lively
// world that can be used to communicate to/from that world
// The SessionTracker will manage the connections to Lively
// worlds and connections to other Lively servers (server-
// to-server connections). Those connections are used to
// a) route messages from one session to another (local
// seends as well as remote sends requiring server-to-server
// routing are supported).
// Also, certain actions (getSessions, reportActivity) are
// directly processed by the tracker and do not require
// connections to other sessions.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
var websockets = require(path.join(process.env.WORKSPACE_LK, 'core/servers/support/websockets'));
var WebSocketServer = websockets.WebSocketServer;
var WebSocketClient = websockets.WebSocketClient;

function SessionTracker(options) {
    // options = {route: STRING, subserver: OBJECT}
    this.debug = debugLevel;
    options = options || {};
    this.route = options.route + 'connect';
    this.subserver = options.subserver;
    this.trackerId = 'tracker-' + uuid();
    this.websocketServer = new WebSocketServer({sender: this.trackerId, debugLevel: this.debug});
    this.serverToServerConnections = {}; // for connections to other servers
    this.inactiveSessionRemovalTime = options.inactiveSessionRemovalTime || 60*1000;
    this.server2serverReconnectTimeout = options.server2serverReconnectTimeout || 60*1000;
    this.initTrackerData();
}

(function() {

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // initialization
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    this.id = function() { return this.trackerId }

    this.getActions = function(informFunc) {
        var sessionTracker = this;
        return Object.keys(services).reduce(function(actions, name) {
            if (informFunc) {
                actions[name] = function(con, msg) {
                    informFunc(sessionTracker, con, msg);
                    services[name](sessionTracker, con, msg);
                };
            } else {
                actions[name] = services[name].bind(null, sessionTracker);
            }
            return actions;
        }, {});
    }

    this.listen = function() {
        // accepting/connecting with local or server-to-server socket requests
        // this.websocketServer holds a list of those connections
        this._dispatchLivelyMessageFromServer = this._dispatchLivelyMessageFromServer
                                             || this.dispatchLivelyMessageFromServer.bind(this);
        this.websocketServer.on('lively-message', this._dispatchLivelyMessageFromServer);
        this.websocketServer.listen({
            route: this.route,
            actions: this.getActions(),
            subserver: this.subserver
        });
    }

    this.shutdown = function() {
        // really close the tracker, ensures that the tracker will not
        // attempt reconnects of closed sockets
        if (this._dispatchLivelyMessageFromServer)
            tracker.websocketServer.removeListener('lively-message', this._dispatchLivelyMessageFromServer);
        this.websocketServer && this.websocketServer.close();
        this.removeServerToServerConnections();
        this.stopServerToServerSessionReport();
    }

    this.initTrackerData = function() {
        // this is where the session data gets stored
        this.trackerData = {};
        this.trackerData[this.id()] = {
            isLocal: true,
            hostname: require('os').hostname(),
            sessions: {}
        }
    }

    this.resetTrackerData = function() {
        // cleanup and restart
        this.shutdown();
        this.initTrackerData();
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // message dispatch
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    this.dispatchLivelyMessage = function(msg, connection) {
        if (msg.target && msg.target !== this.trackerId) {
            this.routeMessage(msg, connection); return; }
        var actions = this.getActions(), action = actions[msg.action];
        if (action) action(connection, msg);
        else if (actions.messageNotUnderstood) actions.messageNotUnderstood(connection, msg);
        else connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {error: 'messageNotUnderstood'},
            target: msg.sender,
            messageId: 'tracker-msg:'+uuid()
        });
    }

    this.dispatchLivelyMessageFromServer = function(msg, connection) {
        this.dispatchLivelyMessage(msg, connection);
    }

    this.dispatchLivelyMessageFromServerToServerConnection = function(msg, connection) {
        this.dispatchLivelyMessage(msg, connection);
    }

    this.routeMessage = function(msg, connection) {
        log(3, '%s routing %s to %s', this, msg.action, msg.target);
        function answer(response) {
            log(3, '%s got answer from routed message %s: ', tracker, msg.messageId, response);
            connection.send({
                action: msg.action + 'Result',
                inResponseTo: msg.messageId,
                data: response.data,
                target: msg.sender,
                messageId: response.messageId
            });
        }
        var tracker = this, target = msg.target || msg.data.target;
        if (!target) { answer({data: {error: 'Message does not specify target'}}); return; }
        this.findConnection(target, function(err, targetConnection) {
            if (err || !targetConnection) {
                console.warn('%s failed to route message: Failure finding target connection: ', tracker, err);
                answer({data: {error: 'Failure finding target connection: ' + err, target: target}})
                return; }
            targetConnection.send({
                action: msg.action,
                data: msg.data,
                sender: msg.sender,
                target: target,
                messageId: msg.messageId
            }, answer);
        });
    }
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

    this.getSessionList = function(options, thenDo) {
        // session list of both local and remote sessions
        // will invoke getSessionList of server-to-server connections unless
        // explicitly stated otherwise in options
        options = options || {};
        var tasks = [this.getLocalSessions.bind(this, options)];
        if (!options.onlyLocal) tasks.push(this.getServerToServerSessionList.bind(this, options));
        var tracker = this;
        async.parallel(tasks, function(err, sessions) {
            if (err) {
                console.error('%s getSessionList: ', this, err);
            }
            var result = sessions[0];
            if (options.onlyLocal) { thenDo(result); return; }
            // sessions[1] looks like [{
            //  url: {
            //    connection: WEBSOCKET_CONNECTION,
            //    remoteTrackerId: ID,
            //    trackersWithSessions: {trackerId: {sessionId: {session_spec}}}
            //  }}]
            // flatten it so that {trackerId: {sessionId: {sess spec}*}}
            Object.keys(sessions[1]).forEach(function(url) {
                var remote = sessions[1][url];
                for (var id in remote.trackersWithSessions)
                    if (!result[id]) result[id] = remote.trackersWithSessions[id];
            });
            thenDo(result);
        });
    }

    this.getLocalSessions = function(options, thenDo) {
        var result = {};
        Object.keys(this.trackerData).forEach(function(trackerId) {
            result[trackerId] = this.trackerData[trackerId].sessions;
        }, this);
        if (!result[this.id()]) result[this.id()] = {};
        thenDo && thenDo(null, result);
        return result;
    }

    this.removeLocalSessionOf = function(sessionId) {
        var sessions = this.getLocalSessions()[this.id()];
        delete sessions[sessionId];
        this.websocketServer.removeConnection(sessionId);
    }

    this.getServerToServerSessionList = function(options, thenDo) {
        // returns something like
        // {'ws://localhost:9001/nodejs/SessionTrackerFederationTest2/connect': [
        //     {user: 'SessionTrackerTestUser2',
        //     worldURL: 'http://localhost:9001/lively2lively2.html',
        //     id: 'E<PASSWORD>' }]}
        var tracker = this;
        this.withServerToServerConnectionsDo(function(next, url, con) {
            if (!con.isOpen()) { next(null, {error: 'not connected'}); return; }
            con.send(
                {action: 'getSessions', data: {options: {onlyLocal: true}}},
                function(msg) {
                    // get sessions gives us a map like {
                    //   trackerId1: {sessionId1: {/*sessiondata*/}, sessionId2: {...}},
                    //   trackerId2: {sessionId3: {/*sessiondata*/}, sessionId4: {...}},
                    //   ... }
                    // note that the onlyLocal flag will still gives us sessions of
                    // other trackers that were added by those trackers themselves<commit_msg>Lively2Lively: improved session lifetime reporting by tracker<commit_after>var util = require('util'),
    path = require('path'),
    j = require('path').join,
    i = function(obj, depth, showAll) { return util.inspect(obj, showAll, typeof depth === 'number' ? depth : 1); },
    async = require(j(process.env.LK_SCRIPTS_ROOT, 'node_modules/async'));

function uuid() { // helper
    var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }).toUpperCase();
    return id;
}

var debugLevel = 1;
var debugThreshold = 1;
var logSessionLifetime = true;

function log(/*level, msg, arguments*/) {
    // the smaller logLevel the more important the message
    var args = Array.prototype.slice.call(arguments);
    var logLevel = typeof args[0] === 'number' ? args.shift() : 1;
    if (logLevel > debugThreshold) return;
    console.log.apply(console, args);
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// session actions, when messages come in they specify in
// their "action" parameter what they want to do. This is
// the table that defines what functions are behind those
// actions.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
var sessionActions = {

    reportServices: function(sessionServer, connection, msg) {
        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {services: Object.keys(sessionServer.getActions())}
        });
    },

    registerClient: function(sessionServer, connection, msg) {
        // Adds a lively2lively session object for the client identified with
        // msg.data.id (or reuses the old session if client is already registered).
        // Note that there is a difference between the session object created
        // here and the (HTTP/websocket) connection object used to send/receive
        // messages: The session can persist even though the underlaying
        // connection may change or be closed. Session objects have the structure: {
        //     timeOfCreation: NUMBER,    -- timestamp of session creation, reported by client
        //     timeOfRegistration: NUMBER -- timestamp of session registration, attached by server
        //     lastActivity: NUMBER,      -- timestamp of last reported activity
        //     user: STRING,              -- name of the session, not unique
        //     worldURL: STRING           -- the URL of the connected "world"
        //     id: STRING                 -- unique, identifies the client, looks like 'client-session:E25F3CCC-F82D-41F4-B3E7-CDA398BD7D2D'
        //     remoteAddress: STRING      -- ip
        // }
        var sessions = sessionServer.getLocalSessions()[sessionServer.id()],
            session = sessions[msg.sender],
            isNewSession = false;

        if (!session) {
            isNewSession = true;
            session = sessions[msg.sender] = {}
        }

        util._extend(session, msg.data);
        util._extend(session, {
            remoteAddress: connection.remoteAddress,
            timeOfRegistration: new Date().getTime()
        });

        connection.id = msg.data.id;
        connection.on('close', function() {
            // remove the session registration after the connection closes
            // give it some time because the connection might be just flaky and
            // the client might come back
            if (!sessions[msg.sender]) return; // unregistered
            if (!sessionServer.inactiveSessionRemovalTime) return;
            setTimeout(function() {
                var newConnection = sessionServer.websocketServer.getConnection(msg.sender);
                if (!newConnection) {
                    log(2, '%s removes local session of %s', sessionServer, msg.sender);
                    sessionServer.removeLocalSessionOf(msg.sender);
                    if (logSessionLifetime) {
                        console.log('l2l deregistration "%s" %s %s %s',
                            session.user, session.worldURL, session.id, session.remoteAddress);
                    }
                }
            }, sessionServer.inactiveSessionRemovalTime);
        });

        if (logSessionLifetime && isNewSession) {
            console.log('l2l registration "%s" %s %s %s',
                session.user, session.worldURL, session.id, session.remoteAddress);
        }

        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {success: true, tracker: {id: sessionServer.id()}}
        });
    },

    unregisterClient: function(sessionServer, connection, msg) {
        var session = sessionServer.removeLocalSessionOf(msg.sender);
        if (logSessionLifetime) {
            console.log('l2l deregistration "%s" %s %s %s',
                session.user, session.worldURL, session.id, session.remoteAddress);
        }
        connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId, data: {success: true}});
        connection.close();
    },

    initServerToServerConnect: function(sessionServer, connection, msg) {
        var url = msg.data.url;
        sessionServer.serverToServerConnect(url, function(err, remoteClient) {
            if (err) console.error(err);
            connection.send({action: 'initServerToServerConnectResult', inResponseTo: msg.messageId, data: {success: !err}});
        });
    },

    initServerToServerDisconnect: function(sessionServer, connection, msg) {
        var remoteURLs = Object.keys(sessionServer.serverToServerConnections);
        sessionServer.removeServerToServerConnections();
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true, message: 'Connections to ' + remoteURLs.join(', ') + ' closed'}});
    },

    getSessions: function(sessionServer, connection, msg) {
        // send the sessions accessible from this tracker. includes local sessions,
        // reported sessions
        sessionServer.getSessionList(msg.data.options, function(sessions) {
            connection.send({
                action: msg.action,
                inResponseTo: msg.messageId,
                data: sessions
            });
        })
    },

    reportSessions: function(sessionServer, connection, msg) {
        // another tracker is reporting its sessions to sessionServer.
        // sessionServer is acting as a "lively2lively central"
        if (!msg.data || !msg.data.trackerId) {
            console.error('%s got reportSession request without id: ', sessionServer, msg);
            return;
        }
        var id = connection.id = msg.data.trackerId;
        connection.on('close', function() {
            if (!sessionServer.inactiveSessionRemovalTime) return;
            setTimeout(function() {
                var newConnection = sessionServer.websocketServer.getConnection(msg.sender);
                if (!newConnection) {
                    log(2, '%s removes reported session of %s', sessionServer, id);
                    delete sessionServer.trackerData[id];
                }
            }, sessionServer.inactiveSessionRemovalTime);
        });
        sessionServer.trackerData[id] = {sessions: msg.data[id]};
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true, message: 'Sessions added to ' + sessionServer}});
    },

    reportActivity: function(sessionServer, connection, msg) {
        // lively session sends infos about last user activity from time to time
         var sessions = sessionServer.getLocalSessions()[sessionServer.id()],
            session = sessions[msg.sender] = sessions[msg.sender] || {};
        session.lastActivity = msg.data.lastActivity;
        connection.send({action: msg.action + 'Result', inResponseTo: msg.messageId, data: {success: true}});
    }

}

var services = require("./LivelyServices").services;
util._extend(services, sessionActions);

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// SessionTracker
// The main server component for managing sessions. A
// session represents a onlinr connection of a lively
// world that can be used to communicate to/from that world
// The SessionTracker will manage the connections to Lively
// worlds and connections to other Lively servers (server-
// to-server connections). Those connections are used to
// a) route messages from one session to another (local
// seends as well as remote sends requiring server-to-server
// routing are supported).
// Also, certain actions (getSessions, reportActivity) are
// directly processed by the tracker and do not require
// connections to other sessions.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
var websockets = require(path.join(process.env.WORKSPACE_LK, 'core/servers/support/websockets'));
var WebSocketServer = websockets.WebSocketServer;
var WebSocketClient = websockets.WebSocketClient;

function SessionTracker(options) {
    // options = {route: STRING, subserver: OBJECT}
    this.debug = debugLevel;
    options = options || {};
    this.route = options.route + 'connect';
    this.subserver = options.subserver;
    this.trackerId = 'tracker-' + uuid();
    this.websocketServer = new WebSocketServer({sender: this.trackerId, debugLevel: this.debug});
    this.serverToServerConnections = {}; // for connections to other servers
    this.inactiveSessionRemovalTime = options.inactiveSessionRemovalTime || 60*1000;
    this.server2serverReconnectTimeout = options.server2serverReconnectTimeout || 60*1000;
    this.initTrackerData();
}

(function() {

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // initialization
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    this.id = function() { return this.trackerId }

    this.getActions = function(informFunc) {
        var sessionTracker = this;
        return Object.keys(services).reduce(function(actions, name) {
            if (informFunc) {
                actions[name] = function(con, msg) {
                    informFunc(sessionTracker, con, msg);
                    services[name](sessionTracker, con, msg);
                };
            } else {
                actions[name] = services[name].bind(null, sessionTracker);
            }
            return actions;
        }, {});
    }

    this.listen = function() {
        // accepting/connecting with local or server-to-server socket requests
        // this.websocketServer holds a list of those connections
        this._dispatchLivelyMessageFromServer = this._dispatchLivelyMessageFromServer
                                             || this.dispatchLivelyMessageFromServer.bind(this);
        this.websocketServer.on('lively-message', this._dispatchLivelyMessageFromServer);
        this.websocketServer.listen({
            route: this.route,
            actions: this.getActions(),
            subserver: this.subserver
        });
    }

    this.shutdown = function() {
        // really close the tracker, ensures that the tracker will not
        // attempt reconnects of closed sockets
        if (this._dispatchLivelyMessageFromServer)
            tracker.websocketServer.removeListener('lively-message', this._dispatchLivelyMessageFromServer);
        this.websocketServer && this.websocketServer.close();
        this.removeServerToServerConnections();
        this.stopServerToServerSessionReport();
    }

    this.initTrackerData = function() {
        // this is where the session data gets stored
        this.trackerData = {};
        this.trackerData[this.id()] = {
            isLocal: true,
            hostname: require('os').hostname(),
            sessions: {}
        }
    }

    this.resetTrackerData = function() {
        // cleanup and restart
        this.shutdown();
        this.initTrackerData();
    }

    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
    // message dispatch
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

    this.dispatchLivelyMessage = function(msg, connection) {
        if (msg.target && msg.target !== this.trackerId) {
            this.routeMessage(msg, connection); return; }
        var actions = this.getActions(), action = actions[msg.action];
        if (action) action(connection, msg);
        else if (actions.messageNotUnderstood) actions.messageNotUnderstood(connection, msg);
        else connection.send({
            action: msg.action + 'Result',
            inResponseTo: msg.messageId,
            data: {error: 'messageNotUnderstood'},
            target: msg.sender,
            messageId: 'tracker-msg:'+uuid()
        });
    }

    this.dispatchLivelyMessageFromServer = function(msg, connection) {
        this.dispatchLivelyMessage(msg, connection);
    }

    this.dispatchLivelyMessageFromServerToServerConnection = function(msg, connection) {
        this.dispatchLivelyMessage(msg, connection);
    }

    this.routeMessage = function(msg, connection) {
        log(3, '%s routing %s to %s', this, msg.action, msg.target);
        function answer(response) {
            log(3, '%s got answer from routed message %s: ', tracker, msg.messageId, response);
            connection.send({
                action: msg.action + 'Result',
                inResponseTo: msg.messageId,
                data: response.data,
                target: msg.sender,
                messageId: response.messageId
            });
        }
        var tracker = this, target = msg.target || msg.data.target;
        if (!target) { answer({data: {error: 'Message does not specify target'}}); return; }
        this.findConnection(target, function(err, targetConnection) {
            if (err || !targetConnection) {
                console.warn('%s failed to route message: Failure finding target connection: ', tracker, err);
                answer({data: {error: 'Failure finding target connection: ' + err, target: target}})
                return; }
            targetConnection.send({
                action: msg.action,
                data: msg.data,
                sender: msg.sender,
                target: target,
                messageId: msg.messageId
            }, answer);
        });
    }
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

    this.getSessionList = function(options, thenDo) {
        // session list of both local and remote sessions
        // will invoke getSessionList of server-to-server connections unless
        // explicitly stated otherwise in options
        options = options || {};
        var tasks = [this.getLocalSessions.bind(this, options)];
        if (!options.onlyLocal) tasks.push(this.getServerToServerSessionList.bind(this, options));
        var tracker = this;
        async.parallel(tasks, function(err, sessions) {
            if (err) {
                console.error('%s getSessionList: ', this, err);
            }
            var result = sessions[0];
            if (options.onlyLocal) { thenDo(result); return; }
            // sessions[1] looks like [{
            //  url: {
            //    connection: WEBSOCKET_CONNECTION,
            //    remoteTrackerId: ID,
            //    trackersWithSessions: {trackerId: {sessionId: {session_spec}}}
            //  }}]
            // flatten it so that {trackerId: {sessionId: {sess spec}*}}
            Object.keys(sessions[1]).forEach(function(url) {
                var remote = sessions[1][url];
                for (var id in remote.trackersWithSessions)
                    if (!result[id]) result[id] = remote.trackersWithSessions[id];
            });
            thenDo(result);
        });
    }

    this.getLocalSessions = function(options, thenDo) {
        var result = {};
        Object.keys(this.trackerData).forEach(function(trackerId) {
            result[trackerId] = this.trackerData[trackerId].sessions;
        }, this);
        if (!result[this.id()]) result[this.id()] = {};
        thenDo && thenDo(null, result);
        return result;
    }

    this.removeLocalSessionOf = function(sessionId) {
        var sessions = this.getLocalSessions()[this.id()];
        var session = sessions[sessionId];
        delete sessions[sessionId];
        this.websocketServer.removeConnection(sessionId);
        return session;
    }

    this.getServerToServerSessionList = function(options, thenDo) {
        // returns something like
        // {'ws://localhost:9001/nodejs/SessionTrackerFederationTest2/connect': [
        //     {user: 'SessionTrackerTestUser2',
        //     worldURL: 'http://localhost:9001/lively2lively2.html',
        //     id: 'E447959D-93DE-4CB3-81E7-7BBA91CDE6CD' }]}
        var tracker = this;
        this.withServerToServerConnectionsDo(function(next, url, con) {
            if (!con.isOpen()) { next(null, {error: 'not connected'}); return; }
            con.send(
                {action: 'getSessions', data: {options: {onlyLocal: true}}},
                function(msg) {
                    // get sessions gives us a map like {
                    //   trackerId1: {sessionId1: {/*sessiondata*/}, sessionId2: {...}},
                    //   trackerId2: {sessionId3: {/*sessiondata*/}, sessionId4: {...}},
                    //   ... }
                    // note that the onlyLocal flag will still gives us sessions of
                    // other trackers that were added by those trackers themselves
{'fraction_non_alphanumeric': 0.1389818769479395, 'fraction_numerical': 0.004819346646658201, 'mean_word_length': 2.210394663702057, 'pattern_counts': {'":': 0, '<': 4, '<?xml version=': 0, '>': 6, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 4, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1184434', 'n_tokens_mistral': 9482, 'n_tokens_neox': 8946, 'n_words': 2247}