<commit_before>var ddb = require('../../config/dynamo_database').ddb;
var passport = require('passport');
var uuid = require('node-uuid');
var email = require('../service/email_service');
var mail_config =require('../../config/mail_config');
var userModel = require('../model/userModel');
var bcrypt =require('bcrypt');
var Promise = require('promise');
var _ = require("underscore");

var findAll = function (req, res){
	return ddb.scan('accounts', {}, function (err, users) {
		if (!err) {
			return res.send(users.items);
		} else {
			return console.log(err);
		}
	});
};

exports.update = function (req, res){
	var user = userModel.updateUser(req, false);

	return ddb.updateItem('accounts', req.params.username, null, user, function (err, users) {
		if (!err) {
			console.log("Updated user");
		} else {
			console.log(err);
		}
		return res.send(202, users);
	});
};

exports.findById = function (req, res){
	if(!req._passport.session.user) return res.send(401);
	return ddb.getItem('accounts', req._passport.session.user, null, {}, function (err, user) {
		if (!err) {
			return res.send(user);
		} else {
			return console.log(err);
		}
	});
};

exports.delete = function (req, res){
	ddb.deleteItem('accounts', req.params.username, null, {},  function (err, user) {
		if (!err) {
			console.log("Removed user");
			return res.send('');
		} else {
			console.log(err);
		}
	});
};

exports.register =  function(req, res, next) {
	var user = userModel.newUser(req, true);

	ddb.putItem('accounts', user, {}, function (err) {
		if (!err) {
			email.sendMail(mail_config.regularMailFrom, user.username, 'Confirmação de email', email.templateConfirm(user.comfirmationPass));
			return console.log("Created User");
		} else {
			return console.log(err);
		}
	});

	req.logIn(user, function(err) {
		if(err)
			return next(err);
		if(req.body.rememberme){
			req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7;
		}
		res.json(200, { "user": user });
	});
}

exports.login = function(req, res, next) {
	passport.authenticate('local', function(err, user) {

		if(err)     { return next(err); }
		if(!user)   { return res.send(400); }

		req.logIn(user, function(err) {
			if(err) {
				return next(err);
			}
			if(req.body.rememberme){
				req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7;
			}
			res.json(200, { "user": user });
		});
	})(req, res, next);
}

exports.logout= function(req, res) {
	req.logout();
	res.send(200);
}

exports.validadeMail = function (req, res){

	ddb.getItem('accounts', req.params.username, null, {}, function (err, user) {
		if (!err) {
			if(user.comfirmationPass == req.params.comfirmationPass){
				ddb.updateItem('accounts', req.params.username, null, {emailConfirmed:{value:"true"}}, function (err, users) {
					if (!err) {
						console.log("email validado");
					} else {
						console.log(err);
					}
					return res.send(202, "OK");
				});
			}
		}

		return res.send(403, "NOK");
	});
};

exports.findRatingsById = function (req, res){
	return ddb.scan('ratings', {filter : { username: { eq: req._passport.session.user}}}, function (err, ratings) {
		if (!err) {
			var response = {};
			ratings.items.forEach(function(element){
				response[element.id] = element.nota;
			});
			return res.send(response);
		} else {
			return console.log(err);
		}
	});
};

/*exports.findCompleteBlindRatingsById = function (req, res){
	return ddb.scan('blindRatings', {filter : { username: { eq: req._passport.session.user }}}, function (err, ratings) {
		if (!err){

		} else {
			return console.log(err);
		}
	});
};*/

multipleFindBlindRatings = function(resolve, reject, options, result){

	return ddb.scan('blindRatings', options, function (err, ratings) {
		if (!err){
			if(result == undefined)
				result = ratings.items;
			else
				result = _.union(result, ratings.items);

			if(ratings.lastEvaluateKey == {}){
				return resolve(result);
			}else{
				options.exclusiveStartKey = ratings.lastEvaluateKey;
				multipleFindBlindRatings(resolve,reject,options, result);
			}
		}else {
			return reject();
		}
	});
};

exports.findCompleteBlindRatingsById = function (req, res){
	var promise = new Promise(function(resolve, reject){
		multipleFindBlindRatings(resolve,reject, {filter : { username: { eq: req._passport.session.user }}});
	});

	promise.then(function(result) {
		var prop = [];
		var response = {};
		result.forEach(function(element){
			prop.push(element.id);
			response[element.id] = element.nota;
		});
		return ddb.scan('proposals', {filter : { id: { in: prop }}}, function (err, proposals) {
			if (!err) {
				proposals.items.forEach(function(element){
					element.nota = response[element.id];
				});
				return res.send(proposals.items);
			} else {
				return console.log(err);
			}
		});
	}, function(err) {
		console.log(err);
	});
};
<commit_msg>Debugando<commit_after>var ddb = require('../../config/dynamo_database').ddb;
var passport = require('passport');
var uuid = require('node-uuid');
var email = require('../service/email_service');
var mail_config =require('../../config/mail_config');
var userModel = require('../model/userModel');
var bcrypt =require('bcrypt');
var Promise = require('promise');
var _ = require("underscore");

var findAll = function (req, res){
	return ddb.scan('accounts', {}, function (err, users) {
		if (!err) {
			return res.send(users.items);
		} else {
			return console.log(err);
		}
	});
};

exports.update = function (req, res){
	var user = userModel.updateUser(req, false);

	return ddb.updateItem('accounts', req.params.username, null, user, function (err, users) {
		if (!err) {
			console.log("Updated user");
		} else {
			console.log(err);
		}
		return res.send(202, users);
	});
};

exports.findById = function (req, res){
	if(!req._passport.session.user) return res.send(401);
	return ddb.getItem('accounts', req._passport.session.user, null, {}, function (err, user) {
		if (!err) {
			return res.send(user);
		} else {
			return console.log(err);
		}
	});
};

exports.delete = function (req, res){
	ddb.deleteItem('accounts', req.params.username, null, {},  function (err, user) {
		if (!err) {
			console.log("Removed user");
			return res.send('');
		} else {
			console.log(err);
		}
	});
};

exports.register =  function(req, res, next) {
	var user = userModel.newUser(req, true);

	ddb.putItem('accounts', user, {}, function (err) {
		if (!err) {
			email.sendMail(mail_config.regularMailFrom, user.username, 'Confirmação de email', email.templateConfirm(user.comfirmationPass));
			return console.log("Created User");
		} else {
			return console.log(err);
		}
	});

	req.logIn(user, function(err) {
		if(err)
			return next(err);
		if(req.body.rememberme){
			req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7;
		}
		res.json(200, { "user": user });
	});
}

exports.login = function(req, res, next) {
	passport.authenticate('local', function(err, user) {

		if(err)     { return next(err); }
		if(!user)   { return res.send(400); }

		req.logIn(user, function(err) {
			if(err) {
				return next(err);
			}
			if(req.body.rememberme){
				req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7;
			}
			res.json(200, { "user": user });
		});
	})(req, res, next);
}

exports.logout= function(req, res) {
	req.logout();
	res.send(200);
}

exports.validadeMail = function (req, res){

	ddb.getItem('accounts', req.params.username, null, {}, function (err, user) {
		if (!err) {
			if(user.comfirmationPass == req.params.comfirmationPass){
				ddb.updateItem('accounts', req.params.username, null, {emailConfirmed:{value:"true"}}, function (err, users) {
					if (!err) {
						console.log("email validado");
					} else {
						console.log(err);
					}
					return res.send(202, "OK");
				});
			}
		}

		return res.send(403, "NOK");
	});
};

exports.findRatingsById = function (req, res){
	return ddb.scan('ratings', {filter : { username: { eq: req._passport.session.user}}}, function (err, ratings) {
		if (!err) {
			var response = {};
			ratings.items.forEach(function(element){
				response[element.id] = element.nota;
			});
			return res.send(response);
		} else {
			return console.log(err);
		}
	});
};

/*exports.findCompleteBlindRatingsById = function (req, res){
	return ddb.scan('blindRatings', {filter : { username: { eq: req._passport.session.user }}}, function (err, ratings) {
		if (!err){

		} else {
			return console.log(err);
		}
	});
};*/

multipleFindBlindRatings = function(resolve, reject, options, result){
	console.log("Nova Chamada ao banco")
	return ddb.scan('blindRatings', options, function (err, ratings) {
		if (!err){
			if(result == undefined)
				result = ratings.items;
			else
				result = _.union(result, ratings.items);

			consol.log(ratings.ScannedCount);
			if(ratings.lastEvaluateKey == {}){
				return resolve(result);
			}else{
				options.exclusiveStartKey = ratings.lastEvaluateKey;
				multipleFindBlindRatings(resolve,reject,options, result);
			}
		}else {
			return reject();
		}
	});
};

exports.findCompleteBlindRatingsById = function (req, res){
	var promise = new Promise(function(resolve, reject){
		multipleFindBlindRatings(resolve,reject, {filter : { username: { eq: req._passport.session.user }}});
	});

	promise.then(function(result) {
		var prop = [];
		var response = {};
		result.forEach(function(element){
			prop.push(element.id);
			response[element.id] = element.nota;
		});
		return ddb.scan('proposals', {filter : { id: { in: prop }}}, function (err, proposals) {
			if (!err) {
				proposals.items.forEach(function(element){
					element.nota = response[element.id];
				});
				return res.send(proposals.items);
			} else {
				return console.log(err);
			}
		});
	}, function(err) {
		console.log(err);
	});
};
<|endoftext|>


<commit_before>exports.fix = function (node) {
    if (node.type === 'BinaryExpression' && (node.operator === '==' || node.operator === '!=')) {
        var util = require('../util');
        var token = node.left.endToken.next;
        while (token) {
            if (token.type === 'Punctuator' && token.value === node.operator) {
                break;
            }
            token = token.next;
        }
        token.value = token.value === '==' ? '===' : '!==';
    }
};
<commit_msg>避免修改 == null<commit_after>exports.fix = function (node) {
    if (
        node.type === 'BinaryExpression'
        && (node.operator === '==' || node.operator === '!=')
        && node.right.value !== null
    ) {
        var util = require('../util');
        var token = node.left.endToken.next;
        while (token) {
            if (token.type === 'Punctuator' && token.value === node.operator) {
                break;
            }
            token = token.next;
        }
        token.value += '=';
    }
};
<|endoftext|>


<commit_before>var common = require('@screeps/common'),
    db = common.storage.db,
    env = common.storage.env,
    pubsub = common.storage.pubsub,
    config = common.configManager.config,
    native = require('../../native/build/Release/native.node'),
    ivm = require('isolated-vm'),
    q = require('q'),
    _ = require('lodash'),
    runtimeData = require('./data'),
    runtimeUserVm = require('./user-vm');

let staticTerrainData, staticTerrainDataSize = 0;

// Convert a room name to/from usable coordinates
// "E1N1" -> { xx: 129, yy: 126 }
let kWorldSize = 255; // Talk to marcel before growing world larger than W127N127 :: E127S127
function parseRoomName(roomName) {
    let room = /^([WE])([0-9]+)([NS])([0-9]+)$/.exec(roomName);
    if (!room) {
        throw new Error('Invalid room name '+roomName);
    }
    let rx = (kWorldSize >> 1) + (room[1] === 'W' ? -Number(room[2]) : Number(room[2]) + 1);
    let ry = (kWorldSize >> 1) + (room[3] === 'N' ? -Number(room[4]) : Number(room[4]) + 1);
    if (!(rx >=0 && rx <= kWorldSize && ry >= 0 && ry <= kWorldSize)) {
        throw new Error('Invalid room name '+roomName);
    }
    return { xx: rx, yy: ry };
}

function initPathFinder(rooms) {

    let terrainData = [];
    rooms.forEach(function(room) {
        let pack = new Uint8Array(50 * 50 / 4);
        let terrain = room.terrain;
        for (let xx = 0; xx < 50; ++xx) {
            for (let yy = 0; yy < 50; ++yy) {
                let ii = xx * 50 + yy;
                let bit = Number(terrain[yy * 50 + xx]);
                pack[ii / 4 | 0] = pack[ii / 4 | 0] & ~(0x03 << ii % 4 * 2) | bit << ii % 4 * 2;
            }
        }
        terrainData.push({
            room: parseRoomName(room.room),
            bits: pack,
        });
    });

    native.loadTerrain(terrainData);
}

function getAllTerrainData() {
    if(staticTerrainData) {
        return;
    }
    return runtimeData.getAllTerrainData()
        .then((result) => {

            if(staticTerrainData) {
                return;
            }

            initPathFinder(result);

            staticTerrainDataSize = result.length * 2500;
            let bufferConstructor = typeof SharedArrayBuffer === 'undefined' ? ArrayBuffer : SharedArrayBuffer;
            let view = new Uint8Array(new bufferConstructor(staticTerrainDataSize));
            staticTerrainData = {
                buffer: view.buffer,
                roomOffsets: {},
            };

            result.forEach((room, roomIndex) => {
                var offset = roomIndex * 2500;
                for (var i = 0; i < 2500; i++) {
                    view[i + offset] = Number(room.terrain.charAt(i));
                }
                staticTerrainData.roomOffsets[room.room] = offset;
            });

            console.log('Terrain shared buffer size:', staticTerrainDataSize);
        })
}

function getUserData(userId) {
    return db.users.findOne({_id: userId})
        .then((user) => {

            var cpu;
            if(user.cpu) {
                cpu = user.cpuAvailable || 0;
                if(user.skipTicksPenalty > 0) {
                    console.log(`Skip user execution ${user.username} (penalty ${user.skipTicksPenalty})`);
                    db.users.update({_id: user._id}, {$set: {
                        lastUsedCpu: 0,
                        lastUsedDirtyTime: 0
                    }, $inc: {skipTicksPenalty: -1}});
                    return q.reject({
                        type: 'error',
                        error: 'Your script is temporary blocked due to a hard reset inflicted to the runtime process.\nPlease try to change your code in order to prevent causing hard timeout resets.'
                    });
                }
                if(user.cpuAvailable < 0) {
                    console.log(`Skip user execution ${user.username} (${user.cpuAvailable})`);
                    db.users.update({_id: user._id}, {$set: {
                        lastUsedCpu: 0,
                        lastUsedDirtyTime: 0,
                        cpuAvailable: user.cpuAvailable < -user.cpu * 2 ? -user.cpu * 2 : user.cpuAvailable + user.cpu
                    }});
                    return q.reject({
                        type: 'error',
                        error: 'Script execution has been terminated: CPU bucket is empty'
                    });
                }
                cpu += user.cpu;
                if(cpu > config.engine.cpuMaxPerTick) {
                    cpu = config.engine.cpuMaxPerTick;
                }
            }
            else {
                cpu = Infinity;
            }

            return {user, cpu};

        });
}

async function make (scope, userId, onlyInRoom) {

    let userData;

    try {

        await getAllTerrainData();

        if(scope.abort) {
            throw 'aborted';
        }

        userData = await getUserData(userId);

        if(scope.abort) {
            throw 'aborted';
        }

    }
    catch(error) {
        console.error(error);
        if(_.isObject(error)) {
            throw error;
        }
        throw {error};
    }

    let runResult;

    try {

        let data = await runtimeData.get(userId, onlyInRoom);
        let dataRef = new ivm.ExternalCopy(data);

        if(scope.abort) {
            throw 'aborted';
        }

        await runtimeUserVm.create({
            userId,
            staticTerrainData,
            staticTerrainDataSize,
            codeTimestamp: data.userCodeTimestamp
        });

        let vm = runtimeUserVm.get(userId);

        let run = await vm.start.apply(undefined, [dataRef.copyInto()]);

        if(scope.abort) {
            throw 'aborted';
        }

        config.engine.emit('playerSandbox', {
            run(code) {
                return vm.isolate.compileScriptSync(code).runSync(vm.context);
            },
            set(name, value) {
                return vm.jail.setSync(name, new ivm.ExternalCopy(value).copyInto());
            },
            get(name) {
                return vm.jail.getSync(name).copySync();
            },
            getIsolate() {
                return vm.isolate;
            },
            getContext() {
                return vm.context;
            },
            getGlobal() {
                return vm.jail;
            }
        }, userId);

        runResult = await run.apply(undefined, [], {timeout: data.timeout});

        run.dispose();
        dataRef.dispose();

        var $set = {
            lastUsedCpu: runResult.usedTime,
            lastUsedDirtyTime: runResult.usedDirtyTime
        };
        if (runResult.activeSegments) {
            $set.activeSegments = runResult.activeSegments;
        }
        if (runResult.defaultPublicSegment !== undefined) {
            $set.defaultPublicSegment = runResult.defaultPublicSegment;
        }
        if (userData.cpu < Infinity) {
            var newCpuAvailable = userData.user.cpuAvailable + userData.user.cpu - runResult.usedTime;
            if(newCpuAvailable > config.engine.cpuBucketSize) {
                newCpuAvailable = config.engine.cpuBucketSize;
            }
            $set.cpuAvailable = newCpuAvailable;
        }

        db.users.update({_id: userData.user._id}, {$set});

        if (runResult.activeForeignSegment !== undefined) {
            if (runResult.activeForeignSegment === null) {
                db.users.update({_id: userData.user._id}, {
                    $unset: {
                        activeForeignSegment: true
                    }
                });
            }
            else {
                if (userData.user.activeForeignSegment &&
                    runResult.activeForeignSegment.username == userData.user.activeForeignSegment.username &&
                    runResult.activeForeignSegment.id) {
                    db.users.update({_id: userData.user._id}, {$merge: {
                        activeForeignSegment: {id: runResult.activeForeignSegment.id}
                    }});
                }
                else {
                    db.users.findOne({username: runResult.activeForeignSegment.username}, {defaultPublicSegment: true})
                        .then(user => {
                            runResult.activeForeignSegment.user_id = user._id;
                            if(!runResult.activeForeignSegment.id && user.defaultPublicSegment) {
                                runResult.activeForeignSegment.id = user.defaultPublicSegment;
                            }
                        })
                        .finally(() => {
                            db.users.update({_id: userData.user._id}, {$set: {
                                activeForeignSegment: runResult.activeForeignSegment
                            }});
                        })
                }
            }
        }

        if(runResult.publicSegments) {
            env.set(env.keys.PUBLIC_MEMORY_SEGMENTS+userData.user._id, runResult.publicSegments.join(','));
        }

        if(runResult.visual) {
            for (var roomName in runResult.visual) {
                env.setex(
                    env.keys.ROOM_VISUAL + userData.user._id + ',' + roomName + ',' + data.time,
                    config.engine.mainLoopResetInterval / 1000,
                    runResult.visual[roomName]);
            }
        }

        if (/CPU limit reached/.test(runResult.error)) {
            pubsub.publish(`user:${userData.user._id}/cpu`, JSON.stringify({
                cpu: 'error',
                memory: runResult.memory.data.length
            }));
        }
        else {
            pubsub.publish(`user:${userData.user._id}/cpu`, JSON.stringify({
                cpu: runResult.usedTime,
                memory: runResult.memory.data.length
            }));
        }

        runResult.username = userData && userData.user && userData.user.username;

        return runResult;
    }
    catch (error) {
        if(/Isolate is disposed/.test(""+error) ||
            /Isolate has exhausted v8 heap space/.test(""+error)) {
            runtimeUserVm.clear(userId);
        }
        if(/Array buffer allocation failed/.test(""+error) && !runResult) {
            runtimeUserVm.clear(userId);
            throw { error: "Script execution has been terminated: unable to allocate memory, restarting virtual machine"};
        }
        throw {error: error.stack || error};
    }
}

module.exports = function(userId, onlyInRoom) {
    const scope = {abort: false};
    let timeout;
    return new Promise((resolve, reject) => {
        timeout = setTimeout(() => {
            scope.abort = true;
            reject({error: 'Script execution timed out ungracefully, restarting virtual machine'});
            runtimeUserVm.clear(userId);
            console.error('isolated-vm timeout', userId);
            pubsub.publish(`user:${userId}/cpu`, JSON.stringify({cpu: 'error'}));
        }, 5000);
        make(scope, userId, onlyInRoom).then(resolve).catch(reject);
    })
        .then(result => {
            clearTimeout(timeout);
            return result;
        })
        .catch(error => {
            clearTimeout(timeout);
            return Promise.reject(error);
        })
};
<commit_msg>fix(runtime): Fixes double-joining of publicSegment data (#27)<commit_after>var common = require('@screeps/common'),
    db = common.storage.db,
    env = common.storage.env,
    pubsub = common.storage.pubsub,
    config = common.configManager.config,
    native = require('../../native/build/Release/native.node'),
    ivm = require('isolated-vm'),
    q = require('q'),
    _ = require('lodash'),
    runtimeData = require('./data'),
    runtimeUserVm = require('./user-vm');

let staticTerrainData, staticTerrainDataSize = 0;

// Convert a room name to/from usable coordinates
// "E1N1" -> { xx: 129, yy: 126 }
let kWorldSize = 255; // Talk to marcel before growing world larger than W127N127 :: E127S127
function parseRoomName(roomName) {
    let room = /^([WE])([0-9]+)([NS])([0-9]+)$/.exec(roomName);
    if (!room) {
        throw new Error('Invalid room name '+roomName);
    }
    let rx = (kWorldSize >> 1) + (room[1] === 'W' ? -Number(room[2]) : Number(room[2]) + 1);
    let ry = (kWorldSize >> 1) + (room[3] === 'N' ? -Number(room[4]) : Number(room[4]) + 1);
    if (!(rx >=0 && rx <= kWorldSize && ry >= 0 && ry <= kWorldSize)) {
        throw new Error('Invalid room name '+roomName);
    }
    return { xx: rx, yy: ry };
}

function initPathFinder(rooms) {

    let terrainData = [];
    rooms.forEach(function(room) {
        let pack = new Uint8Array(50 * 50 / 4);
        let terrain = room.terrain;
        for (let xx = 0; xx < 50; ++xx) {
            for (let yy = 0; yy < 50; ++yy) {
                let ii = xx * 50 + yy;
                let bit = Number(terrain[yy * 50 + xx]);
                pack[ii / 4 | 0] = pack[ii / 4 | 0] & ~(0x03 << ii % 4 * 2) | bit << ii % 4 * 2;
            }
        }
        terrainData.push({
            room: parseRoomName(room.room),
            bits: pack,
        });
    });

    native.loadTerrain(terrainData);
}

function getAllTerrainData() {
    if(staticTerrainData) {
        return;
    }
    return runtimeData.getAllTerrainData()
        .then((result) => {

            if(staticTerrainData) {
                return;
            }

            initPathFinder(result);

            staticTerrainDataSize = result.length * 2500;
            let bufferConstructor = typeof SharedArrayBuffer === 'undefined' ? ArrayBuffer : SharedArrayBuffer;
            let view = new Uint8Array(new bufferConstructor(staticTerrainDataSize));
            staticTerrainData = {
                buffer: view.buffer,
                roomOffsets: {},
            };

            result.forEach((room, roomIndex) => {
                var offset = roomIndex * 2500;
                for (var i = 0; i < 2500; i++) {
                    view[i + offset] = Number(room.terrain.charAt(i));
                }
                staticTerrainData.roomOffsets[room.room] = offset;
            });

            console.log('Terrain shared buffer size:', staticTerrainDataSize);
        })
}

function getUserData(userId) {
    return db.users.findOne({_id: userId})
        .then((user) => {

            var cpu;
            if(user.cpu) {
                cpu = user.cpuAvailable || 0;
                if(user.skipTicksPenalty > 0) {
                    console.log(`Skip user execution ${user.username} (penalty ${user.skipTicksPenalty})`);
                    db.users.update({_id: user._id}, {$set: {
                        lastUsedCpu: 0,
                        lastUsedDirtyTime: 0
                    }, $inc: {skipTicksPenalty: -1}});
                    return q.reject({
                        type: 'error',
                        error: 'Your script is temporary blocked due to a hard reset inflicted to the runtime process.\nPlease try to change your code in order to prevent causing hard timeout resets.'
                    });
                }
                if(user.cpuAvailable < 0) {
                    console.log(`Skip user execution ${user.username} (${user.cpuAvailable})`);
                    db.users.update({_id: user._id}, {$set: {
                        lastUsedCpu: 0,
                        lastUsedDirtyTime: 0,
                        cpuAvailable: user.cpuAvailable < -user.cpu * 2 ? -user.cpu * 2 : user.cpuAvailable + user.cpu
                    }});
                    return q.reject({
                        type: 'error',
                        error: 'Script execution has been terminated: CPU bucket is empty'
                    });
                }
                cpu += user.cpu;
                if(cpu > config.engine.cpuMaxPerTick) {
                    cpu = config.engine.cpuMaxPerTick;
                }
            }
            else {
                cpu = Infinity;
            }

            return {user, cpu};

        });
}

async function make (scope, userId, onlyInRoom) {

    let userData;

    try {

        await getAllTerrainData();

        if(scope.abort) {
            throw 'aborted';
        }

        userData = await getUserData(userId);

        if(scope.abort) {
            throw 'aborted';
        }

    }
    catch(error) {
        console.error(error);
        if(_.isObject(error)) {
            throw error;
        }
        throw {error};
    }

    let runResult;

    try {

        let data = await runtimeData.get(userId, onlyInRoom);
        let dataRef = new ivm.ExternalCopy(data);

        if(scope.abort) {
            throw 'aborted';
        }

        await runtimeUserVm.create({
            userId,
            staticTerrainData,
            staticTerrainDataSize,
            codeTimestamp: data.userCodeTimestamp
        });

        let vm = runtimeUserVm.get(userId);

        let run = await vm.start.apply(undefined, [dataRef.copyInto()]);

        if(scope.abort) {
            throw 'aborted';
        }

        config.engine.emit('playerSandbox', {
            run(code) {
                return vm.isolate.compileScriptSync(code).runSync(vm.context);
            },
            set(name, value) {
                return vm.jail.setSync(name, new ivm.ExternalCopy(value).copyInto());
            },
            get(name) {
                return vm.jail.getSync(name).copySync();
            },
            getIsolate() {
                return vm.isolate;
            },
            getContext() {
                return vm.context;
            },
            getGlobal() {
                return vm.jail;
            }
        }, userId);

        runResult = await run.apply(undefined, [], {timeout: data.timeout});

        run.dispose();
        dataRef.dispose();

        var $set = {
            lastUsedCpu: runResult.usedTime,
            lastUsedDirtyTime: runResult.usedDirtyTime
        };
        if (runResult.activeSegments) {
            $set.activeSegments = runResult.activeSegments;
        }
        if (runResult.defaultPublicSegment !== undefined) {
            $set.defaultPublicSegment = runResult.defaultPublicSegment;
        }
        if (userData.cpu < Infinity) {
            var newCpuAvailable = userData.user.cpuAvailable + userData.user.cpu - runResult.usedTime;
            if(newCpuAvailable > config.engine.cpuBucketSize) {
                newCpuAvailable = config.engine.cpuBucketSize;
            }
            $set.cpuAvailable = newCpuAvailable;
        }

        db.users.update({_id: userData.user._id}, {$set});

        if (runResult.activeForeignSegment !== undefined) {
            if (runResult.activeForeignSegment === null) {
                db.users.update({_id: userData.user._id}, {
                    $unset: {
                        activeForeignSegment: true
                    }
                });
            }
            else {
                if (userData.user.activeForeignSegment &&
                    runResult.activeForeignSegment.username == userData.user.activeForeignSegment.username &&
                    runResult.activeForeignSegment.id) {
                    db.users.update({_id: userData.user._id}, {$merge: {
                        activeForeignSegment: {id: runResult.activeForeignSegment.id}
                    }});
                }
                else {
                    db.users.findOne({username: runResult.activeForeignSegment.username}, {defaultPublicSegment: true})
                        .then(user => {
                            runResult.activeForeignSegment.user_id = user._id;
                            if(!runResult.activeForeignSegment.id && user.defaultPublicSegment) {
                                runResult.activeForeignSegment.id = user.defaultPublicSegment;
                            }
                        })
                        .finally(() => {
                            db.users.update({_id: userData.user._id}, {$set: {
                                activeForeignSegment: runResult.activeForeignSegment
                            }});
                        })
                }
            }
        }

        if(runResult.publicSegments) {
            env.set(env.keys.PUBLIC_MEMORY_SEGMENTS+userData.user._id, runResult.publicSegments);
        }

        if(runResult.visual) {
            for (var roomName in runResult.visual) {
                env.setex(
                    env.keys.ROOM_VISUAL + userData.user._id + ',' + roomName + ',' + data.time,
                    config.engine.mainLoopResetInterval / 1000,
                    runResult.visual[roomName]);
            }
        }

        if (/CPU limit reached/.test(runResult.error)) {
            pubsub.publish(`user:${userData.user._id}/cpu`, JSON.stringify({
                cpu: 'error',
                memory: runResult.memory.data.length
            }));
        }
        else {
            pubsub.publish(`user:${userData.user._id}/cpu`, JSON.stringify({
                cpu: runResult.usedTime,
                memory: runResult.memory.data.length
            }));
        }

        runResult.username = userData && userData.user && userData.user.username;

        return runResult;
    }
    catch (error) {
        if(/Isolate is disposed/.test(""+error) ||
            /Isolate has exhausted v8 heap space/.test(""+error)) {
            runtimeUserVm.clear(userId);
        }
        if(/Array buffer allocation failed/.test(""+error) && !runResult) {
            runtimeUserVm.clear(userId);
            throw { error: "Script execution has been terminated: unable to allocate memory, restarting virtual machine"};
        }
        throw {error: error.stack || error};
    }
}

module.exports = function(userId, onlyInRoom) {
    const scope = {abort: false};
    let timeout;
    return new Promise((resolve, reject) => {
        timeout = setTimeout(() => {
            scope.abort = true;
            reject({error: 'Script execution timed out ungracefully, restarting virtual machine'});
            runtimeUserVm.clear(userId);
            console.error('isolated-vm timeout', userId);
            pubsub.publish(`user:${userId}/cpu`, JSON.stringify({cpu: 'error'}));
        }, 5000);
        make(scope, userId, onlyInRoom).then(resolve).catch(reject);
    })
        .then(result => {
            clearTimeout(timeout);
            return result;
        })
        .catch(error => {
            clearTimeout(timeout);
            return Promise.reject(error);
        })
};
<|endoftext|>


<commit_before>var _ = require('underscore')._;

// Helper functions:

// Is a number (ignores type).
var isNumeric = function(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
};

// Used to fetch current values.
var getter = function(value, properties) {
  // Most calculations happen within the setter and the value passed is typically the value we want to use.
  // Typically, the getter just returns the value.
  // Modifications to the value within the getter are not written to the object.

  // Return default value if present & current value is undefined -- do not write to object
  if(_.isUndefined(value) && !_.isUndefined(properties.default)) {
    value = setter.call(this, (_.isFunction(properties.default) ? properties.default.call(this) : properties.default), value, properties);
  }

  return value;
};

// Returns typecasted value if possible. If rejected, originalValue is returned.
var setter = function(value, originalValue, properties) {
  // Allow transform to manipulate raw properties.
  if(properties.transform) {
    value = properties.transform.call(this, value, originalValue, properties);
  }

  switch(properties.type) {
    case 'string':
      // Reject if object or array.
      if(_.isObject(value) || _.isArray(value)) {
        return originalValue;
      }

      // If index is being set with null or undefined, set value and end.
      if(_.isUndefined(value) || value === null) {
        return value;
      }

      // Typecast to String.
      value = value + '';

      // If stringTransform function is defined, use.
      // This is used before we do validation checks (except to be sure we have a string at all).
      if(properties.stringTransform) {
        value = properties.stringTransform.call(this, value, originalValue, properties);
      }

      // If clip property & maxLength properties are set, the string should be clipped.
      // This is basically a shortcut property that could be done with stringTransform.
      if(properties.clip && !_.isUndefined(properties.maxLength)) {
        value = value.substr(0, properties.maxLength);
      }

      // Check against filters before returning value
      if(
        // If enum is being used, be sure the value is within definition.
        (_.isArray(properties.enum) && properties.enum.indexOf(value) === -1)

        // If minLength is defined, check to be sure the string is > minLength
        || (!_.isUndefined(properties.minLength) && value.length < properties.minLength)

        // If maxLength is defined, check to be sure the string is < maxLength
        || (!_.isUndefined(properties.maxLength) && value.length > properties.maxLength)

        // If regex is defined, check to be sure the string matches the regex pattern.
        || (properties.regex && !properties.regex.test(value))
      ) {
        // If the string doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
    break;

    case 'number':
      // Set values for boolean.
      if(_.isBoolean(value)) {
        value = value ? 1 : 0;
      }

      if(
        // Reject if array.
        _.isArray(value)

        // Reject if object.
        || _.isObject(value)

        // Reject if not numeric.
        || !isNumeric(value)
      ) {
         // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      // Typecast to number.
      value = value * 1;

      // Check against filters before returning value
      if(
        // If min is defined, check to be sure the integer is > min
        (!_.isUndefined(properties.min) && value < properties.min)

        // If max is defined, check to be sure the string is < max
        || (!_.isUndefined(properties.max) && value > properties.max)
      ) {
        // If the string doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
    break;

    case 'boolean':
      // If is String and is 'false', return false.
      if(_.isString(value) && value === 'false') {
        return false;
      }

      // If is Number, <0 is true and >0 is false.
      if(isNumeric(value)) {
        value = value * 1;
        return value > 0 ? true : false;
      }

      // Use Javascript to eval and return boolean.
      return value ? true : false;
    break;

    case 'array':
      // If it's an object, typecast to an array and return array.
      if(_.isObject(value)) {
        value = _.toArray(value);
      }

      // Reject if not array
      if(!_.isArray(value)) {
        // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
    break;

    case 'object':
      // If it's not an Object, reject.
      if(!_.isObject(value)) {
        // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      // If object is schema object and an entirely new object was passed, clear values and set.
      // This preserves the object instance.
      if(properties.objectType) {
        // The object will usually exist because it's initialized immediately for deep access within SchemaObjects.
        // However, in the case of Array elements, it will not exist.
        var schemaObject;
        if(!_.isUndefined(originalValue)) {
          // Clear existing values.
          schemaObject = originalValue;
          schemaObject.clear();
        } else {
          // The SchemaObject doesn't exist yet. Let's initialize a new one.
          // This is used for Array types.
          schemaObject = new properties.objectType;
        }

        // Copy value to SchemaObject and set value to SchemaObject.
        _.each(value, function(v, k) {
          schemaObject[k] = v;
        });
        value = schemaObject;
      }

      // Otherwise, it's OK.
      return value;
    break;

    case 'date':
      // Reject if object, array or boolean.
      if(!_.isDate(value) && !_.isString(value) && !_.isNumber(value)) {
        return originalValue;
      }

      // Attempt to parse string value with Date.parse (which returns number of milliseconds).
      if(_.isString(value)) {
        value = Date.parse(value);
      }

      // If is timestamp, convert to Date.
      if(_.isNumber(value)) {
        value = new Date((value + '').length > 10 ? value : value * 1000);
      }

      // If the date couldn't be parsed, do not modify index.
      if(value == 'Invalid Date' || !_.isDate(value)) {
        value = originalValue;
      }

      return value;
    break;

    default:
      return value;
    break;
  }
};

// Properties can be passed in multiple forms (an object, just a type, etc).
// Normalize to a standard format.
var normalizeProperties = function(properties) {
  // Allow for shorthand type declaration:

  // index: Type is translated to index: {type: Type}
  if(properties && _.isUndefined(properties.type)) {
    properties = {type: properties};
  }

  // Type may be an object with properties.
  // If "type.type" exists, we'll assume it's meant to be properties.
  // This means that shorthand objects can't use the "type" index.
  // If "type" is necessary, they must be wrapped in a SchemaObject.
  if(_.isObject(properties.type) && !_.isUndefined(properties.type.type)) {
    _.each(properties.type, function(value, key) {
      if(_.isUndefined(properties[key])) {
        properties[key] = value;
      }
    });
    properties.type = properties.type.type;
  }

  // Null or undefined should be flexible and allow any value.
  if(properties.type === null || properties.type === undefined) {
    properties.type = 'any';

  // Convert object representation of type to lowercase string.
  // String is converted to 'string', Number to 'number', etc.
  } else if(properties.type.name) {
    properties.type = properties.type.name;
  }
  if(_.isString(properties.type)) {
    properties.type = properties.type.toLowerCase();
  }

  // index: [Type] or index: [] is translated to index: {type: Array, arrayType: Type}
  if(_.isArray(properties.type)) {
    if(_.size(properties.type)) {
      // Properties will be normalized when array is initialized.
      properties.arrayType = properties.type[0];
    }
    properties.type = 'array';
  }

  // index: {} or index: SchemaObject is translated to index: {type: Object, objectType: Type}
  // SchemaObject factory is initialized when raw schema is provided.
  if(!_.isString(properties.type)) {
    if(_.isFunction(properties.type)) {
      properties.objectType = properties.type;
      properties.type = 'object';
    } else if(properties.type === {}) {
      properties.type = 'object';
    } else if(_.isObject(properties.type) && _.size(properties.type)) {
      properties.objectType = new SchemaObject(properties.type);
      properties.type = 'object';
    }
  }

  return properties;
};

// Represents a basic array with typecasted values.
var SchemaArray = function(self, properties) {
  this._self = self;
  this._properties = properties;

  if(this._properties.arrayType) {
    this._properties.arrayType = normalizeProperties(this._properties.arrayType);
  }
};
SchemaArray.prototype = new Array;
SchemaArray.prototype.push = function() {
  // Values are passed through the setter before being allowed onto the array if arrayType is set.
  // In the case of rejection, the setter returns undefined, which is not appended to the array.
  var values;
  if(this._properties.arrayType) {
    values = [].map.call(arguments, function(value) {
      return setter.call(this._self, value, undefined, this._properties.arrayType);
    }, this);
  } else {
    values = arguments;
  }

  if(this._properties.unique) {
    values = _.difference(values, _.toArray(this));
  }

  var ret = [].push.apply(this, values);

  return ret;
};
SchemaArray.prototype.toArray = function() {
  // Create new Array to hold elements.
  var array = [];

  // Loop through each element, clone if necessary.
  _.each(this, function(element) {
    // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects).
    if(_.isObject(element) && _.isFunction(element.toObject)) {
      element = element.toObject();

    // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object.
    } else if(_.isObject(element)) {
      element = _.clone(element);
    }

    array.push(element);
  });

  return array;
};

// Represents an object with typed indexes.
var SchemaObject = function(schema) {
  return function(defaults) {
    var self = this;

    // Object used to store properties internally.
    var obj;
    self._obj = obj = {};

    // Schema as defined by constructor.
    self._schema = schema;

    // Define getters/setters based off of schema.
    _.each(schema, function(properties, index) {
      // Normalize properties to allow for various shorthand declarations.
      schema[index] = properties = normalizeProperties(properties);

      // The actual index on the object may be an alias.
      var objIndex = index;
      if(properties.type === 'alias') {
        // Use properties of referenced index.
        // TODO: Allow for alias to be recognized via dot-notation.
        objIndex = properties.index;
        var referencedProperties = normalizeProperties(schema[objIndex]);
        referencedProperties.isAlias = true;

        // Allow alias to use transform() to pre-transform any values passing through it.
        if(properties.transform) {
          referencedProperties.transform = function(value, originalValue, properties) {
            value = properties.transform.call(this, value, originalValue, properties);
            value = referencedProperties.transform.call(this, value, originalValue, properties);
            return value;
          };
        }
      }

      // Getter / setter.
      self.__defineGetter__(index, function() {
        return getter.call(self, obj[objIndex], properties);
      });
      self.__defineSetter__(index, function(value) {
        // Don't proceed if readOnly is true.
        if(properties.readOnly) {
          return;
        }

        // self[index] is used instead of obj[index] to route through the getter
        obj[objIndex] = setter.call(self, value, self[objIndex], properties);
      });

      // In case of object & array, they must be initialized immediately. However, this should not be done for aliases.
      if(properties.isAlias !== true) {
        if(properties.type === 'object') {
          obj[objIndex] = properties.objectType ? new properties.objectType : {};

        // Native arrays are never used so that toArray can be globally supported.
        // Additionally, other properties such as unique rely on passing through SchemaObject.
        } else if(properties.type === 'array') {
          obj[objIndex] = new SchemaArray(self, properties);
        }
      }
    });

    // Return raw object.
    this.toObject = function() {
      var getObj = {};

      // Populate all properties in schema.
      _.each(schema, function(properties, index) {
        // Do not write values to object that are marked as writeOnly
        if(properties.invisible) {
          return;
        }

        // Fetch value from self[index] to route through getter.
        var value = self[index];

        // If value does not need to be cloned, place in index.
        if((value === undefined || value === null)
        || properties.type !== 'object' && properties.type !== 'array' && properties.type !== 'date') {
          getObj[index] = value;

        // Clone Object
        } else if(properties.type === 'object') {
          // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects).
          if(_.isFunction(value.toObject)) {
            getObj[index] = value.toObject();

          // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object.
          } else if(_.isObject(value)) {
            getObj[index] = _.clone(value);
          }

        // Clone Array
        } else if(properties.type === 'array') {
          // Built in method to clone array to native type
          getObj[index] = value.toArray();

        // Clone Date object.
        } else if(properties.type === 'date') {
          // https://github.com/documentcloud/underscore/pull/863
          // _.clone doesn't work on Date object.
          getObj[index] = new Date(value.getTime());
        }
      });

      return getObj;
    }

    // Clear all values.
    this.clear = function() {
      self._obj = {};
    }

    // Populate runtime defaults as provided to this instance of object.
    // (Different than the default for each field - is simply a shortcut to populate values in object.)
    if(_.isObject(defaults)) {
      _.each(defaults, function(value, key) {
        self[key] = value;
      });
    };

    return self;
  }
};

module.exports = SchemaObject;<commit_msg>Minor code cleanup.<commit_after>var _ = require('underscore')._;

// Is a number (ignores type).
var isNumeric = function(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
};

// Used to fetch current values.
var getter = function(value, properties) {
  // Most calculations happen within the setter and the value passed is typically the value we want to use.
  // Typically, the getter just returns the value.
  // Modifications to the value within the getter are not written to the object.

  // Return default value if present & current value is undefined -- do not write to object
  if(_.isUndefined(value) && !_.isUndefined(properties.default)) {
    value = setter.call(this, (_.isFunction(properties.default) ? properties.default.call(this) : properties.default), value, properties);
  }

  return value;
};

// Returns typecasted value if possible. If rejected, originalValue is returned.
var setter = function(value, originalValue, properties) {
  // Allow transform to manipulate raw properties.
  if(properties.transform) {
    value = properties.transform.call(this, value, originalValue, properties);
  }

  switch(properties.type) {
    case 'string':
      // Reject if object or array.
      if(_.isObject(value) || _.isArray(value)) {
        return originalValue;
      }

      // If index is being set with null or undefined, set value and end.
      if(_.isUndefined(value) || value === null) {
        return value;
      }

      // Typecast to String.
      value = value + '';

      // If stringTransform function is defined, use.
      // This is used before we do validation checks (except to be sure we have a string at all).
      if(properties.stringTransform) {
        value = properties.stringTransform.call(this, value, originalValue, properties);
      }

      // If clip property & maxLength properties are set, the string should be clipped.
      // This is basically a shortcut property that could be done with stringTransform.
      if(properties.clip && !_.isUndefined(properties.maxLength)) {
        value = value.substr(0, properties.maxLength);
      }

      // Check against filters before returning value
      if(
        // If enum is being used, be sure the value is within definition.
        (_.isArray(properties.enum) && properties.enum.indexOf(value) === -1)

        // If minLength is defined, check to be sure the string is > minLength
        || (!_.isUndefined(properties.minLength) && value.length < properties.minLength)

        // If maxLength is defined, check to be sure the string is < maxLength
        || (!_.isUndefined(properties.maxLength) && value.length > properties.maxLength)

        // If regex is defined, check to be sure the string matches the regex pattern.
        || (properties.regex && !properties.regex.test(value))
      ) {
        // If the string doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
      break;

    case 'number':
      // Set values for boolean.
      if(_.isBoolean(value)) {
        value = value ? 1 : 0;
      }

      if(
        // Reject if array.
        _.isArray(value)

        // Reject if object.
        || _.isObject(value)

        // Reject if not numeric.
        || !isNumeric(value)
      ) {
         // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      // Typecast to number.
      value = value * 1;

      // Check against filters before returning value
      if(
        // If min is defined, check to be sure the integer is > min
        (!_.isUndefined(properties.min) && value < properties.min)

        // If max is defined, check to be sure the string is < max
        || (!_.isUndefined(properties.max) && value > properties.max)
      ) {
        // If the string doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
      break;

    case 'boolean':
      // If is String and is 'false', return false.
      if(_.isString(value) && value === 'false') {
        return false;
      }

      // If is Number, <0 is true and >0 is false.
      if(isNumeric(value)) {
        return (value * 1) > 0 ? true : false;
      }

      // Use Javascript to eval and return boolean.
      return value ? true : false;
      break;

    case 'array':
      // If it's an object, typecast to an array and return array.
      if(_.isObject(value)) {
        value = _.toArray(value);
      }

      // Reject if not array
      if(!_.isArray(value)) {
        // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      return value;
      break;

    case 'object':
      // If it's not an Object, reject.
      if(!_.isObject(value)) {
        // If the value doesn't meet requirements, it will remain untouched.
        return originalValue;
      }

      // If object is schema object and an entirely new object was passed, clear values and set.
      // This preserves the object instance.
      if(properties.objectType) {
        // The object will usually exist because it's initialized immediately for deep access within SchemaObjects.
        // However, in the case of Array elements, it will not exist.
        var schemaObject;
        if(!_.isUndefined(originalValue)) {
          // Clear existing values.
          schemaObject = originalValue;
          schemaObject.clear();
        } else {
          // The SchemaObject doesn't exist yet. Let's initialize a new one.
          // This is used for Array types.
          schemaObject = new properties.objectType;
        }

        // Copy value to SchemaObject and set value to SchemaObject.
        _.each(value, function(v, k) {
          schemaObject[k] = v;
        });
        value = schemaObject;
      }

      // Otherwise, it's OK.
      return value;
      break;

    case 'date':
      // Reject if object, array or boolean.
      if(!_.isDate(value) && !_.isString(value) && !_.isNumber(value)) {
        return originalValue;
      }

      // Attempt to parse string value with Date.parse (which returns number of milliseconds).
      if(_.isString(value)) {
        value = Date.parse(value);
      }

      // If is timestamp, convert to Date.
      if(_.isNumber(value)) {
        value = new Date((value + '').length > 10 ? value : value * 1000);
      }

      // If the date couldn't be parsed, do not modify index.
      if(value == 'Invalid Date' || !_.isDate(value)) {
        value = originalValue;
      }

      return value;
      break;

    default:
      return value;
      break;
  }
};

// Properties can be passed in multiple forms (an object, just a type, etc).
// Normalize to a standard format.
var normalizeProperties = function(properties) {
  // Allow for shorthand type declaration:

  // index: Type is translated to index: {type: Type}
  if(properties && _.isUndefined(properties.type)) {
    properties = {type: properties};
  }

  // Type may be an object with properties.
  // If "type.type" exists, we'll assume it's meant to be properties.
  // This means that shorthand objects can't use the "type" index.
  // If "type" is necessary, they must be wrapped in a SchemaObject.
  if(_.isObject(properties.type) && !_.isUndefined(properties.type.type)) {
    _.each(properties.type, function(value, key) {
      if(_.isUndefined(properties[key])) {
        properties[key] = value;
      }
    });
    properties.type = properties.type.type;
  }

  // Null or undefined should be flexible and allow any value.
  if(properties.type === null || properties.type === undefined) {
    properties.type = 'any';

  // Convert object representation of type to lowercase string.
  // String is converted to 'string', Number to 'number', etc.
  } else if(properties.type.name) {
    properties.type = properties.type.name;
  }
  if(_.isString(properties.type)) {
    properties.type = properties.type.toLowerCase();
  }

  // index: [Type] or index: [] is translated to index: {type: Array, arrayType: Type}
  if(_.isArray(properties.type)) {
    if(_.size(properties.type)) {
      // Properties will be normalized when array is initialized.
      properties.arrayType = properties.type[0];
    }
    properties.type = 'array';
  }

  // index: {} or index: SchemaObject is translated to index: {type: Object, objectType: Type}
  // SchemaObject factory is initialized when raw schema is provided.
  if(!_.isString(properties.type)) {
    if(_.isFunction(properties.type)) {
      properties.objectType = properties.type;
      properties.type = 'object';
    } else if(properties.type === {}) {
      properties.type = 'object';
    } else if(_.isObject(properties.type) && _.size(properties.type)) {
      properties.objectType = new SchemaObject(properties.type);
      properties.type = 'object';
    }
  }

  return properties;
};

// Represents a basic array with typecasted values.
var SchemaArray = function(self, properties) {
  this._self = self;
  this._properties = properties;

  if(this._properties.arrayType) {
    this._properties.arrayType = normalizeProperties(this._properties.arrayType);
  }
};
SchemaArray.prototype = new Array;
SchemaArray.prototype.push = function() {
  // Values are passed through the setter before being allowed onto the array if arrayType is set.
  // In the case of rejection, the setter returns undefined, which is not appended to the array.
  var values;
  if(this._properties.arrayType) {
    values = [].map.call(arguments, function(value) {
      return setter.call(this._self, value, undefined, this._properties.arrayType);
    }, this);
  } else {
    values = arguments;
  }

  if(this._properties.unique) {
    values = _.difference(values, _.toArray(this));
  }

  var ret = [].push.apply(this, values);

  return ret;
};
SchemaArray.prototype.toArray = function() {
  // Create new Array to hold elements.
  var array = [];

  // Loop through each element, clone if necessary.
  _.each(this, function(element) {
    // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects).
    if(_.isObject(element) && _.isFunction(element.toObject)) {
      element = element.toObject();

    // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object.
    } else if(_.isObject(element)) {
      element = _.clone(element);
    }

    array.push(element);
  });

  return array;
};

// Represents an object with typed indexes.
var SchemaObject = function(schema) {
  return function(defaults) {
    var self = this;

    // Object used to store properties internally.
    var obj;
    self._obj = obj = {};

    // Schema as defined by constructor.
    self._schema = schema;

    // Define getters/setters based off of schema.
    _.each(schema, function(properties, index) {
      // Normalize properties to allow for various shorthand declarations.
      schema[index] = properties = normalizeProperties(properties);

      // The actual index on the object may be an alias.
      var objIndex = index;
      if(properties.type === 'alias') {
        // Use properties of referenced index.
        // TODO: Allow for alias to be recognized via dot-notation.
        objIndex = properties.index;
        var referencedProperties = normalizeProperties(schema[objIndex]);
        referencedProperties.isAlias = true;

        // Allow alias to use transform() to pre-transform any values passing through it.
        if(properties.transform) {
          referencedProperties.transform = function(value, originalValue, properties) {
            value = properties.transform.call(this, value, originalValue, properties);
            value = referencedProperties.transform.call(this, value, originalValue, properties);
            return value;
          };
        }
      }

      // Use getter / setter to intercept and re-route, transform, etc.
      self.__defineGetter__(index, function() {
        return getter.call(self, obj[objIndex], properties);
      });
      self.__defineSetter__(index, function(value) {
        // Don't proceed if readOnly is true.
        if(properties.readOnly) {
          return;
        }

        // self[index] is used instead of obj[index] to route through the getter
        obj[objIndex] = setter.call(self, value, self[objIndex], properties);
      });

      // In case of object & array, they must be initialized immediately. However, this should not be done for aliases.
      if(properties.isAlias !== true) {
        if(properties.type === 'object') {
          obj[objIndex] = properties.objectType ? new properties.objectType : {};

        // Native arrays are never used so that toArray can be globally supported.
        // Additionally, other properties such as unique rely on passing through SchemaObject.
        } else if(properties.type === 'array') {
          obj[objIndex] = new SchemaArray(self, properties);
        }
      }
    });

    // Return object without getter/setters, extra properties, etc.
    this.toObject = function() {
      var getObj = {};

      // Populate all properties in schema.
      _.each(schema, function(properties, index) {
        // Do not write values to object that are marked as writeOnly
        if(properties.invisible) {
          return;
        }

        // Fetch value from self[index] to route through getter.
        var value = self[index];

        // If value does not need to be cloned, place in index.
        if((value === undefined || value === null)
        || properties.type !== 'object' && properties.type !== 'array' && properties.type !== 'date') {
          getObj[index] = value;

        // Clone Object
        } else if(properties.type === 'object') {
          // Call toObject() method if defined (this allows us to return primitive objects instead of SchemaObjects).
          if(_.isFunction(value.toObject)) {
            getObj[index] = value.toObject();

          // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object.
          } else if(_.isObject(value)) {
            getObj[index] = _.clone(value);
          }

        // Clone Array
        } else if(properties.type === 'array') {
          // Built in method to clone array to native type
          getObj[index] = value.toArray();

        // Clone Date object.
        } else if(properties.type === 'date') {
          // https://github.com/documentcloud/underscore/pull/863
          // _.clone doesn't work on Date object.
          getObj[index] = new Date(value.getTime());
        }
      });

      return getObj;
    };

    // Clear all values.
    this.clear = function() {
      self._obj = {};
    };

    // Populate runtime defaults as provided to this instance of object.
    // (Different than the default for each field - is simply a shortcut to populate values in object.)
    if(_.isObject(defaults)) {
      _.each(defaults, function(value, key) {
        self[key] = value;
      });
    };

    return self;
  }
};

module.exports = SchemaObject;<|endoftext|>


<commit_before>var socketClusterServer = require('socketcluster-server');
var EventEmitter = require('events').EventEmitter;
var crypto = require('crypto');
var uuid = require('uuid');
var http = require('http');
var https = require('https');
var fs = require('fs');
var base64id = require('base64id');
var async = require('async');
var AuthEngine = require('sc-auth').AuthEngine;

var scErrors = require('sc-errors');
var InvalidActionError = scErrors.InvalidActionError;
var ResourceLimitError = scErrors.ResourceLimitError;
var BrokerError = scErrors.BrokerError;
var HTTPServerError = scErrors.HTTPServerError;
var TimeoutError = scErrors.TimeoutError;

var SCWorker = function (options) {
  var self = this;

  this.EVENT_ERROR = 'error';
  this.EVENT_WARNING = 'warning';
  this.EVENT_EXIT = 'exit';
  this.EVENT_READY = 'ready';
  this.EVENT_CONNECTION = 'connection';

  this.MIDDLEWARE_START = 'start';

  this.type = 'worker';
  self._pendingResponseHandlers = {};

  this._init(options);
};

SCWorker.prototype = Object.create(EventEmitter.prototype);

SCWorker.prototype.setAuthEngine = function (authEngine) {
  this.auth = authEngine;

  this.httpServer.auth = this.auth;
  this.scServer.setAuthEngine(this.auth);
};

SCWorker.prototype.setCodecEngine = function (codecEngine) {
  this.codec = codecEngine;
  this.scServer.setCodecEngine(this.codec);
};

SCWorker.prototype._init = function (options) {
  var self = this;

  this.options = {};

  for (var i in options) {
    if (options.hasOwnProperty(i)) {
      this.options[i] = options[i];
    }
  }

  this.id = this.options.id;
  this.isLeader = this.id == 0;

  this._middleware = {};
  this._middleware[this.MIDDLEWARE_START] = [];

  if (this.options.downgradeToUser && process.setuid) {
    try {
      process.setuid(this.options.downgradeToUser);
    } catch (err) {
      throw new InvalidActionError('Could not downgrade to user "' + this.options.downgradeToUser +
        '" - Either this user does not exist or the current process does not have the permission' +
        ' to switch to it');
    }
  }

  this.brokerEngine = require(this.options.brokerEngine);

  this._paths = options.paths;

  this._httpRequestCount = 0;
  this._wsRequestCount = 0;
  this._httpRPM = 0;
  this._wsRPM = 0;

  this.brokerEngineClient = new this.brokerEngine.Client({
    brokers: this.options.brokers,
    secretKey: this.options.secretKey,
    pubSubBatchDuration: this.options.pubSubBatchDuration,
    connectRetryErrorThreshold: this.options.brokerConnectRetryErrorThreshold
  });

  this.brokerEngineClient.on('error', function (err) {
    var error;
    if (typeof err == 'string') {
      error = new BrokerError(err);
    } else {
      error = err;
    }
    self.emitError(error);
  });
  this.brokerEngineClient.on('warning', function (warning) {
    self.emitWarning(warning);
  });
  this.exchange = this.global = this.brokerEngineClient.exchange();

  if (this.options.httpServerModule) {
    var httpServerFactory = require(this.options.httpServerModule);
    this.httpServer = httpServerFactory.createServer(this.options.protocolOptions);
  } else {
    if (this.options.protocol == 'https') {
      this.httpServer = https.createServer(this.options.protocolOptions);
    } else {
      this.httpServer = http.createServer();
    }
  }
  this.httpServer.on('request', this._httpRequestHandler.bind(this));
  this.httpServer.on('upgrade', this._httpRequestHandler.bind(this));

  this.httpServer.exchange = this.httpServer.global = this.exchange;

  this.httpServer.on('error', function (err) {
    var error;
    if (typeof err == 'string') {
      error = new HTTPServerError(err);
    } else {
      error = err;
    }
    self.emitError(error);
  });

  var secure = this.options.protocol == 'https' ? 1 : 0;

  this.scServer = socketClusterServer.attach(this.httpServer, {
    brokerEngine: this.brokerEngineClient,
    wsEngine: this._paths.wsEnginePath,
    allowClientPublish: this.options.allowClientPublish,
    handshakeTimeout: this.options.handshakeTimeout,
    ackTimeout: this.options.ackTimeout,
    pingTimeout: this.options.pingTimeout,
    pingInterval: this.options.pingInterval,
    origins: this.options.origins,
    appName: this.options.appName,
    path: this.options.path,
    authKey: this.options.authKey,
    authPrivateKey: this.options.authPrivateKey,
    authPublicKey: this.options.authPublicKey,
    authAlgorithm: this.options.authAlgorithm,
    authSignAsync: this.options.authSignAsync,
    authVerifyAsync: this.options.authVerifyAsync,
    authDefaultExpiry: this.options.authDefaultExpiry,
    middlewareEmitWarnings: this.options.middlewareEmitWarnings,
    socketChannelLimit: this.options.socketChannelLimit,
    pubSubBatchDuration: this.options.pubSubBatchDuration,
    perMessageDeflate: this.options.perMessageDeflate
  });

  if (this.brokerEngineClient.setSCServer) {
    this.brokerEngineClient.setSCServer(this.scServer);
  }

  // Default authentication engine
  this.setAuthEngine(new AuthEngine());
  this.codec = this.scServer.codec;

  this._socketPath = this.scServer.getPath();
  this._socketPathRegex = new RegExp('^' + this._socketPath);

  this.scServer.on('_connection', function (socket) {
    // The connection event counts as a WS request
    self._wsRequestCount++;
    socket.on('message', function () {
      self._wsRequestCount++;
    });
    self.emit(self.EVENT_CONNECTION, socket);
  });

  this.scServer.on('warning', function (warning) {
    self.emitWarning(warning);
  });
  this.scServer.on('error', function (error) {
    self.emitError(error);
  });
  this.scServer.on('ready', function () {
    self.emit(self.EVENT_READY);
  });
};

SCWorker.prototype.open = function () {
  this._startServer();
};

SCWorker.prototype.close = function (callback) {
  this.scServer.close();
  this.httpServer.close(callback);
};

// getSocketURL is deprecated
SCWorker.prototype.getSocketPath = SCWorker.prototype.getSocketURL = function () {
  return this._socketPath;
};

SCWorker.prototype.addMiddleware = function (type, middleware) {
  this._middleware[type].push(middleware);
};

SCWorker.prototype.removeMiddleware = function (type, middleware) {
  var middlewareFunctions = this._middleware[type];

  this._middleware[type] = middlewareFunctions.filter(function (fn) {
    return fn != middleware;
  });
};

SCWorker.prototype._startServer = function () {
  var self = this;

  var options = this.options;

  var start = function () {
    if (options.tcpSynBacklog != null) {
      self.httpServer.listen(options.sourcePort, options.host, options.tcpSynBacklog);
    } else if (options.host != null) {
      self.httpServer.listen(options.sourcePort, options.host);
    } else {
      self.httpServer.listen(options.sourcePort);
    }
  };

  var startMiddleware = this._middleware[this.MIDDLEWARE_START];
  if (startMiddleware.length) {
    var callbackInvoked = false;

    async.applyEachSeries(startMiddleware, options, function (err) {
      if (callbackInvoked) {
        self.emit('warning', new InvalidActionError('Callback for ' + self.MIDDLEWARE_START + ' middleware was already invoked'));
      } else {
        callbackInvoked = true;
        if (err) {
          throw err;
        } else {
          start();
        }
      }
    });
  } else {
    start();
  }
};

SCWorker.prototype.start = function () {
  var self = this;

  this._httpRequestCount = 0;
  this._wsRequestCount = 0;
  this._httpRPM = 0;
  this._wsRPM = 0;

  if (this._statusInterval != null) {
    clearInterval(this._statusInterval);
  }
  this._statusInterval = setInterval(this._calculateStatus.bind(this), this.options.workerStatusInterval);

  // Run the initController to initialize the global context
  if (this._paths.appInitControllerPath != null) {
    this._initController = require(this._paths.appInitControllerPath);
    this._initController.run(this);
  }

  this._workerController = require(this._paths.appWorkerControllerPath);
  this._workerController.run(this);

  this._startServer();
};

SCWorker.prototype._httpRequestHandler = function (req, res) {
  var self = this;

  this._httpRequestCount++;

  req.exchange = req.global = this.exchange;

  var forwardedFor = req.headers['x-forwarded-for'];

  if (forwardedFor) {
    var forwardedClientIP;
    if (forwardedFor.indexOf(',') > -1) {
      forwardedClientIP = forwardedFor.split(',')[0];
    } else {
      forwardedClientIP = forwardedFor;
    }
    req.forwardedForAddress = forwardedClientIP;
  }
  if (req.connection) {
    req.remoteAddress = req.connection.remoteAddress;
    req.remoteFamily = req.connection.remoteFamily;
    req.remotePort = req.connection.remotePort;
  } else if (req.socket) {
    req.remoteAddress = req.socket.remoteAddress;
    req.remoteFamily = req.socket.remoteFamily;
    req.remotePort = req.socket.remotePort;
  }
};

SCWorker.prototype.getSCServer = function () {
  return this.scServer;
};

SCWorker.prototype.getHTTPServer = function () {
  return this.httpServer;
};

SCWorker.prototype._calculateStatus = function () {
  var perMinuteFactor = 60000 / this.options.workerStatusInterval;
  this._httpRPM = this._httpRequestCount * perMinuteFactor;
  this._wsRPM = this._wsRequestCount * perMinuteFactor;
  this._httpRequestCount = 0;
  this._wsRequestCount = 0;

  var memThreshold = this.options.killWorkerMemoryThreshold;

  if (memThreshold != null) {
    var memoryUsage = process.memoryUsage();
    if (memoryUsage.heapUsed > memThreshold) {
      var message = 'Worker killed itself because its memory ';
      message += 'usage of ' + memoryUsage.heapUsed + ' exceeded ';
      message += 'the killWorkerMemoryThreshold of ' + memThreshold;
      var warning = new ResourceLimitError(message);
      this.emitWarning(warning);
      process.exit();
    }
  }
};

SCWorker.prototype.getStatus = function () {
  return {
    clientCount: this.scServer.clientsCount,
    httpRPM: this._httpRPM,
    wsRPM: this._wsRPM
  };
};

SCWorker.prototype._createIPCResponseHandler = function (callback) {
  var self = this;
  var cid = uuid.v4();

  var responseTimeout = setTimeout(function () {
    var responseHandler = self._pendingResponseHandlers[cid];
    delete self._pendingResponseHandlers[cid];
    var timeoutError = new TimeoutError('IPC response timed out');
    responseHandler.callback(timeoutError);
  }, this.options.ipcAckTimeout);

  this._pendingResponseHandlers[cid] = {
    callback: callback,
    timeout: responseTimeout
  };

  return cid;
};

SCWorker.prototype.handleMasterResponse = function (message) {
  var responseHandler = this._pendingResponseHandlers[message.rid];
  if (responseHandler) {
    clearTimeout(responseHandler.timeout);
    delete this._pendingResponseHandlers[message.rid];
    var properError = scErrors.hydrateError(message.error, true);
    responseHandler.callback(properError, message.data);
  }
};

SCWorker.prototype.sendToMaster = function (data, callback) {
  var messagePacket = {
    type: 'workerMessage',
    data: data,
    workerId: this.id
  };

  if (callback) {
    messagePacket.cid = this._createIPCResponseHandler(callback);
  }
  process.send(messagePacket);
};

SCWorker.prototype.respondToMaster = function (err, data, rid) {
  process.send({
    type: 'workerResponse',
    error: scErrors.dehydrateError(err, true),
    data: data,
    workerId: this.id,
    rid: rid
  });
};

SCWorker.prototype.handleMasterEvent = function () {
  this.emit.apply(this, arguments);
};

SCWorker.prototype.handleMasterMessage = function (message) {
  var self = this

  self.emit('masterMessage', message.data, function (err, data) {
    if (message.cid) {
      self.respondToMaster(err, data, message.cid);
    }
  });
};

SCWorker.prototype.emitError = function (err) {
  this.emit(this.EVENT_ERROR, err);
};

SCWorker.prototype.emitWarning = function (warning) {
  this.emit(this.EVENT_WARNING, warning);
};

module.exports = SCWorker;
<commit_msg>Remove old scworker logic<commit_after><|endoftext|>


<commit_before>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2014, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

/* global chrome */

var _ = require('lodash');
var async = require('async');
var localStore = require('./core/localStore');
var DEVICEPORTS = 'devicePorts';

var debug = require('./bows')('SerialDevice');

var moduleCounter = 1;

module.exports = function(config) {
  config = config || {};
  var connection = null;
  var connectionPort = null;
  var buffer = [];
  var packetBuffer = [];
  var packetHandler = null;
  var moduleNumber = moduleCounter++;
  var portPattern = '/dev/cu.usb.+';
  var bitrate = 9600;
  var log = '';
  var logcount = 0;
  var loglimit = 400;
  var doLogging = (config && config.doLogging) || false;

  if (config.silent) {
    // debug = _.noop;
  }

  function init() {
    connection = null;
    connectionPort = null;
    buffer = [];
    packetBuffer = [];
    packetHandler = null;
    log = '';
    localStore.init(DEVICEPORTS, function() {});
  }

  init();

  // This is the object that is passed to the packetHandler
  // it lets us abstract away the details of the packetHandling
  var bufobj = {
    // get(x) -- returns char at x
    get : function(n) {return buffer[n]; },
    // len() -- returns length
    len : function() { return buffer.length; },
    // discard(n) -- deletes n chars at start of buffer
    discard : function(n) { discardBytes(n); },
    // bytes() -- returns entire buffer as a Uint8Array
    bytes : function() {
      return new Uint8Array(buffer);
    }
  };

  function addlog(s) {
    if (!doLogging) {
      return;
    }

    if (logcount < loglimit) {
      if (s.indexOf('\n') !== -1) {
        ++logcount;
      }
      log += s;
    }
  }

  var logdump = _.debounce(function() {
    if (doLogging) {
      // debug(log);
    }
  }, 1000);

  var emitLog = function(clearLog) {
    debug(log);
    if (clearLog) {
      log = '';
      logcount = 0;
    }
  };

  function portListener(info) {
    if (connection && info.connectionId == connection.connectionId && info.data) {
      var bufView=new Uint8Array(info.data);
      for (var i=0; i<bufView.byteLength; i++) {
        buffer.push(bufView[i]);
      }

      addlog('  rcv ');
      for (i in bufView) {
        addlog(('00' + bufView[i].toString(16)).substr(-2) + ' ');
      }
      addlog('\n');
      logdump();

      // we got some bytes, let's see if they make one or more packets
      if (packetHandler) {
        var pkt = packetHandler(bufobj);
        while (pkt) {
          packetBuffer.push(pkt);
          pkt = packetHandler(bufobj);
        }
      }
    }
  }

    // requires a packethandler
  function connect(deviceInfo, packethandler, probe, cb) {
    if (arguments.length != 4) {
      console.log('serial connect called with wrong number of arguments!');
    }

    // add a listener for any serial traffic
    // do this first so that we don't lose anything (not that it's all that
    // likely, but it doesn't hurt)
    chrome.serial.onReceive.addListener(portListener);
    setPacketHandler(packethandler);
    flush();

    debug('in SerialDevice.connect, info ', deviceInfo);

    // generate a list of all the ports in the system
    chrome.serial.getDevices(function(ports) {
      // filter the list of ports if we can, using the portPattern if we have one
      if (deviceInfo.portPattern) {
        ports = _.filter(ports, function(p) {
          return p.path.match(deviceInfo.portPattern);
        });
      }

      // now sort that list based on index within the list of successful ports
      // we've used for this device
      var storedPorts = localStore.getItem(DEVICEPORTS);
      var allports = {};
      if (!_.isEmpty(storedPorts)) {
        allports = JSON.parse(storedPorts);
      }
      var successfulPorts = [];
      if (allports) {
        successfulPorts = allports[deviceInfo.driverId];
      }
      ports = _.sortBy(ports, function(port) {
        var i = _.indexOf(successfulPorts, port.path);
        if (i < 0) {
          return 1000;
        } else {
          return i;
        }
      });

      // now walk all the matching serial ports and try each one
      // debug(ports);

      async.eachSeries(ports, function(port, done) {
        debug('now trying port ', port);
        chrome.serial.connect(port.path, { bitrate: bitrate, name: port.path, sendTimeout: 100, receiveTimeout: 200 }, function(conn) {
          if (conn && conn.connectionId) {
            connection = conn;
            probe(function(err) {
              if (!err) {
                return done(conn); // we found a port so abort the eachSerial
              } else {
                // it didn't work so disconnect
                chrome.serial.disconnect(conn.connectionId, function() {});  // don't care what the result is
                return done();
              }
            });
          } else {
            done();
          }
        });
      }, function(result) {
        if (result) {
          addlog(' conn\n');
          logdump();
          connection = result;
          connectionPort = connection.name;
          debug('connected to ' + connection.name);
          cb();
        } else {
          connection = null;
          clearPacketHandler();
          cb(new Error('Could not connect to a matching device port ' + deviceInfo));
        }
      });
    });
  }

  function recordPort(deviceId) {
    var ap = localStore.getItem(DEVICEPORTS);
    var allports = ap ? JSON.parse(ap) : {};
    var successfulPorts = [];
    if (allports && allports[deviceId]) {
      successfulPorts = allports[deviceId];
    } else {
      allports = {};
    }
    if (_.indexOf(successfulPorts, connection.name) == -1) {
      successfulPorts.unshift(connection.name);
    }
    allports[deviceId] = successfulPorts;
    var s = JSON.stringify(allports);
    localStore.setItem(DEVICEPORTS, s);
  }

  function disconnect(cb) {
    chrome.serial.onReceive.removeListener(portListener);
    if (connection) {
      chrome.serial.disconnect(connection.connectionId, function(result) {
        addlog(' done\n');
        logdump();
        init();
        if (cb) {
          cb(result);
        }
      });
    } else {
      if (cb) {
        cb(null);
      }
    }
  }

  // allows changing the bit rate of an existing connection -- it disconnects and
  // reconnects
  function changeBitRate(newrate, cb) {
    if (connection) {
      addlog(' change bitrate to ' + newrate + '\n');
      bitrate = newrate;
      chrome.serial.disconnect(connection.connectionId, function(result) {
        debug('reconnecting on ' + connection.name + ' at ' + bitrate);
        setTimeout(function() {
          chrome.serial.connect(connection.name, { bitrate: bitrate }, function(conn) {
            debug('reconnected');
            connection = conn;
            if (cb) {
              cb(true);
            }
          });
        }, 500);
      });
    } else {
      if (cb) {
        cb(false);
      }
    }
  }

  function discardBytes(discardCount) {
    addlog('discard ' + discardCount + '\n');
    logdump();
    buffer = buffer.slice(discardCount);
  }

  function readSerial(bytes, timeout, callback) {
    var packet;
    if (buffer.length >= bytes) {
      packet = buffer.slice(0,bytes);
      buffer = buffer.slice(0 - bytes);
      callback(packet);
    } else if (timeout === 0) {
      packet = buffer;
      buffer = [];
      callback(packet);
    } else {
      setTimeout(function() {
        readSerial(bytes, 0, callback);
      }, timeout);
    }
  }

  function writeSerial(bytes, callback) {
    var bufView = new Uint8Array(bytes);
    var l = bufView.length;
    var timerId = setTimeout(function() {
      sendcheck({ bytesSent: 0, error: 'timeout' });
    }, 500);
    var sendcheck = function(info) {
      clearTimeout(timerId);
      // debug('Sent ', info.bytesSent,' bytes');
      addlog(' xmit ');
      for (var i in bufView) {
        addlog(('00' + bufView[i].toString(16)).substr(-2) + ' ');
      }
      addlog('\n');
      logdump();

      if (l != info.bytesSent) {
        debug('Only ' + info.bytesSent + ' bytes sent out of ' + l);
      }
      else if (info.error) {
        debug('Serial send returned ' + info.error);
      }
      callback(info);
    };
    if (bytes == null) {
      debug('just tried to send nothing!');
    } else {
      chrome.serial.send(connection.connectionId, bytes, sendcheck);
    }
  }

  // a handler should be a function that takes a parameter of a buffer
  // and tries to extract a packet from it; if it finds one, it should delete
  // the characters that make up the packet from the buffer, and return the
  // packet.
  function setPacketHandler(handler) {
    packetHandler = handler;
  }

  function clearPacketHandler() {
    packetHandler = null;
  }

  function hasAvailablePacket() {
    return packetBuffer.length > 0;
  }

  function peekPacket() {
    if (hasAvailablePacket()) {
      return packetBuffer[0];
    } else {
      return null;
    }
  }

  function nextPacket() {
    if (hasAvailablePacket()) {
      return packetBuffer.shift();
    } else {
      return null;
    }
  }

  function flush() {
    addlog('flush\n');
    logdump();
    packetBuffer = [];
  }

  function setPattern(p) {
    debug('Module #' + moduleNumber + ' setting portPattern to ' + p);
    portPattern = p;
  }

  function setBitrate(br) {
    bitrate = br;
  }

  return {
    setPattern: setPattern,
    setBitrate: setBitrate,
    connect: connect,
    recordPort: recordPort,
    disconnect: disconnect,
    changeBitRate: changeBitRate,
    discardBytes: discardBytes,
    readSerial: readSerial,
    writeSerial: writeSerial,
    setPacketHandler: setPacketHandler,
    clearPacketHandler: clearPacketHandler,
    hasAvailablePacket: hasAvailablePacket,
    peekPacket: peekPacket,
    nextPacket: nextPacket,
    flush: flush,
    emitLog: emitLog
  };

};
<commit_msg>Add error handling for serial comms<commit_after>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2014, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

/* global chrome */

var _ = require('lodash');
var async = require('async');
var localStore = require('./core/localStore');
var DEVICEPORTS = 'devicePorts';

var debug = require('./bows')('SerialDevice');

var moduleCounter = 1;

module.exports = function(config) {
  config = config || {};
  var connection = null;
  var connectionPort = null;
  var buffer = [];
  var packetBuffer = [];
  var packetHandler = null;
  var errorHandler = null;
  var moduleNumber = moduleCounter++;
  var portPattern = '/dev/cu.usb.+';
  var bitrate = config.bitrate || 9600;
  var log = '';
  var logcount = 0;
  var loglimit = 400;
  var doLogging = (config && config.doLogging) || false;

  if (config.silent) {
    // debug = _.noop;
  }

  function init() {
    connection = null;
    connectionPort = null;
    buffer = [];
    packetBuffer = [];
    packetHandler = null;
    log = '';
    localStore.init(DEVICEPORTS, function() {});
  }

  init();

  // This is the object that is passed to the packetHandler
  // it lets us abstract away the details of the packetHandling
  var bufobj = {
    // get(x) -- returns char at x
    get : function(n) {return buffer[n]; },
    // len() -- returns length
    len : function() { return buffer.length; },
    // discard(n) -- deletes n chars at start of buffer
    discard : function(n) { discardBytes(n); },
    // bytes() -- returns entire buffer as a Uint8Array
    bytes : function() {
      return new Uint8Array(buffer);
    }
  };

  function addlog(s) {
    if (!doLogging) {
      return;
    }

    if (logcount < loglimit) {
      if (s.indexOf('\n') !== -1) {
        ++logcount;
      }
      log += s;
    }
  }

  var logdump = _.debounce(function() {
    if (doLogging) {
      // debug(log);
    }
  }, 1000);

  var emitLog = function(clearLog) {
    debug(log);
    if (clearLog) {
      log = '';
      logcount = 0;
    }
  };

  function portListener(info) {
    if (connection && info.connectionId == connection.connectionId && info.data) {
      var bufView=new Uint8Array(info.data);
      for (var i=0; i<bufView.byteLength; i++) {
        buffer.push(bufView[i]);
      }

      addlog('  rcv ');
      for (i in bufView) {
        addlog(('00' + bufView[i].toString(16)).substr(-2) + ' ');
      }
      addlog('\n');
      logdump();

      // we got some bytes, let's see if they make one or more packets
      if (packetHandler) {
        var pkt = packetHandler(bufobj);
        while (pkt) {
          packetBuffer.push(pkt);
          pkt = packetHandler(bufobj);
        }
      }
    }
  }

  // if an errorHandler was specified, then we call it with every error.
  // If it returns anything, it should return either a string or an array
  // containing the bytes it wants to insert into the buffer.
  // According to chrome's docs, info will contain connectionId, and an
  // error member that can be any of "disconnected", "timeout", "device_lost", or "system_error"
  function errorListener(info) {
    if (info.error && errorHandler) {
      var bytes = errorHandler(info);
      for (var i=0; i<bytes.length; i++) {
        if (_.isString(bytes)) {
          buffer.push(bytes.charCodeAt(i));
        } else if (_.isArray(bytes)) {
          buffer.push(bytes[i]);
        }
      }
    }
  }

  function connect(deviceInfo, handler, probe, cb) {
    if (arguments.length != 4) {
      console.log('serial connect called with wrong number of arguments!');
    }

    // If the driver specifies a bitrate override, we have to use it
    if (!!deviceInfo.bitrate) {
      bitrate = deviceInfo.bitrate;
    }

    // add a listener for any serial traffic
    // do this first so that we don't lose anything (not that it's all that
    // likely, but it doesn't hurt)
    chrome.serial.onReceive.addListener(portListener);
    if (_.isFunction(handler)) {
      setPacketHandler(handler);
    } else if (_.isObject(handler)) {
      setPacketHandler(handler.packethandler);
      if (handler.errorhandler) {
        setErrorHandler(handler.errorhandler);
        chrome.serial.onReceiveError.addListener(errorListener);
      }
    }
    flush();

    debug('in SerialDevice.connect, info ', deviceInfo);

    // generate a list of all the ports in the system
    chrome.serial.getDevices(function(ports) {
      // filter the list of ports if we can, using the portPattern if we have one
      if (deviceInfo.portPattern) {
        ports = _.filter(ports, function(p) {
          return p.path.match(deviceInfo.portPattern);
        });
      }

      // now sort that list based on index within the list of successful ports
      // we've used for this device
      var storedPorts = localStore.getItem(DEVICEPORTS);
      var allports = {};
      if (!_.isEmpty(storedPorts)) {
        allports = JSON.parse(storedPorts);
      }
      var successfulPorts = [];
      if (allports) {
        successfulPorts = allports[deviceInfo.driverId];
      }
      ports = _.sortBy(ports, function(port) {
        var i = _.indexOf(successfulPorts, port.path);
        if (i < 0) {
          return 1000;
        } else {
          return i;
        }
      });

      // now walk all the matching serial ports and try each one
      // debug(ports);

      async.eachSeries(ports, function(port, done) {
        debug('now trying port ', port);
        var connectopts = {
          bitrate: bitrate,
          name: port.path,
          sendTimeout: deviceInfo.sendTimeout || 100,
          receiveTimeout: deviceInfo.receiveTimeout || 500
        };

        chrome.serial.connect(port.path, connectopts, function(conn) {
          if (conn && conn.connectionId) {
            connection = conn;
            probe(function(err) {
              if (!err) {
                return done(conn); // we found a port so abort the eachSerial
              } else {
                // it didn't work so disconnect
                chrome.serial.disconnect(conn.connectionId, function() {});  // don't care what the result is
                return done();
              }
            });
          } else {
            done();
          }
        });
      }, function(result) {
        if (result) {
          addlog(' conn\n');
          logdump();
          connection = result;
          connectionPort = connection.name;
          debug('connected to ' + connection.name);
          cb();
        } else {
          connection = null;
          clearPacketHandler();
          cb(new Error('Could not connect to a matching device port ' + deviceInfo));
        }
      });
    });
  }

  function recordPort(deviceId) {
    var ap = localStore.getItem(DEVICEPORTS);
    var allports = ap ? JSON.parse(ap) : {};
    var successfulPorts = [];
    if (allports && allports[deviceId]) {
      successfulPorts = allports[deviceId];
    } else {
      allports = {};
    }
    if (_.indexOf(successfulPorts, connection.name) == -1) {
      successfulPorts.unshift(connection.name);
    }
    allports[deviceId] = successfulPorts;
    var s = JSON.stringify(allports);
    localStore.setItem(DEVICEPORTS, s);
  }

  function disconnect(cb) {
    chrome.serial.onReceive.removeListener(portListener);
    if (connection) {
      chrome.serial.disconnect(connection.connectionId, function(result) {
        addlog(' done\n');
        logdump();
        init();
        if (cb) {
          cb(result);
        }
      });
    } else {
      if (cb) {
        cb(null);
      }
    }
  }

  // allows changing the bit rate of an existing connection -- it disconnects and
  // reconnects
  function changeBitRate(newrate, cb) {
    if (connection) {
      addlog(' change bitrate to ' + newrate + '\n');
      bitrate = newrate;
      chrome.serial.disconnect(connection.connectionId, function(result) {
        debug('reconnecting on ' + connection.name + ' at ' + bitrate);
        setTimeout(function() {
          chrome.serial.connect(connection.name, { bitrate: bitrate }, function(conn) {
            debug('reconnected');
            connection = conn;
            if (cb) {
              cb(true);
            }
          });
        }, 500);
      });
    } else {
      if (cb) {
        cb(false);
      }
    }
  }

  function discardBytes(discardCount) {
    addlog('discard ' + discardCount + '\n');
    logdump();
    buffer = buffer.slice(discardCount);
  }

  function readSerial(bytes, timeout, callback) {
    var packet;
    if (buffer.length >= bytes) {
      packet = buffer.slice(0,bytes);
      buffer = buffer.slice(0 - bytes);
      callback(packet);
    } else if (timeout === 0) {
      packet = buffer;
      buffer = [];
      callback(packet);
    } else {
      setTimeout(function() {
        readSerial(bytes, 0, callback);
      }, timeout);
    }
  }

  function writeSerial(bytes, callback) {
    var bufView = new Uint8Array(bytes);
    var l = bufView.length;
    var timerId = setTimeout(function() {
      sendcheck({ bytesSent: 0, error: 'timeout' });
    }, 500);
    var sendcheck = function(info) {
      clearTimeout(timerId);
      // debug('Sent ', info.bytesSent,' bytes');
      addlog(' xmit ');
      for (var i in bufView) {
        addlog(('00' + bufView[i].toString(16)).substr(-2) + ' ');
      }
      addlog('\n');
      logdump();

      if (l != info.bytesSent) {
        debug('Only ' + info.bytesSent + ' bytes sent out of ' + l);
      }
      else if (info.error) {
        debug('Serial send returned ' + info.error);
      }
      callback(info);
    };
    if (bytes == null) {
      debug('just tried to send nothing!');
    } else {
      chrome.serial.send(connection.connectionId, bytes, sendcheck);
    }
  }

  // a handler should be a function that takes a parameter of a buffer
  // and tries to extract a packet from it; if it finds one, it should delete
  // the characters that make up the packet from the buffer, and return the
  // packet.
  function setPacketHandler(handler) {
    packetHandler = handler;
  }

  function clearPacketHandler() {
    packetHandler = null;
  }

  function setErrorHandler(handler) {
    errorHandler = handler;
  }

  function clearErrorHandler() {
    errorHandler = null;
  }

  function hasAvailablePacket() {
    return packetBuffer.length > 0;
  }

  function peekPacket() {
    if (hasAvailablePacket()) {
      return packetBuffer[0];
    } else {
      return null;
    }
  }

  function nextPacket() {
    if (hasAvailablePacket()) {
      return packetBuffer.shift();
    } else {
      return null;
    }
  }

  function flush() {
    addlog('flush\n');
    logdump();
    packetBuffer = [];
  }

  function setPattern(p) {
    debug('Module #' + moduleNumber + ' setting portPattern to ' + p);
    portPattern = p;
  }

  function setBitrate(br) {
    bitrate = br;
  }

  return {
    setPattern: setPattern,
    setBitrate: setBitrate,
    connect: connect,
    recordPort: recordPort,
    disconnect: disconnect,
    changeBitRate: changeBitRate,
    discardBytes: discardBytes,
    readSerial: readSerial,
    writeSerial: writeSerial,
    setPacketHandler: setPacketHandler,
    clearPacketHandler: clearPacketHandler,
    setErrorHandler: setErrorHandler,
    clearErrorHandler: clearErrorHandler,
    hasAvailablePacket: hasAvailablePacket,
    peekPacket: peekPacket,
    nextPacket: nextPacket,
    flush: flush,
    emitLog: emitLog
  };

};
<|endoftext|>