xenia/debugger/src/session.js

490 lines
13 KiB
JavaScript

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2013 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
'use strict';
var module = angular.module('xe.session', []);
module.service('Session', function(
$rootScope, $q, $http, $state, log,
Breakpoint, FileDataSource, RemoteDataSource) {
var State = function(session) {
this.session = session;
this.clear();
};
State.prototype.clear = function() {
this.cache_ = {
moduleList: [],
modules: {},
moduleFunctionLists: {},
functions: {},
threadStates: {},
threadList: []
};
};
State.prototype.sync = function() {
var cache = this.cache_;
var dataSource = this.session.dataSource;
if (!dataSource) {
var d = $q.defer();
d.resolve();
return d.promise;
}
var ps = [];
// Update all modules/functions.
var modulesUpdated = $q.defer();
ps.push(modulesUpdated.promise);
dataSource.getModuleList().then((function(list) {
cache.moduleList = list;
// Update module information.
var moduleFetches = [];
list.forEach(function(module) {
if (cache.modules[module.name]) {
return;
}
var moduleFetch = $q.defer();
moduleFetches.push(moduleFetch.promise);
dataSource.getModule(module.name).
then(function(moduleInfo) {
cache.modules[module.name] = moduleInfo;
moduleFetch.resolve();
}, function(e) {
moduleFetch.reject(e);
});
});
// Update function lists for each module.
list.forEach(function(module) {
var cached = cache.moduleFunctionLists[module.name];
var functionListFetch = $q.defer();
moduleFetches.push(functionListFetch);
dataSource.getFunctionList(module.name, cached ? cached.version : 0).
then(function(result) {
if (cached) {
cached.version = result.version;
for (var n = 0; n < result.list.length; n++) {
cached.list.push(result.list[n]);
}
} else {
cached = cache.moduleFunctionLists[module.name] = {
version: result.version,
list: result.list
};
}
functionListFetch.resolve();
}, function(e) {
functionListFetch.reject(e);
});
});
$q.all(moduleFetches).then(function() {
modulesUpdated.resolve();
}, function(e) {
modulesUpdated.reject();
});
}).bind(this), function(e) {
modulesUpdated.reject(e);
});
// Update threads/thread states.
var threadsUpdated = $q.defer();
ps.push(threadsUpdated.promise);
dataSource.getThreadStates().then((function(states) {
cache.threadStates = states;
cache.threadList = [];
for (var threadId in states) {
cache.threadList.push(states[threadId]);
}
threadsUpdated.resolve();
}).bind(this), function(e) {
threadsUpdated.reject(e);
});
var d = $q.defer();
$q.all(ps).then((function() {
d.resolve();
}).bind(this), (function(e) {
d.reject(e);
}).bind(this));
return d.promise;
};
State.prototype.getModuleList = function() {
return this.cache_.moduleList;
};
State.prototype.getModule = function(moduleName) {
return this.cache_.modules[moduleName] || null;
};
State.prototype.getFunctionList = function(moduleName) {
var cached = this.cache_.moduleFunctionLists[moduleName];
return cached ? cached.list : [];
};
State.prototype.getFunction = function(address) {
return this.cache_.functions[address] || null;
};
State.prototype.fetchFunction = function(address) {
var cache = this.cache_;
var d = $q.defer();
var cached = cache.functions[address];
if (cached) {
d.resolve(cached);
return d.promise;
}
var dataSource = this.session.dataSource;
if (!dataSource) {
d.reject(new Error('Not online.'));
return d.promise;
}
dataSource.getFunction(address).then(function(result) {
cache.functions[address] = result;
d.resolve(result);
}, function(e) {
d.reject(e);
});
return d.promise;
}
Object.defineProperty(State.prototype, 'threadList', {
get: function() {
return this.cache_.threadList || [];
}
});
State.prototype.getThreadStates = function() {
return this.cache_.threadStates || {};
};
State.prototype.getThreadState = function(threadId) {
return this.cache_.threadStates[threadId] || null;
};
var Session = function(id, opt_dataSource) {
this.id = id;
this.breakpoints = {};
this.breakpointsById = {};
this.dataSource = opt_dataSource || null;
this.state = new State(this);
this.activeThread = null;
this.paused = false;
this.loadState();
};
Session.prototype.dispose = function() {
this.saveState();
this.disconnect();
};
Session.prototype.loadState = function() {
var raw = window.localStorage[this.id];
if (!raw) {
return;
}
var json = JSON.parse(raw);
if (!json) {
return;
}
var breakpointList = json.breakpoints;
this.breakpoints = {};
for (var n = 0; n < breakpointList.length; n++) {
var breakpointJson = breakpointList[n];
var breakpoint = Breakpoint.fromJSON(breakpointJson);
this.breakpoints[breakpointJson.address] = breakpoint;
this.breakpointsById[breakpoint.id] = breakpoint;
}
};
Session.prototype.saveState = function() {
var json = {
id: this.id,
breakpoints: []
};
for (var key in this.breakpointsById) {
var breakpoint = this.breakpointsById[key];
if (breakpoint.type != Breakpoint.TEMP) {
json.breakpoints.push(breakpoint.toJSON());
}
}
window.localStorage[this.id] = JSON.stringify(json);
};
Session.DEFAULT_HOST = '127.0.0.1:6200';
Session.getHost = function(opt_host) {
return opt_host || Session.DEFAULT_HOST;
};
Session.query = function(opt_host) {
var url = 'http://' + Session.getHost(opt_host);
var p = $http({
method: 'GET',
url: url + '/sessions',
cache: false,
timeout: 500,
responseType: 'json'
});
var d = $q.defer();
p.then(function(response) {
if (!response.data || !response.data.length) {
d.reject(new Error('No session data'));
return;
}
d.resolve(response.data);
}, function(e) {
d.reject(e);
});
return d.promise;
};
Session.prototype.connect = function(opt_host) {
this.disconnect();
var url = 'ws://' + Session.getHost(opt_host);
log.info('Connecting to ' + url + '...');
log.setProgress(0);
var d = $q.defer();
var dataSource = new RemoteDataSource(url, this);
var p = dataSource.open();
p.then((function() {
log.info('Connected!');
log.clearProgress();
this.setDataSource(dataSource).then((function() {
d.resolve(this);
}).bind(this), (function(e) {
d.reject(e);
}).bind(this));
}).bind(this), (function(e) {
log.error('Unable to connect: ' + e);
log.clearProgress();
d.reject(e);
}).bind(this), function(update) {
log.setProgress(update.progress);
d.notify(update);
});
return d.promise;
};
Session.prototype.disconnect = function() {
this.setDataSource(null);
};
Session.prototype.setDataSource = function(dataSource) {
var self = this;
var d = $q.defer();
if (this.dataSource) {
this.dataSource.dispose();
this.dataSource = null;
}
$rootScope.$emit('refresh');
if (!dataSource) {
d.resolve();
return d.promise;
}
this.state.clear();
this.activeThread = null;
this.dataSource = dataSource;
this.dataSource.on('online', function() {
//
}, this);
this.dataSource.on('offline', function() {
this.setDataSource(null);
}, this);
var ps = [];
// Add breakpoints.
var breakpointList = [];
for (var key in this.breakpoints) {
var breakpoint = this.breakpoints[key];
if (breakpoint.enabled) {
breakpointList.push(breakpoint);
}
}
ps.push(this.dataSource.addBreakpoints(breakpointList));
// Perform a full sync.
var syncDeferred = $q.defer();
ps.push(syncDeferred.promise);
this.state.sync().then((function() {
// Put a breakpoint at the entry point.
// TODO(benvanik): make an option?
var moduleList = this.state.getModuleList();
if (!moduleList.length) {
log.error('No modules found!');
syncDeferred.reject(new Error('No modules found.'));
return;
}
var moduleInfo = this.state.getModule(moduleList[0].name);
if (!moduleInfo) {
log.error('Main module not found!');
syncDeferred.reject(new Error('Main module not found.'));
return;
}
var entryPoint = moduleInfo.exeEntryPoint;
self.addTempBreakpoint(entryPoint, entryPoint);
syncDeferred.resolve();
}).bind(this), (function(e) {
syncDeferred.reject(e);
}).bind(this));
$q.all(ps).then((function() {
this.dataSource.makeReady().then(function() {
d.resolve();
}, function(e) {
log.error('Error making target ready: ' + e);
d.reject(e);
});
}).bind(this), (function(e) {
log.error('Errors preparing target: ' + e);
this.disconnect();
d.reject(e);
}).bind(this));
return d.promise;
};
Session.prototype.addBreakpoint = function(breakpoint) {
this.breakpoints[breakpoint.address] = breakpoint;
this.breakpointsById[breakpoint.id] = breakpoint;
if (this.dataSource) {
this.dataSource.addBreakpoint(breakpoint);
}
this.saveState();
return breakpoint;
};
Session.prototype.addTempBreakpoint = function(fnAddress, address) {
var breakpoint = new Breakpoint();
breakpoint.type = Breakpoint.Type.TEMP;
breakpoint.fnAddress = fnAddress;
breakpoint.address = address;
breakpoint.enabled = true;
return this.addBreakpoint(breakpoint);
};
Session.prototype.addCodeBreakpoint = function(fnAddress, address) {
var breakpoint = new Breakpoint();
breakpoint.type = Breakpoint.Type.CODE;
breakpoint.fnAddress = fnAddress;
breakpoint.address = address;
breakpoint.enabled = true;
return this.addBreakpoint(breakpoint);
};
Session.prototype.removeBreakpoint = function(breakpoint) {
delete this.breakpoints[breakpoint.address];
delete this.breakpointsById[breakpoint.id];
if (this.dataSource) {
this.dataSource.removeBreakpoint(breakpoint.id);
}
this.saveState();
};
Session.prototype.toggleBreakpoint = function(breakpoint, enabled) {
var oldEnabled = enabled;
breakpoint.enabled = enabled;
if (this.dataSource) {
if (breakpoint.enabled) {
this.dataSource.addBreakpoint(breakpoint);
} else {
this.dataSource.removeBreakpoint(breakpoint.id);
}
}
this.saveState();
};
Session.prototype.onBreakpointHit = function(breakpointId, threadId) {
// Now paused!
this.paused = true;
this.state.sync().then((function() {
// Switch active thread.
var thread = this.state.getThreadState(threadId);
this.activeThread = thread;
if (!breakpointId) {
// Just a general pause.
log.info('Execution paused.');
return;
}
var breakpoint = this.breakpointsById[breakpointId];
if (!breakpoint) {
log.error('Breakpoint hit but not found.');
return;
}
// TODO(benvanik): stash current breakpoint/thread/etc.
log.info('Breakpoint hit at 0x' +
breakpoint.address.toString(16).toUpperCase() + '.');
$state.go('session.code.function', {
sessionId: this.id,
function: breakpoint.fnAddress.toString(16).toUpperCase(),
a: breakpoint.address.toString(16).toUpperCase()
}, {
notify: true,
reloadOnSearch: false
});
}).bind(this), (function(e) {
log.error('Unable to synchronize state,');
}).bind(this));
};
Session.prototype.continueExecution = function() {
if (!this.dataSource) {
return;
}
this.paused = false;
this.dataSource.continueExecution().then(function() {
log.info('Execution resumed.');
}, function(e) {
log.error('Unable to continue: ' + e);
});
};
Session.prototype.breakExecution = function() {
if (!this.dataSource) {
return;
}
this.paused = true;
this.dataSource.breakExecution().then(function() {
log.info('Execution paused.');
$rootScope.$emit('refresh');
}, function(e) {
log.error('Unable to break: ' + e);
});
};
Session.prototype.stepNext = function(threadId) {
if (!this.dataSource) {
return;
}
this.paused = false;
this.dataSource.stepNext(threadId).then(function() {
}, function(e) {
log.error('Unable to step: ' + e);
});
};
return Session;
});