Messaging over debug channel.
This commit is contained in:
parent
a9378eb7eb
commit
80d8dc02aa
|
@ -19,3 +19,6 @@
|
||||||
[submodule "third_party/wslay"]
|
[submodule "third_party/wslay"]
|
||||||
path = third_party/wslay
|
path = third_party/wslay
|
||||||
url = https://github.com/benvanik/wslay.git
|
url = https://github.com/benvanik/wslay.git
|
||||||
|
[submodule "third_party/jansson"]
|
||||||
|
path = third_party/jansson
|
||||||
|
url = https://github.com/akheron/jansson.git
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="debugger-main">
|
<div class="debugger-main" ng-controller="CodeTabController">
|
||||||
<div class="debugger-header">
|
<div class="debugger-header">
|
||||||
debug header/toolbar/etc
|
debug header/toolbar/etc
|
||||||
<div ui-view></div>
|
<div ui-view></div>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
******************************************************************************
|
||||||
|
* 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.ui.code', [
|
||||||
|
'xe.log',
|
||||||
|
'xe.session'
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
module.controller('CodeTabController', function(
|
||||||
|
$rootScope, $scope, app, log) {
|
||||||
|
|
||||||
|
$rootScope.$on('refresh', function() {
|
||||||
|
var dataSource = app.session.dataSource;
|
||||||
|
|
||||||
|
dataSource.getModuleList().then(function(list) {
|
||||||
|
console.log(list);
|
||||||
|
}, function(e) {
|
||||||
|
log('Unable to fetch module list');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('refresh');
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,12 +13,15 @@
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<div class="btn-group navbar-btn">
|
<div class="btn-group navbar-btn">
|
||||||
<button type="button" class="btn btn-default" ng-click="connectClicked()" ng-disabled="app.loading">
|
<button type="button" class="btn btn-default" ng-click="connect()" ng-disabled="app.loading">
|
||||||
<span class="glyphicon glyphicon-link"></span> Connect
|
<span class="glyphicon glyphicon-link"></span> Connect
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-default" ng-click="openClicked()" ng-disabled="app.loading">
|
<button type="button" class="btn btn-default" ng-click="open()" ng-disabled="app.loading">
|
||||||
<span class="glyphicon glyphicon-file"></span> Open
|
<span class="glyphicon glyphicon-file"></span> Open
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-default" ng-click="refresh()" ng-disabled="app.loading">
|
||||||
|
<span class="glyphicon glyphicon-refresh"></span> Refresh
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="app.loading">
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="app.loading">
|
||||||
<span class="glyphicon glyphicon-cog"></span> <span class="caret"></span>
|
<span class="glyphicon glyphicon-cog"></span> <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -13,9 +13,13 @@ var module = angular.module('xe.ui.navbar', []);
|
||||||
|
|
||||||
|
|
||||||
module.controller('NavbarController', function(
|
module.controller('NavbarController', function(
|
||||||
$scope, $state, app, log) {
|
$rootScope, $scope, $state, app, log) {
|
||||||
|
|
||||||
$scope.connectClicked = function() {
|
$scope.refresh = function() {
|
||||||
|
$rootScope.$emit('refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.connect = function() {
|
||||||
// TODO(benvanik): show a fancy dialog or something.
|
// TODO(benvanik): show a fancy dialog or something.
|
||||||
var oldSession = app.session;
|
var oldSession = app.session;
|
||||||
app.connect().then(function(session) {
|
app.connect().then(function(session) {
|
||||||
|
@ -34,7 +38,7 @@ module.controller('NavbarController', function(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.openClicked = function() {
|
$scope.open = function() {
|
||||||
var inputEl = document.createElement('input');
|
var inputEl = document.createElement('input');
|
||||||
inputEl.type = 'file';
|
inputEl.type = 'file';
|
||||||
inputEl.accept = '.xe-trace,application/x-extension-xe-trace';
|
inputEl.accept = '.xe-trace,application/x-extension-xe-trace';
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<script src="src/session.js"></script>
|
<script src="src/session.js"></script>
|
||||||
<script src="assets/ui/navbar.js"></script>
|
<script src="assets/ui/navbar.js"></script>
|
||||||
<script src="assets/ui/console/console.js"></script>
|
<script src="assets/ui/console/console.js"></script>
|
||||||
|
<script src="assets/ui/code/code-tab.js"></script>
|
||||||
|
|
||||||
<script src="debugger.js"></script>
|
<script src="debugger.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -17,6 +17,7 @@ var module = angular.module('app', [
|
||||||
'xe.log',
|
'xe.log',
|
||||||
'xe.router',
|
'xe.router',
|
||||||
'xe.session',
|
'xe.session',
|
||||||
|
'xe.ui.code',
|
||||||
'xe.ui.console',
|
'xe.ui.console',
|
||||||
'xe.ui.navbar'
|
'xe.ui.navbar'
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -12,20 +12,88 @@
|
||||||
var module = angular.module('xe.datasources', []);
|
var module = angular.module('xe.datasources', []);
|
||||||
|
|
||||||
|
|
||||||
var DataSource = function(source) {
|
module.service('DataSource', function($q) {
|
||||||
|
var DataSource = function(source) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.online = false;
|
this.online = false;
|
||||||
this.status = 'disconnected';
|
this.status = 'disconnected';
|
||||||
};
|
};
|
||||||
DataSource.prototype.open = function() {};
|
DataSource.prototype.open = function() {};
|
||||||
DataSource.prototype.dispose = function() {};
|
DataSource.prototype.dispose = function() {};
|
||||||
|
DataSource.prototype.issue = function(command) {};
|
||||||
|
|
||||||
|
DataSource.prototype.addBreakpoint = function(breakpoint) {
|
||||||
|
return this.addBreakpoints([breakpoint]);
|
||||||
|
};
|
||||||
|
|
||||||
module.service('RemoteDataSource', function($q) {
|
DataSource.prototype.addBreakpoints = function(breakpoints) {
|
||||||
|
if (!breakpoints.length) {
|
||||||
|
var d = $q.defer();
|
||||||
|
d.resolve();
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.add_breakpoints',
|
||||||
|
breakpoints: breakpoints.map(function(breakpoint) {
|
||||||
|
return breakpoint.toJSON();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.removeBreakpoint = function(breakpointId) {
|
||||||
|
return this.removeBreakpoints([breakpointId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.removeBreakpoints = function(breakpointIds) {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.remove_breakpoints',
|
||||||
|
breakpointIds: breakpointIds
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.removeAllBreakpoints = function() {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.remove_all_breakpoints'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.getModuleList = function() {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.get_module_list'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.getModule = function(moduleId) {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.get_module',
|
||||||
|
moduleId: moduleId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.getFunctionList = function(moduleId) {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.get_function_list',
|
||||||
|
moduleId: moduleId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.getFunction = function(address) {
|
||||||
|
return this.issue({
|
||||||
|
command: 'cpu.get_function',
|
||||||
|
address: address
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return DataSource;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.service('RemoteDataSource', function($q, DataSource) {
|
||||||
var RemoteDataSource = function(url) {
|
var RemoteDataSource = function(url) {
|
||||||
DataSource.call(this, url);
|
DataSource.call(this, url);
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
|
this.nextRequestId_ = 1;
|
||||||
|
this.pendingRequests_ = {};
|
||||||
};
|
};
|
||||||
inherits(RemoteDataSource, DataSource);
|
inherits(RemoteDataSource, DataSource);
|
||||||
|
|
||||||
|
@ -62,12 +130,28 @@ module.service('RemoteDataSource', function($q) {
|
||||||
|
|
||||||
this.socket.onmessage = (function(e) {
|
this.socket.onmessage = (function(e) {
|
||||||
console.log('message', e.data);
|
console.log('message', e.data);
|
||||||
|
var json = JSON.parse(e.data);
|
||||||
|
if (json.requestId) {
|
||||||
|
// Response to a previous request.
|
||||||
|
var request = this.pendingRequests_[json.requestId];
|
||||||
|
if (request) {
|
||||||
|
delete this.pendingRequests_[json.requestId];
|
||||||
|
if (json.status) {
|
||||||
|
request.deferred.resolve(json.result);
|
||||||
|
} else {
|
||||||
|
request.deferred.reject(json.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Notification.
|
||||||
|
}
|
||||||
}).bind(this);
|
}).bind(this);
|
||||||
|
|
||||||
return d.promise;
|
return d.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteDataSource.prototype.dispose = function() {
|
RemoteDataSource.prototype.dispose = function() {
|
||||||
|
this.pendingRequests_ = {};
|
||||||
this.online = false;
|
this.online = false;
|
||||||
this.status = 'disconnected';
|
this.status = 'disconnected';
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
|
@ -77,11 +161,20 @@ module.service('RemoteDataSource', function($q) {
|
||||||
DataSource.prototype.dispose.call(this);
|
DataSource.prototype.dispose.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RemoteDataSource.prototype.issue = function(command) {
|
||||||
|
var d = $q.defer();
|
||||||
|
command.requestId = this.nextRequestId_++;
|
||||||
|
this.socket.send(JSON.stringify(command));
|
||||||
|
command.deferred = d;
|
||||||
|
this.pendingRequests_[command.requestId] = command;
|
||||||
|
return d.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return RemoteDataSource;
|
return RemoteDataSource;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
module.service('FileDataSource', function($q) {
|
module.service('FileDataSource', function($q, DataSource) {
|
||||||
var FileDataSource = function(file) {
|
var FileDataSource = function(file) {
|
||||||
DataSource.call(this, file.name);
|
DataSource.call(this, file.name);
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <third_party/jansson/src/jansson.h>
|
||||||
|
|
||||||
#include <xenia/emulator.h>
|
#include <xenia/emulator.h>
|
||||||
#include <xenia/debug/debug_server.h>
|
#include <xenia/debug/debug_server.h>
|
||||||
|
@ -133,10 +134,10 @@ void WSClientOnMsgCallback(wslay_event_context_ptr ctx,
|
||||||
WSClient* client = reinterpret_cast<WSClient*>(user_data);
|
WSClient* client = reinterpret_cast<WSClient*>(user_data);
|
||||||
switch (arg->opcode) {
|
switch (arg->opcode) {
|
||||||
case WSLAY_TEXT_FRAME:
|
case WSLAY_TEXT_FRAME:
|
||||||
XELOGW("Text frame ignored; use binary messages");
|
client->OnMessage(arg->msg, arg->msg_length);
|
||||||
break;
|
break;
|
||||||
case WSLAY_BINARY_FRAME:
|
case WSLAY_BINARY_FRAME:
|
||||||
//client->OnMessage(arg->msg, arg->msg_length);
|
// Ignored.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown opcode - some frame stuff?
|
// Unknown opcode - some frame stuff?
|
||||||
|
@ -371,7 +372,18 @@ void WSClient::EventThread() {
|
||||||
delete this;
|
delete this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WSClient::Write(uint8_t** buffers, size_t* lengths, size_t count) {
|
void WSClient::Write(const char* value) {
|
||||||
|
const uint8_t* buffers[] = {
|
||||||
|
(uint8_t*)value,
|
||||||
|
};
|
||||||
|
size_t lengths[] = {
|
||||||
|
sizeof(char) * xestrlena(value),
|
||||||
|
};
|
||||||
|
Write(buffers, lengths, XECOUNT(buffers), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WSClient::Write(const uint8_t** buffers, size_t* lengths, size_t count,
|
||||||
|
bool binary) {
|
||||||
if (!count) {
|
if (!count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -400,7 +412,7 @@ void WSClient::Write(uint8_t** buffers, size_t* lengths, size_t count) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct wslay_event_msg msg = {
|
struct wslay_event_msg msg = {
|
||||||
WSLAY_BINARY_FRAME,
|
binary ? WSLAY_BINARY_FRAME : WSLAY_TEXT_FRAME,
|
||||||
combined_buffer,
|
combined_buffer,
|
||||||
combined_length,
|
combined_length,
|
||||||
};
|
};
|
||||||
|
@ -415,3 +427,84 @@ void WSClient::Write(uint8_t** buffers, size_t* lengths, size_t count) {
|
||||||
xe_socket_loop_set_queued_write(loop_);
|
xe_socket_loop_set_queued_write(loop_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WSClient::OnMessage(const uint8_t* data, size_t length) {
|
||||||
|
//
|
||||||
|
const char* s = (const char*)data;
|
||||||
|
printf(s);
|
||||||
|
// command
|
||||||
|
// requestId
|
||||||
|
|
||||||
|
json_error_t error;
|
||||||
|
json_t* request = json_loadb(
|
||||||
|
(const char*)data, length, JSON_DECODE_INT_AS_REAL, &error);
|
||||||
|
if (!request) {
|
||||||
|
// Failed to parse.
|
||||||
|
XELOGE("Failed to parse JSON request: %d:%d %s %s",
|
||||||
|
error.line, error.column,
|
||||||
|
error.source, error.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!json_is_object(request)) {
|
||||||
|
XELOGE("Expected JSON object");
|
||||||
|
json_decref(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t* request_id_json = json_object_get(request, "requestId");
|
||||||
|
if (!request_id_json || !json_is_number(request_id_json)) {
|
||||||
|
XELOGE("Need requestId field");
|
||||||
|
json_decref(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
json_t* command_json = json_object_get(request, "command");
|
||||||
|
if (!command_json || !json_is_string(command_json)) {
|
||||||
|
XELOGE("Need command field");
|
||||||
|
json_decref(request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle command.
|
||||||
|
bool succeeded = false;
|
||||||
|
const char* command = json_string_value(command_json);
|
||||||
|
json_t* result_json = HandleMessage(command, request, succeeded);
|
||||||
|
if (!result_json) {
|
||||||
|
result_json = json_null();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response.
|
||||||
|
json_t* response = json_object();
|
||||||
|
json_object_set(response, "requestId", request_id_json);
|
||||||
|
json_t* status_json = succeeded ? json_true() : json_false();
|
||||||
|
json_object_set_new(response, "status", status_json);
|
||||||
|
json_object_set_new(response, "result", result_json);
|
||||||
|
|
||||||
|
// Encode response to string and send back.
|
||||||
|
const char* response_string = json_dumps(response, JSON_INDENT(2));
|
||||||
|
Write(response_string);
|
||||||
|
|
||||||
|
json_decref(request);
|
||||||
|
json_decref(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t* WSClient::HandleMessage(const char* command, json_t* request,
|
||||||
|
bool& succeeded) {
|
||||||
|
succeeded = false;
|
||||||
|
|
||||||
|
// Get target.
|
||||||
|
char target_name[16] = { 0 };
|
||||||
|
const char* dot = xestrchra(command, '.');
|
||||||
|
if (dot) {
|
||||||
|
if (dot - command > XECOUNT(target_name)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
xestrncpya(target_name, XECOUNT(target_name),
|
||||||
|
command, dot - command);
|
||||||
|
}
|
||||||
|
const char* sub_command = command + (dot - command + 1);
|
||||||
|
|
||||||
|
// Lookup target and dispatch.
|
||||||
|
|
||||||
|
succeeded = true;
|
||||||
|
return json_null();
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
struct wslay_event_msg;
|
struct wslay_event_msg;
|
||||||
|
struct json_t;
|
||||||
|
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
|
@ -37,7 +38,12 @@ public:
|
||||||
virtual int Setup();
|
virtual int Setup();
|
||||||
virtual void Close();
|
virtual void Close();
|
||||||
|
|
||||||
void Write(uint8_t** buffers, size_t* lengths, size_t count);
|
void Write(const char* value);
|
||||||
|
void Write(const uint8_t** buffers, size_t* lengths, size_t count,
|
||||||
|
bool binary = true);
|
||||||
|
|
||||||
|
void OnMessage(const uint8_t* data, size_t length);
|
||||||
|
json_t* HandleMessage(const char* command, json_t* request, bool& succeeded);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void StartCallback(void* param);
|
static void StartCallback(void* param);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4e8c4bfbd209e50cc7b13993bbd56adb9282c465
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Copyright 2013 Ben Vanik. All Rights Reserved.
|
||||||
|
{
|
||||||
|
'targets': [
|
||||||
|
{
|
||||||
|
'target_name': 'jansson',
|
||||||
|
'type': '<(library)',
|
||||||
|
|
||||||
|
'direct_dependent_settings': {
|
||||||
|
'include_dirs': [
|
||||||
|
'jansson/win32/',
|
||||||
|
'jansson/src/',
|
||||||
|
],
|
||||||
|
|
||||||
|
'defines': [
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'msvs_disabled_warnings': [4267],
|
||||||
|
|
||||||
|
'defines': [
|
||||||
|
],
|
||||||
|
|
||||||
|
'conditions': [
|
||||||
|
['OS != "win"', {
|
||||||
|
'defines': [
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
['OS == "win"', {
|
||||||
|
'defines': [
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
|
||||||
|
'include_dirs': [
|
||||||
|
'jansson/win32/',
|
||||||
|
'jansson/src/',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sources': [
|
||||||
|
'jansson/win32/jansson_config.h',
|
||||||
|
'jansson/src/dump.c',
|
||||||
|
'jansson/src/error.c',
|
||||||
|
'jansson/src/hashtable.c',
|
||||||
|
'jansson/src/hashtable.h',
|
||||||
|
'jansson/src/jansson.h',
|
||||||
|
'jansson/src/jansson_private.h',
|
||||||
|
'jansson/src/load.c',
|
||||||
|
'jansson/src/memory.c',
|
||||||
|
'jansson/src/pack_unpack.c',
|
||||||
|
'jansson/src/strbuffer.c',
|
||||||
|
'jansson/src/strbuffer.h',
|
||||||
|
'jansson/src/strconv.c',
|
||||||
|
'jansson/src/utf.c',
|
||||||
|
'jansson/src/utf.h',
|
||||||
|
'jansson/src/value.c',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
'third_party/asmjit.gypi',
|
'third_party/asmjit.gypi',
|
||||||
'third_party/beaengine.gypi',
|
'third_party/beaengine.gypi',
|
||||||
'third_party/gflags.gypi',
|
'third_party/gflags.gypi',
|
||||||
|
'third_party/jansson.gypi',
|
||||||
'third_party/sparsehash.gypi',
|
'third_party/sparsehash.gypi',
|
||||||
'third_party/wslay.gypi',
|
'third_party/wslay.gypi',
|
||||||
],
|
],
|
||||||
|
@ -249,6 +250,7 @@
|
||||||
'asmjit',
|
'asmjit',
|
||||||
'beaengine',
|
'beaengine',
|
||||||
'gflags',
|
'gflags',
|
||||||
|
'jansson',
|
||||||
'wslay',
|
'wslay',
|
||||||
'alloy',
|
'alloy',
|
||||||
],
|
],
|
||||||
|
@ -256,6 +258,7 @@
|
||||||
'asmjit',
|
'asmjit',
|
||||||
'beaengine',
|
'beaengine',
|
||||||
'gflags',
|
'gflags',
|
||||||
|
'jansson',
|
||||||
'wslay',
|
'wslay',
|
||||||
'alloy',
|
'alloy',
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue