зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1153822: Adjust Marionette responses to match WebDriver protocol
Introduce protocol version levels in the Marionette server. On establishing a connection to a local end, the remote will return a `marionetteProtocol` field indicating which level it speaks. The protocol level can be used by local ends to either fall into compatibility mode or warn the user that the local end is incompatible with the remote. The protocol is currently also more expressive than it needs to be and this expressiveness has previously resulted in subtle inconsistencies in the fields returned. This patch reduces the amount of superfluous fields, reducing the amount of data sent. Aligning the protocol closer to the WebDriver specification's expectations will also reduce the amount of post-processing required in the httpd. Previous to this patch, this is a value response: {"from":"0","value":null,"status":0,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}"} And this for ok responses: {"from":"0","ok":true} And this for errors: {"from":"0","status":21,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}","error":{"message":"Error loading page, timed out (onDOMContentLoaded)","stacktrace":null,"status":21}} This patch drops the `from` and `sessionId` fields, and the `status` field from non-error responses. It also drops the `ok` field in non-value responses and flattens the error response to a simple dictionary with the `error` (previously `status`), `message`, and `stacktrace` properties, which are now all required. r=jgriffin --HG-- extra : commitid : FbEkv70rxl9 extra : rebase_source : 3116110a0d197289cc95eba8748be0a33566c5a5
This commit is contained in:
Родитель
17c37f82b5
Коммит
1fe3c441c1
|
@ -6,6 +6,7 @@
|
|||
(?i)(^|/)TAGS$
|
||||
(^|/)ID$
|
||||
(^|/)\.DS_Store$
|
||||
.*\.egg-info
|
||||
|
||||
# Vim swap files.
|
||||
^\.sw.$
|
||||
|
|
|
@ -156,7 +156,7 @@ ActionChain.prototype.resetValues = function() {
|
|||
*/
|
||||
ActionChain.prototype.actions = function(chain, touchId, i, keyModifiers) {
|
||||
if (i == chain.length) {
|
||||
this.onSuccess({value: touchId});
|
||||
this.onSuccess({value: touchId || null});
|
||||
this.resetValues();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -14,94 +14,89 @@ Cu.import("chrome://marionette/content/error.js");
|
|||
this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"];
|
||||
const logger = Log.repository.getLogger("Marionette");
|
||||
|
||||
const validator = {
|
||||
exclusionary: {
|
||||
"capabilities": ["error", "value"],
|
||||
"error": ["value", "sessionId", "capabilities"],
|
||||
"sessionId": ["error", "value"],
|
||||
"value": ["error", "sessionId", "capabilities"],
|
||||
},
|
||||
|
||||
set: function(obj, prop, val) {
|
||||
let tests = this.exclusionary[prop];
|
||||
if (tests) {
|
||||
for (let t of tests) {
|
||||
if (obj.hasOwnProperty(t)) {
|
||||
throw new TypeError(`${t} set, cannot set ${prop}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj[prop] = val;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The response body is exposed as an argument to commands.
|
||||
* Commands can set fields on the body through defining properties.
|
||||
*
|
||||
* Setting properties invokes a validator that performs tests for
|
||||
* mutually exclusionary fields on the input against the existing data
|
||||
* in the body.
|
||||
*
|
||||
* For example setting the {@code error} property on the body when
|
||||
* {@code value}, {@code sessionId}, or {@code capabilities} have been
|
||||
* set previously will cause an error.
|
||||
*/
|
||||
this.ResponseBody = () => new Proxy({}, validator);
|
||||
|
||||
/**
|
||||
* Represents the response returned from the remote end after execution
|
||||
* of its corresponding command.
|
||||
*
|
||||
* The Response is a mutable object passed to each command for
|
||||
* modification through the available setters. The response is sent
|
||||
* implicitly by CommandProcessor when a command is finished executing,
|
||||
* and any modifications made subsequent to this will have no effect.
|
||||
* The response is a mutable object passed to each command for
|
||||
* modification through the available setters. To send data in a response,
|
||||
* you modify the body property on the response. The body property can
|
||||
* also be replaced completely.
|
||||
*
|
||||
* The response is sent implicitly by CommandProcessor when a command
|
||||
* has finished executing, and any modifications made subsequent to that
|
||||
* will have no effect.
|
||||
*
|
||||
* @param {number} cmdId
|
||||
* UUID tied to the corresponding command request this is
|
||||
* a response for.
|
||||
* @param {function(number)} okHandler
|
||||
* Callback function called on successful responses with no body.
|
||||
* @param {function(Object, number)} respHandler
|
||||
* Callback function called on successful responses with body.
|
||||
* @param {Object=} msg
|
||||
* A message to populate the response, containing the properties
|
||||
* "sessionId", "status", and "value".
|
||||
* @param {function(Map)=} sanitizer
|
||||
* Run before sending message.
|
||||
* Callback function called on responses.
|
||||
*/
|
||||
this.Response = function(cmdId, okHandler, respHandler, msg, sanitizer) {
|
||||
const removeEmpty = function(map) {
|
||||
let rv = {};
|
||||
for (let [key, value] of map) {
|
||||
if (typeof value == "undefined") {
|
||||
value = null;
|
||||
}
|
||||
rv[key] = value;
|
||||
}
|
||||
return rv;
|
||||
};
|
||||
|
||||
this.Response = function(cmdId, respHandler) {
|
||||
this.id = cmdId;
|
||||
this.ok = true;
|
||||
this.okHandler = okHandler;
|
||||
this.respHandler = respHandler;
|
||||
this.sanitizer = sanitizer || removeEmpty;
|
||||
|
||||
this.data = new Map([
|
||||
["sessionId", msg.sessionId ? msg.sessionId : null],
|
||||
["status", msg.status ? msg.status : "success"],
|
||||
["value", msg.value ? msg.value : undefined],
|
||||
]);
|
||||
};
|
||||
|
||||
Response.prototype = {
|
||||
get name() { return this.data.get("name"); },
|
||||
set name(n) { this.data.set("name", n); },
|
||||
get sessionId() { return this.data.get("sessionId"); },
|
||||
set sessionId(id) { this.data.set("sessionId", id); },
|
||||
get status() { return this.data.get("status"); },
|
||||
set status(ns) { this.data.set("status", ns); },
|
||||
get value() { return this.data.get("value"); },
|
||||
set value(val) {
|
||||
this.data.set("value", val);
|
||||
this.ok = false;
|
||||
}
|
||||
this.sent = false;
|
||||
this.body = ResponseBody();
|
||||
};
|
||||
|
||||
Response.prototype.send = function() {
|
||||
if (this.sent) {
|
||||
logger.warn("Skipped sending response to command ID " +
|
||||
this.id + " because response has already been sent");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ok) {
|
||||
this.okHandler(this.id);
|
||||
} else {
|
||||
let rawData = this.sanitizer(this.data);
|
||||
this.respHandler(rawData, this.id);
|
||||
throw new RangeError("Response has already been sent: " + this.toString());
|
||||
}
|
||||
this.respHandler(this.body, this.id);
|
||||
this.sent = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {(Error|Object)} err
|
||||
* The error to send, either an instance of the Error prototype,
|
||||
* or an object with the properties "message", "status", and "stack".
|
||||
*/
|
||||
Response.prototype.sendError = function(err) {
|
||||
this.status = "status" in err ? err.status : new UnknownError().status;
|
||||
this.value = error.toJSON(err);
|
||||
let wd = error.isWebDriverError(err);
|
||||
let we = wd ? err : new WebDriverError(err.message);
|
||||
|
||||
this.body.error = we.status;
|
||||
this.body.message = we.message || null;
|
||||
this.body.stacktrace = we.stack || null;
|
||||
|
||||
this.send();
|
||||
|
||||
// propagate errors that are implementation problems
|
||||
if (!error.isWebDriverError(err)) {
|
||||
if (!wd) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
@ -131,17 +126,14 @@ this.CommandProcessor = function(driver) {
|
|||
*
|
||||
* @param {Object} payload
|
||||
* Message as received from client.
|
||||
* @param {function(number)} okHandler
|
||||
* Callback function called on successful responses with no body.
|
||||
* @param {function(Object, number)} respHandler
|
||||
* Callback function called on successful responses with body.
|
||||
* Callback function called on responses.
|
||||
* @param {number} cmdId
|
||||
* The unique identifier for the command to execute.
|
||||
*/
|
||||
CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, cmdId) {
|
||||
CommandProcessor.prototype.execute = function(payload, respHandler, cmdId) {
|
||||
let cmd = payload;
|
||||
let resp = new Response(
|
||||
cmdId, okHandler, respHandler, {sessionId: this.driver.sessionId});
|
||||
let resp = new Response(cmdId, respHandler);
|
||||
let sendResponse = resp.send.bind(resp);
|
||||
let sendError = resp.sendError.bind(resp);
|
||||
|
||||
|
@ -156,7 +148,15 @@ CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, c
|
|||
throw new UnknownCommandError(cmd.name);
|
||||
}
|
||||
|
||||
yield fn.bind(this.driver)(cmd, resp);
|
||||
let rv = yield fn.bind(this.driver)(cmd, resp);
|
||||
|
||||
if (typeof rv != "undefined") {
|
||||
if (typeof rv != "object") {
|
||||
resp.body = {value: rv};
|
||||
} else {
|
||||
resp.body = rv;
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.then(sendResponse, sendError).catch(error.report);
|
||||
|
|
|
@ -17,6 +17,8 @@ Cu.import("chrome://marionette/content/driver.js");
|
|||
|
||||
this.EXPORTED_SYMBOLS = ["Dispatcher"];
|
||||
|
||||
const PROTOCOL_VERSION = 2;
|
||||
|
||||
const logger = Log.repository.getLogger("Marionette");
|
||||
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
|
||||
|
@ -38,12 +40,6 @@ this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
|
|||
this.id = connId;
|
||||
this.conn = transport;
|
||||
|
||||
// Marionette uses a protocol based on the debugger server, which
|
||||
// requires passing back actor ID's with responses. Unlike the debugger
|
||||
// server, we don't actually have multiple actors, so just use a dummy
|
||||
// value of "0".
|
||||
this.actorId = "0";
|
||||
|
||||
// callback for when connection is closed
|
||||
this.onclose = null;
|
||||
|
||||
|
@ -64,20 +60,16 @@ this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
|
|||
* over those defined in this.driver.commands.
|
||||
*/
|
||||
Dispatcher.prototype.onPacket = function(packet) {
|
||||
// Avoid using toSource and template strings (or touching the payload at all
|
||||
// if not necessary) for the sake of memory use.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170
|
||||
if (logger.level <= Log.Level.Debug) {
|
||||
logger.debug(this.id + " -> (" + JSON.stringify(packet) + ")");
|
||||
logger.debug(this.id + " -> " + JSON.stringify(packet));
|
||||
}
|
||||
|
||||
if (this.requests && this.requests[packet.name]) {
|
||||
this.requests[packet.name].bind(this)(packet);
|
||||
} else {
|
||||
let id = this.beginNewCommand();
|
||||
let ok = this.sendOk.bind(this);
|
||||
let send = this.send.bind(this);
|
||||
this.commandProcessor.execute(packet, ok, send, id);
|
||||
this.commandProcessor.execute(packet, send, id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -94,11 +86,6 @@ Dispatcher.prototype.onClosed = function(status) {
|
|||
|
||||
// Dispatcher specific command handlers:
|
||||
|
||||
Dispatcher.prototype.getMarionetteID = function() {
|
||||
let id = this.beginNewCommand();
|
||||
this.sendResponse({from: "root", id: this.actorId}, id);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.emulatorCmdResult = function(msg) {
|
||||
switch (this.driver.context) {
|
||||
case Context.CONTENT:
|
||||
|
@ -122,10 +109,7 @@ Dispatcher.prototype.quitApplication = function(msg) {
|
|||
let id = this.beginNewCommand();
|
||||
|
||||
if (this.driver.appName != "Firefox") {
|
||||
this.sendError({
|
||||
"message": "In app initiated quit only supported on Firefox",
|
||||
"status": "webdriver error",
|
||||
}, id);
|
||||
this.sendError(new WebDriverError("In app initiated quit only supported in Firefox"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -145,63 +129,21 @@ Dispatcher.prototype.quitApplication = function(msg) {
|
|||
|
||||
Dispatcher.prototype.sayHello = function() {
|
||||
let id = this.beginNewCommand();
|
||||
let yo = {from: "root", applicationType: "gecko", traits: []};
|
||||
this.sendResponse(yo, id);
|
||||
let whatHo = {
|
||||
applicationType: "gecko",
|
||||
marionetteProtocol: PROTOCOL_VERSION,
|
||||
};
|
||||
this.send(whatHo, id);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.sendOk = function(cmdId) {
|
||||
this.sendResponse({from: this.actorId, ok: true}, cmdId);
|
||||
this.send({}, cmdId);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.sendError = function(err, cmdId) {
|
||||
let packet = {
|
||||
from: this.actorId,
|
||||
status: err.status,
|
||||
sessionId: this.driver.sessionId,
|
||||
error: err
|
||||
let resp = new Response(cmdId, this.send.bind(this));
|
||||
resp.sendError(err);
|
||||
};
|
||||
this.sendResponse(packet, cmdId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marshals and sends message to either client or emulator based on the
|
||||
* provided {@code cmdId}.
|
||||
*
|
||||
* This routine produces a Marionette protocol packet, which is different
|
||||
* to a WebDriver protocol response in that it contains an extra key
|
||||
* {@code from} for the debugger transport actor ID. It also replaces the
|
||||
* key {@code value} with {@code error} when {@code msg.status} isn't
|
||||
* {@code 0}.
|
||||
*
|
||||
* @param {Object} msg
|
||||
* Object with the properties {@code value}, {@code status}, and
|
||||
* {@code sessionId}.
|
||||
* @param {UUID} cmdId
|
||||
* The unique identifier for the command the message is a response to.
|
||||
*/
|
||||
Dispatcher.prototype.send = function(msg, cmdId) {
|
||||
let packet = {
|
||||
from: this.actorId,
|
||||
value: msg.value,
|
||||
status: msg.status,
|
||||
sessionId: msg.sessionId,
|
||||
};
|
||||
|
||||
if (typeof packet.value == "undefined") {
|
||||
packet.value = null;
|
||||
}
|
||||
|
||||
// the Marionette protocol sends errors using the "error"
|
||||
// key instead of, as Selenium, "value"
|
||||
if (!error.isSuccess(msg.status)) {
|
||||
packet.error = packet.value;
|
||||
delete packet.value;
|
||||
}
|
||||
|
||||
this.sendResponse(packet, cmdId);
|
||||
};
|
||||
|
||||
// Low-level methods:
|
||||
|
||||
/**
|
||||
* Delegates message to client or emulator based on the provided
|
||||
|
@ -220,7 +162,7 @@ Dispatcher.prototype.send = function(msg, cmdId) {
|
|||
* The unique identifier for this payload. {@code -1} signifies
|
||||
* that it's an emulator callback.
|
||||
*/
|
||||
Dispatcher.prototype.sendResponse = function(payload, cmdId) {
|
||||
Dispatcher.prototype.send = function(payload, cmdId) {
|
||||
if (emulator.isCallback(cmdId)) {
|
||||
this.sendToEmulator(payload);
|
||||
} else {
|
||||
|
@ -229,6 +171,8 @@ Dispatcher.prototype.sendResponse = function(payload, cmdId) {
|
|||
}
|
||||
};
|
||||
|
||||
// Low-level methods:
|
||||
|
||||
/**
|
||||
* Send message to emulator over the debugger transport socket.
|
||||
* Notably this skips out-of-sync command checks.
|
||||
|
@ -265,11 +209,8 @@ Dispatcher.prototype.sendToClient = function(payload, cmdId) {
|
|||
* and logs it.
|
||||
*/
|
||||
Dispatcher.prototype.sendRaw = function(dest, payload) {
|
||||
// Avoid using toSource and template strings (or touching the payload at all
|
||||
// if not necessary) for the sake of memory use.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170
|
||||
if (logger.level <= Log.Level.Debug) {
|
||||
logger.debug(this.id + " " + dest + " <- (" + JSON.stringify(payload) + ")");
|
||||
logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload));
|
||||
}
|
||||
this.conn.send(payload);
|
||||
};
|
||||
|
@ -292,7 +233,6 @@ Dispatcher.prototype.isOutOfSync = function(cmdId) {
|
|||
};
|
||||
|
||||
Dispatcher.prototype.requests = {
|
||||
getMarionetteID: Dispatcher.prototype.getMarionetteID,
|
||||
emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult,
|
||||
quitApplication: Dispatcher.prototype.quitApplication
|
||||
};
|
||||
|
|
|
@ -573,8 +573,8 @@ GeckoDriver.prototype.newSession = function(cmd, resp) {
|
|||
yield registerBrowsers;
|
||||
yield browserListening;
|
||||
|
||||
resp.sessionId = this.sessionId;
|
||||
resp.value = this.sessionCapabilities;
|
||||
resp.body.sessionId = this.sessionId;
|
||||
resp.body.capabilities = this.sessionCapabilities;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -589,7 +589,7 @@ GeckoDriver.prototype.newSession = function(cmd, resp) {
|
|||
* numerical or string.
|
||||
*/
|
||||
GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) {
|
||||
resp.value = this.sessionCapabilities;
|
||||
resp.body.capabilities = this.sessionCapabilities;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -721,7 +721,7 @@ GeckoDriver.prototype.log = function(cmd, resp) {
|
|||
|
||||
/** Return all logged messages. */
|
||||
GeckoDriver.prototype.getLogs = function(cmd, resp) {
|
||||
resp.value = this.marionetteLog.getLogs();
|
||||
resp.body = this.marionetteLog.getLogs();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -743,7 +743,7 @@ GeckoDriver.prototype.setContext = function(cmd, resp) {
|
|||
|
||||
/** Gets the context of the server, either "chrome" or "content". */
|
||||
GeckoDriver.prototype.getContext = function(cmd, resp) {
|
||||
resp.value = this.context.toString();
|
||||
resp.body.value = this.context.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -844,7 +844,7 @@ GeckoDriver.prototype.executeScriptInSandbox = function(
|
|||
// It's fine to pass on and modify resp here because
|
||||
// executeScriptInSandbox is the last function to be called
|
||||
// in execute and executeWithCallback respectively.
|
||||
resp.value = this.curBrowser.elementManager.wrapValue(res);
|
||||
resp.body.value = this.curBrowser.elementManager.wrapValue(res);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -873,7 +873,7 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) {
|
|||
}
|
||||
|
||||
if (this.context == Context.CONTENT) {
|
||||
resp.value = yield this.listener.executeScript({
|
||||
resp.body.value = yield this.listener.executeScript({
|
||||
script: script,
|
||||
args: args,
|
||||
newSandbox: newSandbox,
|
||||
|
@ -986,7 +986,7 @@ GeckoDriver.prototype.executeJSScript = function(cmd, resp) {
|
|||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.executeJSScript({
|
||||
resp.body.value = yield this.listener.executeJSScript({
|
||||
script: cmd.parameters.script,
|
||||
args: cmd.parameters.args,
|
||||
newSandbox: cmd.parameters.newSandbox,
|
||||
|
@ -1025,7 +1025,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
|
|||
scriptTimeout,
|
||||
filename,
|
||||
line} = cmd.parameters;
|
||||
let sandboxName = cmd.parameters.sandbox || 'default';
|
||||
let sandboxName = cmd.parameters.sandbox || "default";
|
||||
|
||||
if (!scriptTimeout) {
|
||||
scriptTimeout = this.scriptTimeout;
|
||||
|
@ -1035,7 +1035,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
|
|||
}
|
||||
|
||||
if (this.context == Context.CONTENT) {
|
||||
resp.value = yield this.listener.executeAsyncScript({
|
||||
resp.body.value = yield this.listener.executeAsyncScript({
|
||||
script: script,
|
||||
args: args,
|
||||
id: cmd.id,
|
||||
|
@ -1184,7 +1184,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
|
|||
}
|
||||
}.bind(this));
|
||||
|
||||
resp.value = that.curBrowser.elementManager.wrapValue(res);
|
||||
resp.body.value = that.curBrowser.elementManager.wrapValue(res) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1282,15 +1282,15 @@ GeckoDriver.prototype.pageLoadPromise = function() {
|
|||
* When in the context of the chrome, this returns the canonical URL
|
||||
* of the current resource.
|
||||
*/
|
||||
GeckoDriver.prototype.getCurrentUrl = function(cmd, resp) {
|
||||
GeckoDriver.prototype.getCurrentUrl = function(cmd) {
|
||||
switch (this.context) {
|
||||
case Context.CHROME:
|
||||
resp.value = this.getCurrentWindow().location.href;
|
||||
return this.getCurrentWindow().location.href;
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
let isB2G = this.appName == "B2G";
|
||||
resp.value = yield this.listener.getCurrentUrl(isB2G);
|
||||
return this.listener.getCurrentUrl(isB2G);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1300,11 +1300,11 @@ GeckoDriver.prototype.getTitle = function(cmd, resp) {
|
|||
switch (this.context) {
|
||||
case Context.CHROME:
|
||||
let win = this.getCurrentWindow();
|
||||
resp.value = win.document.documentElement.getAttribute("title");
|
||||
resp.body.value = win.document.documentElement.getAttribute("title");
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getTitle();
|
||||
resp.body.value = yield this.listener.getTitle();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1312,7 +1312,7 @@ GeckoDriver.prototype.getTitle = function(cmd, resp) {
|
|||
/** Gets the current type of the window. */
|
||||
GeckoDriver.prototype.getWindowType = function(cmd, resp) {
|
||||
let win = this.getCurrentWindow();
|
||||
resp.value = win.document.documentElement.getAttribute("windowtype");
|
||||
resp.body.value = win.document.documentElement.getAttribute("windowtype");
|
||||
};
|
||||
|
||||
/** Gets the page source of the content document. */
|
||||
|
@ -1321,11 +1321,11 @@ GeckoDriver.prototype.getPageSource = function(cmd, resp) {
|
|||
case Context.CHROME:
|
||||
let win = this.getCurrentWindow();
|
||||
let s = new win.XMLSerializer();
|
||||
resp.value = s.serializeToString(win.document);
|
||||
resp.body.value = s.serializeToString(win.document);
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getPageSource();
|
||||
resp.body.value = yield this.listener.getPageSource();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1359,13 +1359,13 @@ GeckoDriver.prototype.refresh = function(cmd, resp) {
|
|||
GeckoDriver.prototype.getWindowHandle = function(cmd, resp) {
|
||||
// curFrameId always holds the current tab.
|
||||
if (this.curBrowser.curFrameId && this.appName != "B2G") {
|
||||
resp.value = this.curBrowser.curFrameId;
|
||||
resp.body.value = this.curBrowser.curFrameId;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in this.browsers) {
|
||||
if (this.curBrowser == this.browsers[i]) {
|
||||
resp.value = i;
|
||||
resp.body.value = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1412,7 +1412,7 @@ GeckoDriver.prototype.getIdForBrowser = function getIdForBrowser(browser) {
|
|||
* Unique window handles.
|
||||
*/
|
||||
GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
|
||||
let rv = [];
|
||||
let hs = [];
|
||||
let winEn = this.getWinEnumerator();
|
||||
while (winEn.hasMoreElements()) {
|
||||
let win = winEn.getNext();
|
||||
|
@ -1421,7 +1421,7 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
|
|||
for (let i = 0; i < tabbrowser.browsers.length; ++i) {
|
||||
let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i));
|
||||
if (winId !== null) {
|
||||
rv.push(winId);
|
||||
hs.push(winId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -1430,10 +1430,10 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
|
|||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
winId += (this.appName == "B2G") ? "-b2g" : "";
|
||||
rv.push(winId);
|
||||
hs.push(winId);
|
||||
}
|
||||
}
|
||||
resp.value = rv;
|
||||
resp.body = hs;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1450,7 +1450,7 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
|
|||
GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) {
|
||||
for (let i in this.browsers) {
|
||||
if (this.curBrowser == this.browsers[i]) {
|
||||
resp.value = i;
|
||||
resp.body.value = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1464,7 +1464,7 @@ GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) {
|
|||
* Unique window handles.
|
||||
*/
|
||||
GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) {
|
||||
let rv = [];
|
||||
let hs = [];
|
||||
let winEn = this.getWinEnumerator();
|
||||
while (winEn.hasMoreElements()) {
|
||||
let foundWin = winEn.getNext();
|
||||
|
@ -1472,9 +1472,9 @@ GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) {
|
|||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
winId = winId + ((this.appName == "B2G") ? "-b2g" : "");
|
||||
rv.push(winId);
|
||||
hs.push(winId);
|
||||
}
|
||||
resp.value = rv;
|
||||
resp.body = hs;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1485,7 +1485,8 @@ GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) {
|
|||
*/
|
||||
GeckoDriver.prototype.getWindowPosition = function(cmd, resp) {
|
||||
let win = this.getCurrentWindow();
|
||||
resp.value = {x: win.screenX, y: win.screenY};
|
||||
resp.body.x = win.screenX;
|
||||
resp.body.y = win.screenY;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1600,15 +1601,15 @@ GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
|
|||
switch (this.context) {
|
||||
case Context.CHROME:
|
||||
// no frame means top-level
|
||||
resp.value = null;
|
||||
resp.body.value = null;
|
||||
if (this.curFrame) {
|
||||
resp.value = this.curBrowser.elementManager
|
||||
resp.body.value = this.curBrowser.elementManager
|
||||
.addToKnownElements(this.curFrame.frameElement);
|
||||
}
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = this.currentFrameElement;
|
||||
resp.body.value = this.currentFrameElement;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1833,7 +1834,7 @@ GeckoDriver.prototype.actionChain = function(cmd, resp) {
|
|||
}
|
||||
|
||||
let cbs = {};
|
||||
cbs.onSuccess = val => resp.value = val;
|
||||
cbs.onSuccess = val => resp.body.value = val;
|
||||
cbs.onError = err => { throw err; };
|
||||
|
||||
let win = this.getCurrentWindow();
|
||||
|
@ -1843,7 +1844,7 @@ GeckoDriver.prototype.actionChain = function(cmd, resp) {
|
|||
|
||||
case Context.CONTENT:
|
||||
this.addFrameCloseListener("action chain");
|
||||
resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
|
||||
resp.body.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -1880,7 +1881,7 @@ GeckoDriver.prototype.multiAction = function(cmd, resp) {
|
|||
GeckoDriver.prototype.findElement = function(cmd, resp) {
|
||||
switch (this.context) {
|
||||
case Context.CHROME:
|
||||
resp.value = yield new Promise((resolve, reject) => {
|
||||
resp.body.value = yield new Promise((resolve, reject) => {
|
||||
let win = this.getCurrentWindow();
|
||||
this.curBrowser.elementManager.find(
|
||||
{ frame: win },
|
||||
|
@ -1893,7 +1894,7 @@ GeckoDriver.prototype.findElement = function(cmd, resp) {
|
|||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.findElementContent({
|
||||
resp.body.value = yield this.listener.findElementContent({
|
||||
value: cmd.parameters.value,
|
||||
using: cmd.parameters.using,
|
||||
element: cmd.parameters.element,
|
||||
|
@ -1914,7 +1915,7 @@ GeckoDriver.prototype.findElement = function(cmd, resp) {
|
|||
* Value of the element to start from.
|
||||
*/
|
||||
GeckoDriver.prototype.findChildElement = function(cmd, resp) {
|
||||
resp.value = yield this.listener.findElementContent({
|
||||
resp.body.value = yield this.listener.findElementContent({
|
||||
value: cmd.parameters.value,
|
||||
using: cmd.parameters.using,
|
||||
element: cmd.parameters.id,
|
||||
|
@ -1932,7 +1933,7 @@ GeckoDriver.prototype.findChildElement = function(cmd, resp) {
|
|||
GeckoDriver.prototype.findElements = function(cmd, resp) {
|
||||
switch (this.context) {
|
||||
case Context.CHROME:
|
||||
resp.value = yield new Promise((resolve, reject) => {
|
||||
resp.body = yield new Promise((resolve, reject) => {
|
||||
let win = this.getCurrentWindow();
|
||||
this.curBrowser.elementManager.find(
|
||||
{ frame: win },
|
||||
|
@ -1945,7 +1946,7 @@ GeckoDriver.prototype.findElements = function(cmd, resp) {
|
|||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.findElementsContent({
|
||||
resp.body = yield this.listener.findElementsContent({
|
||||
value: cmd.parameters.value,
|
||||
using: cmd.parameters.using,
|
||||
element: cmd.parameters.element,
|
||||
|
@ -1966,7 +1967,7 @@ GeckoDriver.prototype.findElements = function(cmd, resp) {
|
|||
* Value of the element to start from.
|
||||
*/
|
||||
GeckoDriver.prototype.findChildElements = function(cmd, resp) {
|
||||
resp.value = yield this.listener.findElementsContent({
|
||||
resp.body.value = yield this.listener.findElementsContent({
|
||||
value: cmd.parameters.value,
|
||||
using: cmd.parameters.using,
|
||||
element: cmd.parameters.id,
|
||||
|
@ -1975,7 +1976,7 @@ GeckoDriver.prototype.findChildElements = function(cmd, resp) {
|
|||
|
||||
/** Return the active element on the page. */
|
||||
GeckoDriver.prototype.getActiveElement = function(cmd, resp) {
|
||||
resp.value = yield this.listener.getActiveElement();
|
||||
resp.body.value = yield this.listener.getActiveElement();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2021,11 +2022,11 @@ GeckoDriver.prototype.getElementAttribute = function(cmd, resp) {
|
|||
case Context.CHROME:
|
||||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
|
||||
resp.value = utils.getElementAttribute(el, name);
|
||||
resp.body.value = utils.getElementAttribute(el, name);
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementAttribute(id, name);
|
||||
resp.body.value = yield this.listener.getElementAttribute(id, name);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2047,11 +2048,11 @@ GeckoDriver.prototype.getElementText = function(cmd, resp) {
|
|||
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
|
||||
let lines = [];
|
||||
this.getVisibleText(el, lines);
|
||||
resp.value = lines.join("\n");
|
||||
resp.body.value = lines.join("\n");
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementText(id);
|
||||
resp.body.value = yield this.listener.getElementText(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2069,11 +2070,11 @@ GeckoDriver.prototype.getElementTagName = function(cmd, resp) {
|
|||
case Context.CHROME:
|
||||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
|
||||
resp.value = el.tagName.toLowerCase();
|
||||
resp.body.value = el.tagName.toLowerCase();
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementTagName(id);
|
||||
resp.body.value = yield this.listener.getElementTagName(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2091,11 +2092,11 @@ GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) {
|
|||
case Context.CHROME:
|
||||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
|
||||
resp.value = utils.isElementDisplayed(el);
|
||||
resp.body.value = utils.isElementDisplayed(el);
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.isElementDisplayed(id);
|
||||
resp.body.value = yield this.listener.isElementDisplayed(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2116,11 +2117,11 @@ GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) {
|
|||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
|
||||
let sty = win.document.defaultView.getComputedStyle(el, null);
|
||||
resp.value = sty.getPropertyValue(prop);
|
||||
resp.body.value = sty.getPropertyValue(prop);
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementValueOfCssProperty(id, prop);
|
||||
resp.body.value = yield this.listener.getElementValueOfCssProperty(id, prop);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2139,11 +2140,11 @@ GeckoDriver.prototype.isElementEnabled = function(cmd, resp) {
|
|||
// Selenium atom doesn't quite work here
|
||||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
|
||||
resp.value = !(!!el.disabled);
|
||||
resp.body.value = !(!!el.disabled);
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.isElementEnabled(id);
|
||||
resp.body.value = yield this.listener.isElementEnabled(id);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -2163,16 +2164,16 @@ GeckoDriver.prototype.isElementSelected = function(cmd, resp) {
|
|||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
|
||||
if (typeof el.checked != "undefined") {
|
||||
resp.value = !!el.checked;
|
||||
resp.body.value = !!el.checked;
|
||||
} else if (typeof el.selected != "undefined") {
|
||||
resp.value = !!el.selected;
|
||||
resp.body.value = !!el.selected;
|
||||
} else {
|
||||
resp.value = true;
|
||||
resp.body.value = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.isElementSelected(id);
|
||||
resp.body.value = yield this.listener.isElementSelected(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2185,11 +2186,12 @@ GeckoDriver.prototype.getElementSize = function(cmd, resp) {
|
|||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
|
||||
let rect = el.getBoundingClientRect();
|
||||
resp.value = {width: rect.width, height: rect.height};
|
||||
resp.body.width = rect.width;
|
||||
resp.body.height = rect.height;
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementSize(id);
|
||||
resp.body = yield this.listener.getElementSize(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2202,7 +2204,7 @@ GeckoDriver.prototype.getElementRect = function(cmd, resp) {
|
|||
let win = this.getCurrentWindow();
|
||||
let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
|
||||
let rect = el.getBoundingClientRect();
|
||||
resp.value = {
|
||||
resp.body = {
|
||||
x: rect.x + win.pageXOffset,
|
||||
y: rect.y + win.pageYOffset,
|
||||
width: rect.width,
|
||||
|
@ -2211,7 +2213,7 @@ GeckoDriver.prototype.getElementRect = function(cmd, resp) {
|
|||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.getElementRect(id);
|
||||
resp.body = yield this.listener.getElementRect(id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -2351,7 +2353,7 @@ GeckoDriver.prototype.addCookie = function(cmd, resp) {
|
|||
* the result.
|
||||
*/
|
||||
GeckoDriver.prototype.getCookies = function(cmd, resp) {
|
||||
resp.value = yield this.listener.getCookies();
|
||||
resp.body = yield this.listener.getCookies();
|
||||
};
|
||||
|
||||
/** Delete all cookies that are visible to a document. */
|
||||
|
@ -2515,7 +2517,7 @@ GeckoDriver.prototype.deleteSession = function(cmd, resp) {
|
|||
|
||||
/** Returns the current status of the Application Cache. */
|
||||
GeckoDriver.prototype.getAppCacheStatus = function(cmd, resp) {
|
||||
resp.value = yield this.listener.getAppCacheStatus();
|
||||
resp.body.value = yield this.listener.getAppCacheStatus();
|
||||
};
|
||||
|
||||
GeckoDriver.prototype.importScript = function(cmd, resp) {
|
||||
|
@ -2633,11 +2635,11 @@ GeckoDriver.prototype.takeScreenshot = function(cmd, resp) {
|
|||
context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags);
|
||||
let dataUrl = canvas.toDataURL("image/png", "");
|
||||
let data = dataUrl.substring(dataUrl.indexOf(",") + 1);
|
||||
resp.value = data;
|
||||
resp.body.value = data;
|
||||
break;
|
||||
|
||||
case Context.CONTENT:
|
||||
resp.value = yield this.listener.takeScreenshot({
|
||||
resp.body.value = yield this.listener.takeScreenshot({
|
||||
id: cmd.parameters.id,
|
||||
highlights: cmd.parameters.highlights,
|
||||
full: cmd.parameters.full});
|
||||
|
@ -2653,7 +2655,7 @@ GeckoDriver.prototype.takeScreenshot = function(cmd, resp) {
|
|||
* landscape-secondary.
|
||||
*/
|
||||
GeckoDriver.prototype.getScreenOrientation = function(cmd, resp) {
|
||||
resp.value = this.getCurrentWindow().screen.mozOrientation;
|
||||
resp.body.value = this.getCurrentWindow().screen.mozOrientation;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2695,7 +2697,8 @@ GeckoDriver.prototype.setScreenOrientation = function(cmd, resp) {
|
|||
*/
|
||||
GeckoDriver.prototype.getWindowSize = function(cmd, resp) {
|
||||
let win = this.getCurrentWindow();
|
||||
resp.value = {width: win.outerWidth, height: win.outerHeight};
|
||||
resp.body.width = win.outerWidth;
|
||||
resp.body.height = win.outerHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -2781,7 +2784,7 @@ GeckoDriver.prototype.getTextFromDialog = function(cmd, resp) {
|
|||
}
|
||||
|
||||
let {infoBody} = this.dialog.ui;
|
||||
resp.value = infoBody.textContent;
|
||||
resp.body.value = infoBody.textContent;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -53,32 +53,6 @@ const XPCOM_EXCEPTIONS = [];
|
|||
|
||||
this.error = {};
|
||||
|
||||
/**
|
||||
* Marshals an error object into a WebDriver protocol error. The given
|
||||
* error can be a prototypal Error or an object, as long as it has the
|
||||
* properties message, stack, and status.
|
||||
*
|
||||
* If err is a native JavaScript error, the returned object's message
|
||||
* property will be changed to include the error's name.
|
||||
*
|
||||
* @param {Object} err
|
||||
* Object with the properties message, stack, and status.
|
||||
*
|
||||
* @return {Object}
|
||||
* Object with the properties message, stacktrace, and status.
|
||||
*/
|
||||
error.toJSON = function(err) {
|
||||
let msg = err.message;
|
||||
if (!error.isWebDriverError(err) && "name" in error) {
|
||||
msg = `${err.name}: ${msg}`;
|
||||
}
|
||||
return {
|
||||
message: msg,
|
||||
stacktrace: err.stack || null,
|
||||
status: err.status
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the given status is successful.
|
||||
*/
|
||||
|
@ -110,7 +84,7 @@ error.isError = function(val) {
|
|||
*/
|
||||
error.isWebDriverError = function(obj) {
|
||||
return error.isError(obj) &&
|
||||
("name" in obj && errors.indexOf(obj.name) > 0);
|
||||
("name" in obj && errors.indexOf(obj.name) >= 0);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -213,6 +213,7 @@ let clearElementFn = dispatch(clearElement);
|
|||
let isElementDisplayedFn = dispatch(isElementDisplayed);
|
||||
let getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
|
||||
let switchToShadowRootFn = dispatch(switchToShadowRoot);
|
||||
let getCookiesFn = dispatch(getCookies);
|
||||
|
||||
/**
|
||||
* Start all message listeners
|
||||
|
@ -261,7 +262,7 @@ function startListeners() {
|
|||
addMessageListenerId("Marionette:setTestName", setTestName);
|
||||
addMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
|
||||
addMessageListenerId("Marionette:addCookie", addCookie);
|
||||
addMessageListenerId("Marionette:getCookies", getCookies);
|
||||
addMessageListenerId("Marionette:getCookies", getCookiesFn);
|
||||
addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
|
||||
addMessageListenerId("Marionette:deleteCookie", deleteCookie);
|
||||
}
|
||||
|
@ -367,7 +368,7 @@ function deleteSession(msg) {
|
|||
removeMessageListenerId("Marionette:setTestName", setTestName);
|
||||
removeMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
|
||||
removeMessageListenerId("Marionette:addCookie", addCookie);
|
||||
removeMessageListenerId("Marionette:getCookies", getCookies);
|
||||
removeMessageListenerId("Marionette:getCookies", getCookiesFn);
|
||||
removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
|
||||
removeMessageListenerId("Marionette:deleteCookie", deleteCookie);
|
||||
if (isB2G) {
|
||||
|
@ -1878,17 +1879,20 @@ function addCookie(msg) {
|
|||
/**
|
||||
* Get all cookies for the current domain.
|
||||
*/
|
||||
function getCookies(msg) {
|
||||
var toReturn = [];
|
||||
var cookies = getVisibleCookies(curContainer.frame.location);
|
||||
function getCookies() {
|
||||
let rv = [];
|
||||
let cookies = getVisibleCookies(curContainer.frame.location);
|
||||
|
||||
for (let cookie of cookies) {
|
||||
var expires = cookie.expires;
|
||||
if (expires == 0) { // Session cookie, don't return an expiry.
|
||||
let expires = cookie.expires;
|
||||
// session cookie, don't return an expiry
|
||||
if (expires == 0) {
|
||||
expires = null;
|
||||
} else if (expires == 1) { // Date before epoch time, cap to epoch.
|
||||
// date before epoch time, cap to epoch
|
||||
} else if (expires == 1) {
|
||||
expires = 0;
|
||||
}
|
||||
toReturn.push({
|
||||
rv.push({
|
||||
'name': cookie.name,
|
||||
'value': cookie.value,
|
||||
'path': cookie.path,
|
||||
|
@ -1898,7 +1902,8 @@ function getCookies(msg) {
|
|||
'expiry': expires
|
||||
});
|
||||
}
|
||||
sendResponse({value: toReturn}, msg.json.command_id);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Загрузка…
Ссылка в новой задаче