Merge pull request #3492 from birarda/domain-wizard

initial re-tooling of domain-server settings for metaverse setup
This commit is contained in:
Philip Rosedale 2014-09-26 15:40:44 -07:00
commit 2f609bb711
27 changed files with 903 additions and 431 deletions

View file

@ -643,7 +643,7 @@ void AudioMixer::run() {
QJsonObject audioGroupObject = settingsObject[AUDIO_GROUP_KEY].toObject();
// check the payload to see if we have asked for dynamicJitterBuffer support
const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "A-dynamic-jitter-buffer";
const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic-jitter-buffer";
_streamSettings._dynamicJitterBuffers = audioGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool();
if (_streamSettings._dynamicJitterBuffers) {
qDebug() << "Enable dynamic jitter buffers.";
@ -652,21 +652,21 @@ void AudioMixer::run() {
}
bool ok;
const QString DESIRED_JITTER_BUFFER_FRAMES_KEY = "B-static-desired-jitter-buffer-frames";
const QString DESIRED_JITTER_BUFFER_FRAMES_KEY = "static-desired-jitter-buffer-frames";
_streamSettings._staticDesiredJitterBufferFrames = audioGroupObject[DESIRED_JITTER_BUFFER_FRAMES_KEY].toString().toInt(&ok);
if (!ok) {
_streamSettings._staticDesiredJitterBufferFrames = DEFAULT_STATIC_DESIRED_JITTER_BUFFER_FRAMES;
}
qDebug() << "Static desired jitter buffer frames:" << _streamSettings._staticDesiredJitterBufferFrames;
const QString MAX_FRAMES_OVER_DESIRED_JSON_KEY = "C-max-frames-over-desired";
const QString MAX_FRAMES_OVER_DESIRED_JSON_KEY = "max-frames-over-desired";
_streamSettings._maxFramesOverDesired = audioGroupObject[MAX_FRAMES_OVER_DESIRED_JSON_KEY].toString().toInt(&ok);
if (!ok) {
_streamSettings._maxFramesOverDesired = DEFAULT_MAX_FRAMES_OVER_DESIRED;
}
qDebug() << "Max frames over desired:" << _streamSettings._maxFramesOverDesired;
const QString USE_STDEV_FOR_DESIRED_CALC_JSON_KEY = "D-use-stdev-for-desired-calc";
const QString USE_STDEV_FOR_DESIRED_CALC_JSON_KEY = "use-stdev-for-desired-calc";
_streamSettings._useStDevForJitterCalc = audioGroupObject[USE_STDEV_FOR_DESIRED_CALC_JSON_KEY].toBool();
if (_streamSettings._useStDevForJitterCalc) {
qDebug() << "Using Philip's stdev method for jitter calc if dynamic jitter buffers enabled";
@ -674,28 +674,28 @@ void AudioMixer::run() {
qDebug() << "Using Fred's max-gap method for jitter calc if dynamic jitter buffers enabled";
}
const QString WINDOW_STARVE_THRESHOLD_JSON_KEY = "E-window-starve-threshold";
const QString WINDOW_STARVE_THRESHOLD_JSON_KEY = "window-starve-threshold";
_streamSettings._windowStarveThreshold = audioGroupObject[WINDOW_STARVE_THRESHOLD_JSON_KEY].toString().toInt(&ok);
if (!ok) {
_streamSettings._windowStarveThreshold = DEFAULT_WINDOW_STARVE_THRESHOLD;
}
qDebug() << "Window A starve threshold:" << _streamSettings._windowStarveThreshold;
const QString WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES_JSON_KEY = "F-window-seconds-for-desired-calc-on-too-many-starves";
const QString WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES_JSON_KEY = "window-seconds-for-desired-calc-on-too-many-starves";
_streamSettings._windowSecondsForDesiredCalcOnTooManyStarves = audioGroupObject[WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES_JSON_KEY].toString().toInt(&ok);
if (!ok) {
_streamSettings._windowSecondsForDesiredCalcOnTooManyStarves = DEFAULT_WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES;
}
qDebug() << "Window A length:" << _streamSettings._windowSecondsForDesiredCalcOnTooManyStarves << "seconds";
const QString WINDOW_SECONDS_FOR_DESIRED_REDUCTION_JSON_KEY = "G-window-seconds-for-desired-reduction";
const QString WINDOW_SECONDS_FOR_DESIRED_REDUCTION_JSON_KEY = "window-seconds-for-desired-reduction";
_streamSettings._windowSecondsForDesiredReduction = audioGroupObject[WINDOW_SECONDS_FOR_DESIRED_REDUCTION_JSON_KEY].toString().toInt(&ok);
if (!ok) {
_streamSettings._windowSecondsForDesiredReduction = DEFAULT_WINDOW_SECONDS_FOR_DESIRED_REDUCTION;
}
qDebug() << "Window B length:" << _streamSettings._windowSecondsForDesiredReduction << "seconds";
const QString REPETITION_WITH_FADE_JSON_KEY = "H-repetition-with-fade";
const QString REPETITION_WITH_FADE_JSON_KEY = "repetition-with-fade";
_streamSettings._repetitionWithFade = audioGroupObject[REPETITION_WITH_FADE_JSON_KEY].toBool();
if (_streamSettings._repetitionWithFade) {
qDebug() << "Repetition with fade enabled";
@ -703,13 +703,13 @@ void AudioMixer::run() {
qDebug() << "Repetition with fade disabled";
}
const QString PRINT_STREAM_STATS_JSON_KEY = "I-print-stream-stats";
const QString PRINT_STREAM_STATS_JSON_KEY = "print-stream-stats";
_printStreamStats = audioGroupObject[PRINT_STREAM_STATS_JSON_KEY].toBool();
if (_printStreamStats) {
qDebug() << "Stream stats will be printed to stdout";
}
const QString FILTER_KEY = "J-enable-filter";
const QString FILTER_KEY = "enable-filter";
if (audioGroupObject[FILTER_KEY].isBool()) {
_enableFilter = audioGroupObject[FILTER_KEY].toBool();
}
@ -717,7 +717,7 @@ void AudioMixer::run() {
qDebug() << "Filter enabled";
}
const QString UNATTENUATED_ZONE_KEY = "Z-unattenuated-zone";
const QString UNATTENUATED_ZONE_KEY = "unattenuated-zone";
QString unattenuatedZoneString = audioGroupObject[UNATTENUATED_ZONE_KEY].toString();
if (!unattenuatedZoneString.isEmpty()) {

View file

@ -3,14 +3,37 @@ set(TARGET_NAME domain-server)
# setup the project and link required Qt modules
setup_hifi_project(Network)
# remove and then copy the files for the webserver
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web)
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${PROJECT_SOURCE_DIR}/resources/web"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web)
# remove current resources dir
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources
)
# copy all files in resources, including web
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${PROJECT_SOURCE_DIR}/resources"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources
)
if (NOT WIN32)
# remove the web directory so we can make it a symlink
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web
)
# make the web directory a symlink
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E create_symlink
"${PROJECT_SOURCE_DIR}/resources/web"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web
)
endif ()
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)

View file

@ -0,0 +1,128 @@
[
{
"name": "metaverse",
"label": "Metaverse Registration",
"settings": [
{
"name": "access-token",
"label": "Access Token",
"help": "This is an access token generated on the <a href='https://data.highfidelity.io/tokens'>My Tokens</a> page of your High Fidelity account.<br/>Generate a token with the 'domains' scope and paste it here.<br/>This is required to associate this domain-server with a domain in your account."
},
{
"name": "id",
"label": "Domain ID",
"help": "This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank."
}
]
},
{
"name": "security",
"label": "Security",
"settings": [
{
"name": "http-username",
"label": "HTTP Username",
"help": "Username used for basic HTTP authentication."
},
{
"name": "http-password",
"label": "HTTP Password",
"type": "password",
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
"value-hidden": true
}
]
},
{
"name": "audio",
"label": "Audio",
"assignment-types": [0],
"settings": [
{
"name": "enable-filter",
"type": "checkbox",
"label": "Enable Positional Filter",
"help": "positional audio stream uses lowpass filter",
"default": true
},
{
"name": "unattenuated-zone",
"label": "Unattenuated Zone",
"help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)",
"placeholder": "no zone"
},
{
"name": "dynamic-jitter-buffer",
"type": "checkbox",
"label": "Dynamic Jitter Buffers",
"help": "dynamically buffer client audio based on perceived jitter in packet receipt timing",
"default": false,
"advanced": true
},
{
"name": "static-desired-jitter-buffer-frames",
"label": "Static Desired Jitter Buffer Frames",
"help": "If dynamic jitter buffers is disabled, this determines the target number of frames maintained by the AudioMixer's jitter buffers",
"placeholder": "1",
"default": "1",
"advanced": true
},
{
"name": "max-frames-over-desired",
"label": "Max Frames Over Desired",
"help": "The highest number of frames an AudioMixer's ringbuffer can exceed the desired jitter buffer frames by",
"placeholder": "10",
"default": "10",
"advanced": true
},
{
"name": "use-stdev-for-desired-calc",
"type": "checkbox",
"label": "Use Stdev for Desired Jitter Frames Calc:",
"help": "use Philip's method (stdev of timegaps) to calculate desired jitter frames (otherwise Fred's max timegap method is used)",
"default": false,
"advanced": true
},
{
"name": "window-starve-threshold",
"label": "Window Starve Threshold",
"help": "If this many starves occur in an N-second window (N is the number in the next field), then the desired jitter frames will be re-evaluated using Window A.",
"placeholder": "3",
"default": "3",
"advanced": true
},
{
"name": "window-seconds-for-desired-calc-on-too-many-starves",
"label": "Timegaps Window (A) Seconds:",
"help": "Window A contains a history of timegaps. Its max timegap is used to re-evaluate the desired jitter frames when too many starves occur within it.",
"placeholder": "50",
"default": "50",
"advanced": true
},
{
"name": "window-seconds-for-desired-reduction",
"label": "Timegaps Window (B) Seconds:",
"help": "Window B contains a history of timegaps. Its max timegap is used as a ceiling for the desired jitter frames value.",
"placeholder": "10",
"default": "10",
"advanced": true
},
{
"name": "repetition-with-fade",
"type": "checkbox",
"label": "Repetition with Fade:",
"help": "dropped frames and mixing during starves repeat the last frame, eventually fading to silence",
"default": false,
"advanced": true
},
{
"name": "I-print-stream-stats",
"type": "checkbox",
"label": "Print Stream Stats:",
"help": "audio upstream and downstream stats of each agent printed to audio-mixer stdout",
"default": false,
"advanced": true
}
]
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,12 @@
#nodes-lead, #settings-lead {
body {
position: relative;
}
.table-lead {
color: #66CCCC;
}
#nodes-lead .lead-line, #settings-lead .lead-line {
.table-lead .lead-line {
background-color: #66CCCC;
}
@ -46,4 +50,23 @@ span.port {
.stale {
color: red;
}
.advanced-setting {
display: none;
}
#setup-sidebar.affix {
position: fixed;
top: 15px;
}
#setup-sidebar button {
width: 100%;
margin-top: 10px;
}
#small-save-button {
width: 100%;
margin-bottom: 15px;
}

View file

@ -1,4 +1,4 @@
</div>
<script src='/js/jquery-2.0.3.min.js'></script>
<script src='/js/jquery-2.1.1.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/domain-server.js'></script>

View file

@ -25,16 +25,15 @@
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="/">Nodes</a></li>
<li><a href="/settings/">Settings</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assignments <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/assignment">New Assignment</a></li>
</ul>
</li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assignments <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/assignment">New Assignment</a></li>
</ul>
</li>
<li><a href="/settings/">Settings</a></li>
</ul>
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="container">
<div class="container-fluid">

View file

@ -1,63 +1,78 @@
<!--#include file="header.html"-->
<div id="nodes-lead" class="table-lead"><h3>Nodes</h3><div class="lead-line"></div></div>
<div style="clear:both;"></div>
<button type="button" class="btn btn-danger" id="kill-all-btn">
<span class="glyphicon glyphicon-fire"></span> Kill all Nodes
</button>
<table id="nodes-table" class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>UUID</th>
<th>Pool</th>
<th>Username</th>
<th>Public</th>
<th>Local</th>
<th>Uptime (s)</th>
<th>Pending Credits</th>
<th>Kill?</th>
</tr>
</thead>
<tbody>
<script id="nodes-template" type="text/template">
<% _.each(nodes, function(node, node_index){ %>
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Nodes</h3>
</div>
<div class="panel-body">
<table id="nodes-table" class="table table-striped">
<thead>
<tr>
<td><%- node.type %></td>
<td><a href="stats/?uuid=<%- node.uuid %>"><%- node.uuid %></a></td>
<td><%- node.pool %></td>
<td><%- node.username %></td>
<td><%- node.public.ip %><span class='port'>:<%- node.public.port %></span></td>
<td><%- node.local.ip %><span class='port'>:<%- node.local.port %></span></td>
<td><%- ((Date.now() - node.wake_timestamp) / 1000).toLocaleString() %></td>
<td><%- (typeof node.pending_credits == 'number' ? node.pending_credits.toLocaleString() : 'N/A') %></td>
<td><span class='glyphicon glyphicon-remove' data-uuid="<%- node.uuid %>"></span></td>
<th>Type</th>
<th>UUID</th>
<th>Pool</th>
<th>Username</th>
<th>Public</th>
<th>Local</th>
<th>Uptime (s)</th>
<th>Pending Credits</th>
<th>Kill?</th>
</tr>
<% }); %>
</script>
</table>
</thead>
<tbody>
<script id="nodes-template" type="text/template">
<% _.each(nodes, function(node, node_index){ %>
<tr>
<td><%- node.type %></td>
<td><a href="stats/?uuid=<%- node.uuid %>"><%- node.uuid %></a></td>
<td><%- node.pool %></td>
<td><%- node.username %></td>
<td><%- node.public.ip %><span class='port'>:<%- node.public.port %></span></td>
<td><%- node.local.ip %><span class='port'>:<%- node.local.port %></span></td>
<td><%- ((Date.now() - node.wake_timestamp) / 1000).toLocaleString() %></td>
<td><%- (typeof node.pending_credits == 'number' ? node.pending_credits.toLocaleString() : 'N/A') %></td>
<td><span class='glyphicon glyphicon-remove' data-uuid="<%- node.uuid %>"></span></td>
</tr>
<% }); %>
</script>
</table>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-danger" id="kill-all-btn">
<span class="glyphicon glyphicon-remove-circle"></span> Kill all Nodes
</button>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Queued Assignments</h3>
</div>
<div class="panel-body">
<table id="assignments-table" class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>UUID</th>
<th>Pool</th>
</tr>
</thead>
<tbody>
<script id="queued-template" type="text/template">
<% _.each(queued, function(assignment, uuid){ %>
<tr>
<td><%- assignment.type %></td>
<td><%- uuid %></td>
<td><%- assignment.pool %></td>
</tr>
<% }); %>
</script>
</tbody>
</table>
<div>
</div>
</div>
<div id="queued-lead" class="table-lead"><h3>Queued Assignments</h3><div class="lead-line"></div></div>
<table id="assignments-table" class="table table-striped">
<thead>
<tr>
<th>Type</th>
<th>UUID</th>
<th>Pool</th>
</tr>
</thead>
<tbody>
<script id="queued-template" type="text/template">
<% _.each(queued, function(assignment, uuid){ %>
<tr>
<td><%- assignment.type %></td>
<td><%- uuid %></td>
<td><%- assignment.pool %></td>
</tr>
<% }); %>
</script>
</tbody>
</table>
<!--#include file="footer.html"-->
<script src='js/underscore-min.js'></script>
<script src='js/tables.js'></script>
<script src='js/underscore-1.5.0.min.js'></script>
<!--#include file="page-end.html"-->

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,34 +1,128 @@
var Settings = {};
var Settings = {
showAdvanced: false
};
var viewHelpers = {
getFormGroup: function(groupName, setting, values, isAdvanced) {
setting_id = groupName + "_" + setting.name
form_group = "<div class='form-group" + (isAdvanced ? " advanced-setting" : "") + "'>"
if (_.has(values, groupName) && _.has(values[groupName], setting.name)) {
setting_value = values[groupName][setting.name]
} else if (_.has(setting, 'default')) {
setting_value = setting.default
} else {
setting_value = ""
}
if (setting.type === 'checkbox') {
form_group += "<label class='control-label'>" + setting.label + "</label>"
form_group += "<div class='checkbox'>"
form_group += "<label for='" + setting_id + "'>"
form_group += "<input type='checkbox' id='" + setting_id + "' " + (setting_value ? "checked" : "") + "/>"
form_group += " " + setting.help + "</label>";
form_group += "</div>"
} else {
input_type = _.has(setting, 'type') ? setting.type : "text"
form_group += "<label for='" + setting_id + "' class='control-label'>" + setting.label + "</label>";
form_group += "<input type='" + input_type + "' class='form-control' id='" + setting_id +
"' placeholder='" + (_.has(setting, 'placeholder') ? setting.placeholder : "") +
"' value='" + setting_value + "'/>"
form_group += "<span class='help-block'>" + setting.help + "</span>"
}
form_group += "</div>"
return form_group
}
}
$(document).ready(function(){
var source = $('#settings-template').html();
Settings.template = _.template(source);
/*
* Clamped-width.
* Usage:
* <div data-clampedwidth=".myParent">This long content will force clamped width</div>
*
* Author: LV
*/
reloadSettings();
});
$('[data-clampedwidth]').each(function () {
var elem = $(this);
var parentPanel = elem.data('clampedwidth');
var resizeFn = function () {
var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth'));
elem.css('width', sideBarNavWidth);
};
resizeFn();
$(window).resize(resizeFn);
})
$('#settings-form').on('change', 'input', function(){
// this input was changed, add the changed data attribute to it
$(this).attr('data-changed', true)
badgeSidebarForDifferences($(this))
})
$('#advanced-toggle-button').click(function(){
Settings.showAdvanced = !Settings.showAdvanced
var advancedSelector = $('.advanced-setting')
if (Settings.showAdvanced) {
advancedSelector.show()
$(this).html("Hide advanced")
} else {
advancedSelector.hide()
$(this).html("Show advanced")
}
$(this).blur()
})
var panelsSource = $('#panels-template').html()
Settings.panelsTemplate = _.template(panelsSource)
var sidebarTemplate = $('#list-group-template').html()
Settings.sidebarTemplate = _.template(sidebarTemplate)
// $('body').scrollspy({ target: '#setup-sidebar'})
reloadSettings()
})
function reloadSettings() {
$.getJSON('/settings.json', function(data){
$('#settings').html(Settings.template(data));
_.extend(data, viewHelpers)
$('.nav-stacked').html(Settings.sidebarTemplate(data))
$('#panels').html(Settings.panelsTemplate(data))
Settings.initialValues = form2js('settings-form', "_", false, cleanupFormValues, true);
});
}
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
$('#settings').on('click', 'button', function(e){
$('body').on('click', '.save-button', function(e){
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
var formJSON = form2js('settings-form', "_", false, cleanupFormValues, true);
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
// remove focus from the button
$(this).blur()
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
$.ajax('/settings.json', {
data: JSON.stringify(formJSON),
@ -36,12 +130,11 @@ $('#settings').on('click', 'button', function(e){
type: 'POST'
}).done(function(data){
if (data.status == "success") {
showAlertMessage("Domain settings saved.", true);
showRestartModal();
} else {
showAlertMessage(SETTINGS_ERROR_MESSAGE, false);
reloadSettings();
}
reloadSettings();
}).fail(function(){
showAlertMessage(SETTINGS_ERROR_MESSAGE, false);
reloadSettings();
@ -50,10 +143,54 @@ $('#settings').on('click', 'button', function(e){
return false;
});
$('#settings').on('change', 'input', function(){
// this input was changed, add the changed data attribute to it
$(this).attr('data-changed', true);
});
function badgeSidebarForDifferences(changedInput) {
// figure out which group this input is in
var panelParentID = changedInput.closest('.panel').attr('id')
// get a JSON representation of that section
var rootJSON = form2js(panelParentID, "_", false, cleanupFormValues, true);
var panelJSON = rootJSON[panelParentID]
var badgeValue = 0
for (var setting in panelJSON) {
if (panelJSON[setting] != Settings.initialValues[panelParentID][ setting]) {
badgeValue += 1
}
}
// update the list-group-item badge to have the new value
if (badgeValue == 0) {
badgeValue = ""
}
$("a[href='#" + panelParentID + "'] .badge").html(badgeValue);
}
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
function cleanupFormValues(node) {
if (node.type && node.type === 'checkbox') {

View file

@ -0,0 +1,33 @@
$(document).ready(function(){
/*
* Clamped-width.
* Usage:
* <div data-clampedwidth=".myParent">This long content will force clamped width</div>
*
* Author: LV
*/
$('[data-clampedwidth]').each(function () {
var elem = $(this);
var parentPanel = elem.data('clampedwidth');
var resizeFn = function () {
var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth'));
elem.css('width', sideBarNavWidth);
};
resizeFn();
$(window).resize(resizeFn);
});
var listSource = $('#list-group-template').html();
var listTemplate = _.template(listSource);
reloadSettings();
function reloadSettings() {
$.getJSON('describe-setup.json', function(data){
$('.list-group').html(listTemplate(data));
});
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,74 +0,0 @@
{
"audio": {
"label": "Audio",
"assignment-types": [0],
"settings": {
"A-dynamic-jitter-buffer": {
"type": "checkbox",
"label": "Dynamic Jitter Buffers",
"help": "Dynamically buffer client audio based on perceived jitter in packet receipt timing",
"default": false
},
"B-static-desired-jitter-buffer-frames": {
"label": "Static Desired Jitter Buffer Frames",
"help": "If dynamic jitter buffers is disabled, this determines the target number of frames maintained by the AudioMixer's jitter buffers",
"placeholder": "1",
"default": "1"
},
"C-max-frames-over-desired": {
"label": "Max Frames Over Desired",
"help": "The highest number of frames an AudioMixer's ringbuffer can exceed the desired jitter buffer frames by",
"placeholder": "10",
"default": "10"
},
"D-use-stdev-for-desired-calc": {
"type": "checkbox",
"label": "Use Stdev for Desired Jitter Frames Calc:",
"help": "If checked, Philip's method (stdev of timegaps) is used to calculate desired jitter frames. Otherwise, Fred's method (max timegap) is used",
"default": false
},
"E-window-starve-threshold": {
"label": "Window Starve Threshold",
"help": "If this many starves occur in an N-second window (N is the number in the next field), then the desired jitter frames will be re-evaluated using Window A.",
"placeholder": "3",
"default": "3"
},
"F-window-seconds-for-desired-calc-on-too-many-starves": {
"label": "Timegaps Window (A) Seconds:",
"help": "Window A contains a history of timegaps. Its max timegap is used to re-evaluate the desired jitter frames when too many starves occur within it.",
"placeholder": "50",
"default": "50"
},
"G-window-seconds-for-desired-reduction": {
"label": "Timegaps Window (B) Seconds:",
"help": "Window B contains a history of timegaps. Its max timegap is used as a ceiling for the desired jitter frames value.",
"placeholder": "10",
"default": "10"
},
"H-repetition-with-fade": {
"type": "checkbox",
"label": "Repetition with Fade:",
"help": "If enabled, dropped frames and mixing during starves will repeat the last frame, eventually fading to silence",
"default": false
},
"I-print-stream-stats": {
"type": "checkbox",
"label": "Print Stream Stats:",
"help": "If enabled, audio upstream and downstream stats of each agent will be printed each second to stdout",
"default": false
},
"Z-unattenuated-zone": {
"label": "Unattenuated Zone",
"help": "Boxes for source and listener (corner x, corner y, corner z, size x, size y, size z, corner x, corner y, corner z, size x, size y, size z)",
"placeholder": "no zone",
"default": ""
},
"J-enable-filter": {
"type": "checkbox",
"label": "Enable Positional Filter",
"help": "If enabled, positional audio stream uses lowpass filter",
"default": true
}
}
}
}

View file

@ -1,52 +1,78 @@
<!--#include virtual="header.html"-->
<div id="settings-lead" class="table-lead"><h3>Settings</h3><div class="lead-line"></div></div>
<div style="clear: both;"></div>
<div class="alert" style="display:none;"></div>
<form class="form-horizontal" id="settings-form" role="form">
<div class="col-md-10 col-md-offset-1">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
<script id="settings-template" type="text/template">
<% _.each(descriptions, function(group, group_key){ %>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(group.settings, function(setting, setting_key){ %>
<div class="form-group">
<% var setting_id = group_key + "." + setting_key %>
<label for="<%- setting_id %>" class="col-sm-2 control-label"><%- setting.label %></label>
<div class="col-sm-10">
<% if (setting.type === "checkbox") { %>
<% var checked_box = _.has(values, group_key) ? values[group_key][setting_key] : setting.default %>
<input type="checkbox" id="<%- setting_id %>" <%- checked_box ? "checked" : "" %>>
<% } else { %>
<% if (setting.input_addon) { %>
<div class="input-group">
<div class="input-group-addon"><%- setting.input_addon %></div>
<% } %>
<input type="text" class="form-control" id="<%- setting_id %>"
placeholder="<%- setting.placeholder %>"
value="<%- (values[group_key] || {})[setting_key] %>">
<% if (setting.input_addon) { %>
</div>
<% } %>
<% } %>
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col" class="hidden-xs" data-spy="affix" data-offset-top="55">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<li>
<a href="#<%-group.name %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% }); %>
</script>
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" class="btn btn-info">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<div class="panel panel-default" id="<%- group.name %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% split_settings = _.partition(group.settings, function(value, index) { return !value.advanced }) %>
<% _.each(split_settings[0], function(setting) { %>
<%= getFormGroup(group.name, setting, values, false) %>
<% }); %>
<% _.each(split_settings[1], function(setting) { %>
<%= getFormGroup(group.name, setting, values, true) %>
<% }); %>
</div>
<p class="help-block col-sm-offset-2 col-sm-10"><%- setting.help %></p>
</div>
<% }); %>
</div>
</div>
<% }); %>
<button type="submit" class="btn btn-default">Save</button>
</script>
</script>
<div id="panels"></div>
</form>
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save and restart</button>
</div>
</div>
<div class="modal fade" id="restart-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div id="settings"></div>
</form>
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/settings.js'></script>
<script src='/js/form2js.min.js'></script>
<script src='/js/underscore-1.5.0.min.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -30,6 +30,8 @@
#include "DomainServer.h"
int const DomainServer::EXIT_CODE_REBOOT = 234923;
DomainServer::DomainServer(int argc, char* argv[]) :
QCoreApplication(argc, argv),
_shutdownEventListener(this),
@ -48,22 +50,21 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_cookieSessionHash(),
_settingsManager()
{
LogUtils::init();
setOrganizationName("High Fidelity");
setOrganizationDomain("highfidelity.io");
setApplicationName("domain-server");
QSettings::setDefaultFormat(QSettings::IniFormat);
_settingsManager.loadSettingsMap(arguments());
installNativeEventFilter(&_shutdownEventListener);
connect(&_shutdownEventListener, SIGNAL(receivedCloseEvent()), SLOT(quit()));
qRegisterMetaType<DomainServerWebSessionData>("DomainServerWebSessionData");
qRegisterMetaTypeStreamOperators<DomainServerWebSessionData>("DomainServerWebSessionData");
_argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments());
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) {
// we either read a certificate and private key or were not passed one
// and completed login or did not need to
@ -78,13 +79,18 @@ DomainServer::DomainServer(int argc, char* argv[]) :
}
}
void DomainServer::restart() {
qDebug() << "domain-server is restarting.";
exit(DomainServer::EXIT_CODE_REBOOT);
}
bool DomainServer::optionallyReadX509KeyAndCertificate() {
const QString X509_CERTIFICATE_OPTION = "cert";
const QString X509_PRIVATE_KEY_OPTION = "key";
const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE";
QString certPath = _argumentVariantMap.value(X509_CERTIFICATE_OPTION).toString();
QString keyPath = _argumentVariantMap.value(X509_PRIVATE_KEY_OPTION).toString();
QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString();
QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString();
if (!certPath.isEmpty() && !keyPath.isEmpty()) {
// the user wants to use DTLS to encrypt communication with nodes
@ -143,10 +149,11 @@ bool DomainServer::optionallySetupOAuth() {
const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET";
const QString REDIRECT_HOSTNAME_OPTION = "hostname";
_oauthProviderURL = QUrl(_argumentVariantMap.value(OAUTH_PROVIDER_URL_OPTION).toString());
_oauthClientID = _argumentVariantMap.value(OAUTH_CLIENT_ID_OPTION).toString();
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
_oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString());
_oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString();
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
_hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString();
_hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString();
if (!_oauthClientID.isEmpty()) {
if (_oauthProviderURL.isEmpty()
@ -171,9 +178,11 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
const QString CUSTOM_PORT_OPTION = "port";
unsigned short domainServerPort = DEFAULT_DOMAIN_SERVER_PORT;
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (_argumentVariantMap.contains(CUSTOM_PORT_OPTION)) {
domainServerPort = (unsigned short) _argumentVariantMap.value(CUSTOM_PORT_OPTION).toUInt();
if (settingsMap.contains(CUSTOM_PORT_OPTION)) {
domainServerPort = (unsigned short) settingsMap.value(CUSTOM_PORT_OPTION).toUInt();
}
unsigned short domainServerDTLSPort = 0;
@ -183,8 +192,8 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port";
if (_argumentVariantMap.contains(CUSTOM_DTLS_PORT_OPTION)) {
domainServerDTLSPort = (unsigned short) _argumentVariantMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt();
if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) {
domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt();
}
}
@ -197,7 +206,11 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
// set our LimitedNodeList UUID to match the UUID from our config
// nodes will currently use this to add resources to data-web that relate to our domain
nodeList->setSessionUUID(_argumentVariantMap.value(DOMAIN_CONFIG_ID_KEY).toString());
const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id";
const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH);
if (idValueVariant) {
nodeList->setSessionUUID(idValueVariant->toString());
}
connect(nodeList, &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded);
connect(nodeList, &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled);
@ -260,9 +273,10 @@ bool DomainServer::hasOAuthProviderAndAuthInformation() {
bool DomainServer::optionallySetupAssignmentPayment() {
const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments";
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) &&
_argumentVariantMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() &&
if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) &&
settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() &&
hasOAuthProviderAndAuthInformation()) {
qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString());
@ -288,8 +302,10 @@ bool DomainServer::optionallySetupAssignmentPayment() {
void DomainServer::setupDynamicIPAddressUpdating() {
const QString ENABLE_DYNAMIC_IP_UPDATING_OPTION = "update-ip";
if (_argumentVariantMap.contains(ENABLE_DYNAMIC_IP_UPDATING_OPTION) &&
_argumentVariantMap.value(ENABLE_DYNAMIC_IP_UPDATING_OPTION).toBool() &&
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (settingsMap.contains(ENABLE_DYNAMIC_IP_UPDATING_OPTION) &&
settingsMap.value(ENABLE_DYNAMIC_IP_UPDATING_OPTION).toBool() &&
hasOAuthProviderAndAuthInformation()) {
LimitedNodeList* nodeList = LimitedNodeList::getInstance();
@ -338,9 +354,11 @@ void DomainServer::parseAssignmentConfigs(QSet<Assignment::Type>& excludedTypes)
// check for configs from the command line, these take precedence
const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)";
QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING);
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
// scan for assignment config keys
QStringList variantMapKeys = _argumentVariantMap.keys();
QStringList variantMapKeys = settingsMap.keys();
int configIndex = variantMapKeys.indexOf(assignmentConfigRegex);
while (configIndex != -1) {
@ -348,7 +366,7 @@ void DomainServer::parseAssignmentConfigs(QSet<Assignment::Type>& excludedTypes)
Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt();
if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) {
QVariant mapValue = _argumentVariantMap[variantMapKeys[configIndex]];
QVariant mapValue = settingsMap[variantMapKeys[configIndex]];
QJsonArray assignmentArray;
if (mapValue.type() == QVariant::String) {
@ -513,7 +531,7 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
QString connectedUsername;
if (!isAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) {
if (!isAssignment && !_oauthProviderURL.isEmpty() && _settingsManager.getSettingsMap().contains(ALLOWED_ROLES_CONFIG_KEY)) {
// this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones
if (_sessionAuthenticationHash.contains(packetUUID)) {
connectedUsername = _sessionAuthenticationHash.take(packetUUID);
@ -1388,12 +1406,15 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
const QString BASIC_AUTH_CONFIG_KEY = "basic-auth";
const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http-username";
const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http-password";
const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server.";
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (!_oauthProviderURL.isEmpty()
&& (_argumentVariantMap.contains(ADMIN_USERS_CONFIG_KEY) || _argumentVariantMap.contains(ADMIN_ROLES_CONFIG_KEY))) {
&& (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) {
QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY);
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
@ -1404,7 +1425,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
cookieUUID = cookieUUIDRegex.cap(1);
}
if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) {
if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) {
qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication."
<< "These cannot be combined - using OAuth for authentication.";
}
@ -1414,13 +1435,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID);
QString profileUsername = sessionData.getUsername();
if (_argumentVariantMap.value(ADMIN_USERS_CONFIG_KEY).toJsonValue().toArray().contains(profileUsername)) {
if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toJsonValue().toArray().contains(profileUsername)) {
// this is an authenticated user
return true;
}
// loop the roles of this user and see if they are in the admin-roles array
QJsonArray adminRolesArray = _argumentVariantMap.value(ADMIN_ROLES_CONFIG_KEY).toJsonValue().toArray();
QJsonArray adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toJsonValue().toArray();
if (!adminRolesArray.isEmpty()) {
foreach(const QString& userRole, sessionData.getRoles()) {
@ -1455,7 +1476,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
// we don't know about this user yet, so they are not yet authenticated
return false;
}
} else if (_argumentVariantMap.contains(BASIC_AUTH_CONFIG_KEY)) {
} else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) {
// config file contains username and password combinations for basic auth
const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization";
@ -1470,21 +1491,16 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
if (!credentialString.isEmpty()) {
QStringList credentialList = credentialString.split(':');
if (credentialList.size() == 2) {
QString username = credentialList[0];
QString password = credentialList[1];
QString headerUsername = credentialList[0];
QString headerPassword = credentialList[1];
// we've pulled a username and password - now check if there is a match in our basic auth hash
QJsonObject basicAuthObject = _argumentVariantMap.value(BASIC_AUTH_CONFIG_KEY).toJsonValue().toObject();
QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString();
const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH);
QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : "";
if (basicAuthObject.contains(username)) {
const QString BASIC_AUTH_USER_PASSWORD_KEY = "password";
QJsonObject userObject = basicAuthObject.value(username).toObject();
if (userObject.contains(BASIC_AUTH_USER_PASSWORD_KEY)
&& userObject.value(BASIC_AUTH_USER_PASSWORD_KEY).toString() == password) {
// this is username / password match - let this user in
return true;
}
if (settingsUsername == headerUsername && headerPassword == settingsPassword) {
return true;
}
}
}
@ -1557,7 +1573,8 @@ void DomainServer::handleProfileRequestFinished() {
// pull the user roles from the response
QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray();
QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray();
QJsonArray allowedRolesArray = _settingsManager.getSettingsMap()
.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray();
QString connectableUsername;
QString profileUsername = profileJSON.object()["data"].toObject()["user"].toObject()["username"].toString();

View file

@ -35,16 +35,17 @@
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
public:
DomainServer(int argc, char* argv[]);
static int const EXIT_CODE_REBOOT;
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url);
bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url);
void exit(int retCode = 0);
public slots:
/// Called by NodeList to inform us a node has been added
void nodeAdded(SharedNodePointer node);
@ -53,6 +54,8 @@ public slots:
void transactionJSONCallback(const QJsonObject& data);
void restart();
private slots:
void loginFailed();
void readAvailableDatagrams();
@ -115,8 +118,6 @@ private:
QHash<QUuid, PendingAssignedNodeData*> _pendingAssignedNodes;
TransactionHash _pendingAssignmentCredits;
QVariantMap _argumentVariantMap;
bool _isUsingDTLS;
QUrl _oauthProviderURL;

View file

@ -10,45 +10,44 @@
//
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QStandardPaths>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <Assignment.h>
#include <HifiConfigVariantMap.h>
#include <HTTPConnection.h>
#include "DomainServerSettingsManager.h"
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/web/settings/describe.json";
const QString SETTINGS_JSON_FILE_RELATIVE_PATH = "/resources/settings.json";
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
DomainServerSettingsManager::DomainServerSettingsManager() :
_descriptionObject(),
_descriptionArray(),
_settingsMap()
{
// load the description object from the settings description
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
descriptionFile.open(QIODevice::ReadOnly);
_descriptionObject = QJsonDocument::fromJson(descriptionFile.readAll()).object();
// load the existing config file to get the current values
QFile configFile(QCoreApplication::applicationDirPath() + SETTINGS_JSON_FILE_RELATIVE_PATH);
if (configFile.exists()) {
configFile.open(QIODevice::ReadOnly);
_settingsMap = QJsonDocument::fromJson(configFile.readAll()).toVariant().toMap();
}
_descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array();
}
const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
void DomainServerSettingsManager::loadSettingsMap(const QStringList& argumentList) {
_settingsMap = HifiConfigVariantMap::mergeMasterConfigWithUserConfig(argumentList);
// figure out where we are supposed to persist our settings to
_settingsFilepath = HifiConfigVariantMap::userConfigFilepath(argumentList);
}
const QString SETTINGS_PATH = "/settings.json";
bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == "/settings.json") {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) {
// this is a GET operation for our settings
// check if there is a query parameter for settings affecting a particular type of assignment
@ -56,77 +55,28 @@ bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connec
QUrlQuery settingsQuery(url);
QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY);
QJsonObject responseObject;
if (typeValue.isEmpty()) {
// combine the description object and our current settings map
responseObject["descriptions"] = _descriptionObject;
responseObject["values"] = QJsonDocument::fromVariant(_settingsMap).object();
if (!typeValue.isEmpty()) {
QJsonObject responseObject = responseObjectForType(typeValue);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json");
return true;
} else {
// convert the string type value to a QJsonValue
QJsonValue queryType = QJsonValue(typeValue.toInt());
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QString& group, _descriptionObject.keys()) {
QJsonObject groupObject = _descriptionObject[group].toObject();
QJsonObject groupSettingsObject = groupObject[DESCRIPTION_SETTINGS_KEY].toObject();
QJsonObject groupResponseObject;
foreach(const QString& settingKey, groupSettingsObject.keys()) {
QJsonObject settingObject = groupSettingsObject[settingKey].toObject();
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
if (affectedTypesArray.isEmpty()) {
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
}
if (affectedTypesArray.contains(queryType)) {
// this is a setting we should include in the responseObject
// we need to check if the settings map has a value for this setting
QVariant variantValue;
QVariant settingsMapGroupValue = _settingsMap.value(group);
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingKey);
}
if (variantValue.isNull()) {
// no value for this setting, pass the default
groupResponseObject[settingKey] = settingObject[SETTING_DEFAULT_KEY];
} else {
groupResponseObject[settingKey] = QJsonValue::fromVariant(variantValue);
}
}
}
if (!groupResponseObject.isEmpty()) {
// set this group's object to the constructed object
responseObject[group] = groupResponseObject;
}
}
return false;
}
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json");
return true;
}
return false;
}
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == "/settings.json") {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
// we recurse one level deep below each group for the appropriate setting
recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionObject);
recurseJSONObjectAndOverwriteSettings(postedObject, _settingsMap, _descriptionArray);
// store whatever the current _settingsMap is to file
persistToFile();
@ -135,56 +85,153 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) {
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
QJsonObject rootObject;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray;
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
}
return false;
}
const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString DESCRIPTION_NAME_KEY = "name";
QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) {
QJsonObject responseObject;
if (!typeValue.isEmpty() || isAuthenticated) {
// convert the string type value to a QJsonValue
QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt());
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QJsonValue& groupValue, _descriptionArray) {
QJsonObject groupObject = groupValue.toObject();
QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray();
QJsonObject groupResponseObject;
foreach(const QJsonValue& settingValue, groupSettingsArray) {
const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden";
QJsonObject settingObject = settingValue.toObject();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) {
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
if (affectedTypesArray.isEmpty()) {
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
}
if (affectedTypesArray.contains(queryType) ||
(queryType.isNull() && isAuthenticated)) {
// this is a setting we should include in the responseObject
QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString();
// we need to check if the settings map has a value for this setting
QVariant variantValue;
QVariant settingsMapGroupValue = _settingsMap.value(groupObject[DESCRIPTION_NAME_KEY].toString());
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingName);
}
if (variantValue.isNull()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
groupResponseObject[settingName] = settingObject[SETTING_DEFAULT_KEY];
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
groupResponseObject[settingName] = QString("");
}
} else {
groupResponseObject[settingName] = QJsonValue::fromVariant(variantValue);
}
}
}
}
if (!groupResponseObject.isEmpty()) {
// set this group's object to the constructed object
responseObject[groupKey] = groupResponseObject;
}
}
}
return responseObject;
}
const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
QVariantMap& settingsVariant,
QJsonObject descriptionObject) {
QJsonArray descriptionArray) {
foreach(const QString& key, postedObject.keys()) {
QJsonValue rootValue = postedObject[key];
// we don't continue if this key is not present in our descriptionObject
if (descriptionObject.contains(key)) {
if (rootValue.isString()) {
if (rootValue.toString().isEmpty()) {
// this is an empty value, clear it in settings variant so the default is sent
settingsVariant.remove(key);
} else {
if (descriptionObject[key].toObject().contains(SETTING_DESCRIPTION_TYPE_KEY)) {
// for now this means that this is a double, so set it as a double
settingsVariant[key] = rootValue.toString().toDouble();
} else {
settingsVariant[key] = rootValue.toString();
}
}
} else if (rootValue.isBool()) {
settingsVariant[key] = rootValue.toBool();
} else if (rootValue.isObject()) {
// there's a JSON Object to explore, so attempt to recurse into it
QJsonObject nextDescriptionObject = descriptionObject[key].toObject();
if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
if (!settingsVariant.contains(key)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[key] = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingsVariant[key].data());
recurseJSONObjectAndOverwriteSettings(rootValue.toObject(),
thisMap,
nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toObject());
if (thisMap.isEmpty()) {
// we've cleared all of the settings below this value, so remove this one too
foreach(const QJsonValue& groupValue, descriptionArray) {
if (groupValue.toObject()[DESCRIPTION_NAME_KEY].toString() == key) {
QJsonObject groupObject = groupValue.toObject();
if (rootValue.isString()) {
if (rootValue.toString().isEmpty()) {
// this is an empty value, clear it in settings variant so the default is sent
settingsVariant.remove(key);
} else {
QString settingType = groupObject[SETTING_DESCRIPTION_TYPE_KEY].toString();
const QString INPUT_DOUBLE_TYPE = "double";
// make sure the resulting json value has the right type
if (settingType == INPUT_DOUBLE_TYPE) {
settingsVariant[key] = rootValue.toString().toDouble();
} else {
settingsVariant[key] = rootValue.toString();
}
}
} else if (rootValue.isBool()) {
settingsVariant[key] = rootValue.toBool();
} else if (rootValue.isObject()) {
// there's a JSON Object to explore, so attempt to recurse into it
QJsonObject nextDescriptionObject = groupObject;
if (nextDescriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
if (!settingsVariant.contains(key)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[key] = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingsVariant[key].data());
recurseJSONObjectAndOverwriteSettings(rootValue.toObject(),
thisMap,
nextDescriptionObject[DESCRIPTION_SETTINGS_KEY].toArray());
if (thisMap.isEmpty()) {
// we've cleared all of the settings below this value, so remove this one too
settingsVariant.remove(key);
}
}
}
}
@ -197,7 +244,15 @@ QByteArray DomainServerSettingsManager::getJSONSettingsMap() const {
}
void DomainServerSettingsManager::persistToFile() {
QFile settingsFile(QCoreApplication::applicationDirPath() + SETTINGS_JSON_FILE_RELATIVE_PATH);
// make sure we have the dir the settings file is supposed to live in
QFileInfo settingsFileInfo(_settingsFilepath);
if (!settingsFileInfo.dir().exists()) {
settingsFileInfo.dir().mkpath(".");
}
QFile settingsFile(_settingsFilepath);
if (settingsFile.open(QIODevice::WriteOnly)) {
settingsFile.write(getJSONSettingsMap());

View file

@ -12,6 +12,7 @@
#ifndef hifi_DomainServerSettingsManager_h
#define hifi_DomainServerSettingsManager_h
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <HTTPManager.h>
@ -23,14 +24,19 @@ public:
bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url);
bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url);
void loadSettingsMap(const QStringList& argumentList);
QByteArray getJSONSettingsMap() const;
QVariantMap& getSettingsMap() { return _settingsMap; }
private:
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant,
QJsonObject descriptionObject);
QJsonArray descriptionArray);
void persistToFile();
QJsonObject _descriptionObject;
QJsonArray _descriptionArray;
QVariantMap _settingsMap;
QString _settingsFilepath;
};
#endif // hifi_DomainServerSettingsManager_h

View file

@ -28,8 +28,16 @@ int main(int argc, char* argv[]) {
#endif
qInstallMessageHandler(Logging::verboseMessageHandler);
DomainServer domainServer(argc, argv);
return domainServer.exec();
int currentExitCode = 0;
// use a do-while to handle domain-server restart
do {
DomainServer domainServer(argc, argv);
currentExitCode = domainServer.exec();
} while (currentExitCode == DomainServer::EXIT_CODE_REBOOT);
return currentExitCode;
}

View file

@ -17,10 +17,10 @@ const QString ADDRESSBAR_GO_BUTTON_ICON = "images/address-bar-submit.svg";
const QString ADDRESSBAR_GO_BUTTON_ACTIVE_ICON = "images/address-bar-submit-active.svg";
AddressBarDialog::AddressBarDialog() :
FramelessDialog(Application::getInstance()->getWindow(), 0, FramelessDialog::POSITION_TOP) {
setAttribute(Qt::WA_DeleteOnClose, false);
setupUI();
FramelessDialog(Application::getInstance()->getWindow(), 0, FramelessDialog::POSITION_TOP)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setupUI();
}
void AddressBarDialog::setupUI() {

View file

@ -168,8 +168,8 @@ void AddressManager::attemptPlaceNameLookup(const QString& lookupString) {
// assume this is a place name and see if we can get any info on it
QString placeName = QUrl::toPercentEncoding(lookupString);
AccountManager::getInstance().unauthenticatedRequest(GET_PLACE.arg(placeName),
QNetworkAccessManager::GetOperation,
apiCallbackParameters());
QNetworkAccessManager::GetOperation,
apiCallbackParameters());
}
bool AddressManager::handleNetworkAddress(const QString& lookupString) {
@ -269,6 +269,6 @@ void AddressManager::goToUser(const QString& username) {
QString formattedUsername = QUrl::toPercentEncoding(username);
// this is a username - pull the captured name and lookup that user's location
AccountManager::getInstance().unauthenticatedRequest(GET_USER_LOCATION.arg(formattedUsername),
QNetworkAccessManager::GetOperation,
apiCallbackParameters());
QNetworkAccessManager::GetOperation,
apiCallbackParameters());
}

View file

@ -15,6 +15,7 @@
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QStandardPaths>
#include <QtCore/QVariant>
#include "HifiConfigVariantMap.h"
@ -75,30 +76,100 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL
// we have a config file - try and read it
configFilePath = argumentList[configIndex + 1];
} else {
// no config file - try to read a file at resources/config.json
configFilePath = QCoreApplication::applicationDirPath() + "/resources/config.json";
// no config file - try to read a file config.json at the system config path
configFilePath = QString("%1/%2/%3/config.json").arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation),
QCoreApplication::organizationName(),
QCoreApplication::applicationName());
}
QFile configFile(configFilePath);
if (configFile.exists()) {
qDebug() << "Reading JSON config file at" << configFilePath;
configFile.open(QIODevice::ReadOnly);
QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll());
QJsonObject rootObject = configDocument.object();
// enumerate the keys of the configDocument object
foreach(const QString& key, rootObject.keys()) {
if (!mergedMap.contains(key)) {
// no match in existing list, add it
mergedMap.insert(key, QVariant(rootObject[key]));
}
}
} else {
qDebug() << "Could not find JSON config file at" << configFilePath;
}
return mergedMap;
}
QVariantMap HifiConfigVariantMap::mergeMasterConfigWithUserConfig(const QStringList& argumentList) {
// check if there is a master config file
const QString MASTER_CONFIG_FILE_OPTION = "--master-config";
QVariantMap configVariantMap;
int masterConfigIndex = argumentList.indexOf(MASTER_CONFIG_FILE_OPTION);
if (masterConfigIndex != -1) {
QString masterConfigFilepath = argumentList[masterConfigIndex + 1];
mergeMapWithJSONFile(configVariantMap, masterConfigFilepath);
}
// merge the existing configVariantMap with the user config file
mergeMapWithJSONFile(configVariantMap, userConfigFilepath(argumentList));
return configVariantMap;
}
QString HifiConfigVariantMap::userConfigFilepath(const QStringList& argumentList) {
// we've loaded up the master config file, now fill in anything it didn't have with the user config file
const QString USER_CONFIG_FILE_OPTION = "--user-config";
int userConfigIndex = argumentList.indexOf(USER_CONFIG_FILE_OPTION);
QString userConfigFilepath;
if (userConfigIndex != -1) {
userConfigFilepath = argumentList[userConfigIndex + 1];
} else {
userConfigFilepath = QString("%1/%2/%3/config.json").arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation),
QCoreApplication::organizationName(),
QCoreApplication::applicationName());
}
return userConfigFilepath;
}
void HifiConfigVariantMap::mergeMapWithJSONFile(QVariantMap& existingMap, const QString& filename) {
QFile configFile(filename);
if (configFile.exists()) {
qDebug() << "Reading JSON config file at" << filename;
configFile.open(QIODevice::ReadOnly);
QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll());
if (existingMap.isEmpty()) {
existingMap = configDocument.toVariant().toMap();
} else {
addMissingValuesToExistingMap(existingMap, configDocument.toVariant().toMap());
}
} else {
qDebug() << "Could not find JSON config file at" << filename;
}
}
void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap) {
foreach(const QString& key, newMap.keys()) {
if (existingMap.contains(key)) {
// if this is just a regular value, we're done - we don't ovveride
if (newMap[key].canConvert(QMetaType::QVariantMap) && existingMap[key].canConvert(QMetaType::QVariantMap)) {
// there's a variant map below and the existing map has one too, so we need to keep recursing
addMissingValuesToExistingMap(*static_cast<QVariantMap*>(existingMap[key].data()), newMap[key].toMap());
}
} else {
existingMap[key] = newMap[key];
}
}
}
const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) {
int dotIndex = keyPath.indexOf('.');
QString firstKey = (dotIndex == -1) ? keyPath : keyPath.mid(0, dotIndex);
if (variantMap.contains(firstKey)) {
if (dotIndex == -1) {
return &variantMap[firstKey];
} else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1));
}
}
return NULL;
}

View file

@ -17,6 +17,13 @@
class HifiConfigVariantMap {
public:
static QVariantMap mergeCLParametersWithJSONConfig(const QStringList& argumentList);
static QVariantMap mergeMasterConfigWithUserConfig(const QStringList& argumentList);
static QString userConfigFilepath(const QStringList& argumentList);
private:
static void mergeMapWithJSONFile(QVariantMap& existingMap, const QString& filename);
static void addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap);
};
const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath);
#endif // hifi_HifiConfigVariantMap_h