mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-04 19:33:18 +00:00
647 lines
19 KiB
JavaScript
647 lines
19 KiB
JavaScript
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim:set ts=2 sw=2 sts=2 et: */
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is MozJSHTTP code.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Jeff Walden <jwalden+code@mit.edu>.
|
|
* Portions created by the Initial Developer are Copyright (C) 2006
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Robert Sayre <sayrer@gmail.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
// Note that the server script itself already defines Cc, Ci, and Cr for us,
|
|
// and because they're constants it's not safe to redefine them. Scope leakage
|
|
// sucks.
|
|
|
|
// Disable automatic network detection, so tests work correctly when
|
|
// not connected to a network.
|
|
let (ios = Cc["@mozilla.org/network/io-service;1"]
|
|
.getService(Ci.nsIIOService2)) {
|
|
ios.manageOfflineStatus = false;
|
|
ios.offline = false;
|
|
}
|
|
|
|
const SERVER_PORT = 8888;
|
|
var server; // for use in the shutdown handler, if necessary
|
|
|
|
//
|
|
// HTML GENERATION
|
|
//
|
|
var tags = ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'APPLET', 'AREA', 'B', 'BASE',
|
|
'BASEFONT', 'BDO', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'BUTTON',
|
|
'CAPTION', 'CENTER', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD',
|
|
'DEL', 'DFN', 'DIR', 'DIV', 'DL', 'DT', 'EM', 'FIELDSET', 'FONT',
|
|
'FORM', 'FRAME', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
'HEAD', 'HR', 'HTML', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS',
|
|
'ISINDEX', 'KBD', 'LABEL', 'LEGEND', 'LI', 'LINK', 'MAP', 'MENU',
|
|
'META', 'NOFRAMES', 'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP',
|
|
'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', 'SCRIPT',
|
|
'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'STYLE', 'SUB',
|
|
'SUP', 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD',
|
|
'TITLE', 'TR', 'TT', 'U', 'UL', 'VAR'];
|
|
|
|
/**
|
|
* Below, we'll use makeTagFunc to create a function for each of the
|
|
* strings in 'tags'. This will allow us to use s-expression like syntax
|
|
* to create HTML.
|
|
*/
|
|
function makeTagFunc(tagName)
|
|
{
|
|
return function (attrs /* rest... */)
|
|
{
|
|
var startChildren = 0;
|
|
var response = "";
|
|
|
|
// write the start tag and attributes
|
|
response += "<" + tagName;
|
|
// if attr is an object, write attributes
|
|
if (attrs && typeof attrs == 'object') {
|
|
startChildren = 1;
|
|
for (var [key,value] in attrs) {
|
|
var val = "" + value;
|
|
response += " " + key + '="' + val.replace('"','"') + '"';
|
|
}
|
|
}
|
|
response += ">";
|
|
|
|
// iterate through the rest of the args
|
|
for (var i = startChildren; i < arguments.length; i++) {
|
|
if (typeof arguments[i] == 'function') {
|
|
response += arguments[i]();
|
|
} else {
|
|
response += arguments[i];
|
|
}
|
|
}
|
|
|
|
// write the close tag
|
|
response += "</" + tagName + ">\n";
|
|
return response;
|
|
}
|
|
}
|
|
|
|
function makeTags() {
|
|
// map our global HTML generation functions
|
|
for each (var tag in tags) {
|
|
this[tag] = makeTagFunc(tag.toLowerCase());
|
|
}
|
|
}
|
|
|
|
var _quitting = false;
|
|
|
|
/** Quit when all activity has completed. */
|
|
function serverStopped()
|
|
{
|
|
_quitting = true;
|
|
}
|
|
|
|
// only run the "main" section if httpd.js was loaded ahead of us
|
|
if (this["nsHttpServer"]) {
|
|
//
|
|
// SCRIPT CODE
|
|
//
|
|
runServer();
|
|
|
|
// We can only have gotten here if the /server/shutdown path was requested.
|
|
if (_quitting)
|
|
{
|
|
dumpn("HTTP server stopped, all pending requests complete");
|
|
quit(0);
|
|
}
|
|
|
|
// Impossible as the stop callback should have been called, but to be safe...
|
|
dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
|
|
quit(1);
|
|
}
|
|
|
|
var serverBasePath;
|
|
|
|
//
|
|
// SERVER SETUP
|
|
//
|
|
function runServer()
|
|
{
|
|
serverBasePath = __LOCATION__.parent;
|
|
server = createMochitestServer(serverBasePath);
|
|
server.start(SERVER_PORT);
|
|
|
|
// touch a file in the profile directory to indicate we're alive
|
|
var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
|
|
.createInstance(Ci.nsIFileOutputStream);
|
|
var serverAlive = Cc["@mozilla.org/file/local;1"]
|
|
.createInstance(Ci.nsILocalFile);
|
|
serverAlive.initWithFile(serverBasePath);
|
|
serverAlive.append("mochitesttestingprofile");
|
|
|
|
// If we're running outside of the test harness, there might
|
|
// not be a test profile directory present
|
|
if (serverAlive.exists()) {
|
|
serverAlive.append("server_alive.txt");
|
|
foStream.init(serverAlive,
|
|
0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate
|
|
data = "It's alive!";
|
|
foStream.write(data, data.length);
|
|
foStream.close();
|
|
}
|
|
|
|
makeTags();
|
|
|
|
//
|
|
// The following is threading magic to spin an event loop -- this has to
|
|
// happen manually in xpcshell for the server to actually work.
|
|
//
|
|
var thread = Cc["@mozilla.org/thread-manager;1"]
|
|
.getService()
|
|
.currentThread;
|
|
while (!server.isStopped())
|
|
thread.processNextEvent(true);
|
|
|
|
// Server stopped by /server/shutdown handler -- go through pending events
|
|
// and return.
|
|
|
|
// get rid of any pending requests
|
|
while (thread.hasPendingEvents())
|
|
thread.processNextEvent(true);
|
|
}
|
|
|
|
/** Creates and returns an HTTP server configured to serve Mochitests. */
|
|
function createMochitestServer(serverBasePath)
|
|
{
|
|
var server = new nsHttpServer();
|
|
|
|
server.registerDirectory("/", serverBasePath);
|
|
server.registerPathHandler("/server/shutdown", serverShutdown);
|
|
server.registerPathHandler("/server/debug", serverDebug);
|
|
server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
|
|
server.registerContentType("jar", "application/x-jar");
|
|
server.registerContentType("ogg", "application/ogg");
|
|
server.registerContentType("ogv", "video/ogg");
|
|
server.registerContentType("oga", "audio/ogg");
|
|
server.setIndexHandler(defaultDirHandler);
|
|
|
|
var serverRoot =
|
|
{
|
|
getFile: function getFile(path)
|
|
{
|
|
var file = serverBasePath.clone().QueryInterface(Ci.nsILocalFile);
|
|
path.split("/").forEach(function(p) {
|
|
file.appendRelativePath(p);
|
|
});
|
|
return file;
|
|
},
|
|
QueryInterface: function(aIID) { return this; }
|
|
};
|
|
|
|
server.setObjectState("SERVER_ROOT", serverRoot);
|
|
|
|
processLocations(server);
|
|
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Notifies the HTTP server about all the locations at which it might receive
|
|
* requests, so that it can properly respond to requests on any of the hosts it
|
|
* serves.
|
|
*/
|
|
function processLocations(server)
|
|
{
|
|
var serverLocations = serverBasePath.clone();
|
|
serverLocations.append("server-locations.txt");
|
|
|
|
const PR_RDONLY = 0x01;
|
|
var fis = new FileInputStream(serverLocations, PR_RDONLY, 0444,
|
|
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
|
|
|
var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
|
|
lis.QueryInterface(Ci.nsIUnicharLineInputStream);
|
|
|
|
const LINE_REGEXP =
|
|
new RegExp("^([a-z][-a-z0-9+.]*)" +
|
|
"://" +
|
|
"(" +
|
|
"\\d+\\.\\d+\\.\\d+\\.\\d+" +
|
|
"|" +
|
|
"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
|
|
"[a-z](?:[-a-z0-9]*[a-z0-9])?" +
|
|
")" +
|
|
":" +
|
|
"(\\d+)" +
|
|
"(?:" +
|
|
"\\s+" +
|
|
"(\\S+(?:,\\S+)*)" +
|
|
")?$");
|
|
|
|
var line = {};
|
|
var lineno = 0;
|
|
var seenPrimary = false;
|
|
do
|
|
{
|
|
var more = lis.readLine(line);
|
|
lineno++;
|
|
|
|
var lineValue = line.value;
|
|
if (lineValue.charAt(0) == "#" || lineValue == "")
|
|
continue;
|
|
|
|
var match = LINE_REGEXP.exec(lineValue);
|
|
if (!match)
|
|
throw "Syntax error in server-locations.txt, line " + lineno;
|
|
|
|
var [, scheme, host, port, options] = match;
|
|
if (options)
|
|
{
|
|
if (options.split(",").indexOf("primary") >= 0)
|
|
{
|
|
if (seenPrimary)
|
|
{
|
|
throw "Multiple primary locations in server-locations.txt, " +
|
|
"line " + lineno;
|
|
}
|
|
|
|
server.identity.setPrimary(scheme, host, port);
|
|
seenPrimary = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
server.identity.add(scheme, host, port);
|
|
}
|
|
while (more);
|
|
}
|
|
|
|
|
|
// PATH HANDLERS
|
|
|
|
// /server/shutdown
|
|
function serverShutdown(metadata, response)
|
|
{
|
|
response.setStatusLine("1.1", 200, "OK");
|
|
response.setHeader("Content-type", "text/plain", false);
|
|
|
|
var body = "Server shut down.";
|
|
response.bodyOutputStream.write(body, body.length);
|
|
|
|
dumpn("Server shutting down now...");
|
|
server.stop(serverStopped);
|
|
}
|
|
|
|
// /server/debug?[012]
|
|
function serverDebug(metadata, response)
|
|
{
|
|
response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
|
|
if (metadata.queryString.length !== 1)
|
|
return;
|
|
|
|
var mode;
|
|
if (metadata.queryString === "0") {
|
|
// do this now so it gets logged with the old mode
|
|
dumpn("Server debug logs disabled.");
|
|
DEBUG = false;
|
|
DEBUG_TIMESTAMP = false;
|
|
mode = "disabled";
|
|
} else if (metadata.queryString === "1") {
|
|
DEBUG = true;
|
|
DEBUG_TIMESTAMP = false;
|
|
mode = "enabled";
|
|
} else if (metadata.queryString === "2") {
|
|
DEBUG = true;
|
|
DEBUG_TIMESTAMP = true;
|
|
mode = "enabled, with timestamps";
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
|
response.setHeader("Content-type", "text/plain", false);
|
|
var body = "Server debug logs " + mode + ".";
|
|
response.bodyOutputStream.write(body, body.length);
|
|
dumpn(body);
|
|
}
|
|
|
|
//
|
|
// DIRECTORY LISTINGS
|
|
//
|
|
|
|
/**
|
|
* Creates a generator that iterates over the contents of
|
|
* an nsIFile directory.
|
|
*/
|
|
function dirIter(dir)
|
|
{
|
|
var en = dir.directoryEntries;
|
|
while (en.hasMoreElements()) {
|
|
var file = en.getNext();
|
|
yield file.QueryInterface(Ci.nsILocalFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds an optionally nested object containing links to the
|
|
* files and directories within dir.
|
|
*/
|
|
function list(requestPath, directory, recurse)
|
|
{
|
|
var count = 0;
|
|
var path = requestPath;
|
|
if (path.charAt(path.length - 1) != "/") {
|
|
path += "/";
|
|
}
|
|
|
|
var dir = directory.QueryInterface(Ci.nsIFile);
|
|
var links = {};
|
|
|
|
// The SimpleTest directory is hidden
|
|
var files = [file for (file in dirIter(dir))
|
|
if (file.exists() && file.path.indexOf("SimpleTest") == -1)];
|
|
|
|
// Sort files by name, so that tests can be run in a pre-defined order inside
|
|
// a given directory (see bug 384823)
|
|
function leafNameComparator(first, second) {
|
|
if (first.leafName < second.leafName)
|
|
return -1;
|
|
if (first.leafName > second.leafName)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
files.sort(leafNameComparator);
|
|
|
|
count = files.length;
|
|
for each (var file in files) {
|
|
var key = path + file.leafName;
|
|
var childCount = 0;
|
|
if (file.isDirectory()) {
|
|
key += "/";
|
|
}
|
|
if (recurse && file.isDirectory()) {
|
|
[links[key], childCount] = list(key, file, recurse);
|
|
count += childCount;
|
|
} else {
|
|
if (file.leafName.charAt(0) != '.') {
|
|
links[key] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [links, count];
|
|
}
|
|
|
|
/**
|
|
* Heuristic function that determines whether a given path
|
|
* is a test case to be executed in the harness, or just
|
|
* a supporting file.
|
|
*/
|
|
function isTest(filename, pattern)
|
|
{
|
|
if (pattern)
|
|
return pattern.test(filename);
|
|
|
|
return filename.indexOf("test_") > -1 &&
|
|
filename.indexOf(".js") == -1 &&
|
|
filename.indexOf(".css") == -1 &&
|
|
!/\^headers\^$/.test(filename);
|
|
}
|
|
|
|
/**
|
|
* Transform nested hashtables of paths to nested HTML lists.
|
|
*/
|
|
function linksToListItems(links)
|
|
{
|
|
var response = "";
|
|
var children = "";
|
|
for (var [link, value] in links) {
|
|
var classVal = (!isTest(link) && !(value instanceof Object))
|
|
? "non-test invisible"
|
|
: "test";
|
|
if (value instanceof Object) {
|
|
children = UL({class: "testdir"}, linksToListItems(value));
|
|
} else {
|
|
children = "";
|
|
}
|
|
|
|
var bug_title = link.match(/test_bug\S+/);
|
|
var bug_num = null;
|
|
if (bug_title != null) {
|
|
bug_num = bug_title[0].match(/\d+/);
|
|
}
|
|
|
|
if ((bug_title == null) || (bug_num == null)) {
|
|
response += LI({class: classVal}, A({href: link}, link), children);
|
|
} else {
|
|
var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id="+bug_num;
|
|
response += LI({class: classVal}, A({href: link}, link), " - ", A({href: bug_url}, "Bug "+bug_num), children);
|
|
}
|
|
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Transform nested hashtables of paths to a flat table rows.
|
|
*/
|
|
function linksToTableRows(links, recursionLevel)
|
|
{
|
|
var response = "";
|
|
for (var [link, value] in links) {
|
|
var classVal = (!isTest(link) && !(value instanceof Object))
|
|
? "non-test invisible"
|
|
: "";
|
|
|
|
spacer = "padding-left: " + (10 * recursionLevel) + "px";
|
|
|
|
if (value instanceof Object) {
|
|
response += TR({class: "dir", id: "tr-" + link },
|
|
TD({colspan: "3"}, " "),
|
|
TD({style: spacer},
|
|
A({href: link}, link)));
|
|
response += linksToTableRows(value, recursionLevel + 1);
|
|
} else {
|
|
var bug_title = link.match(/test_bug\S+/);
|
|
var bug_num = null;
|
|
if (bug_title != null) {
|
|
bug_num = bug_title[0].match(/\d+/);
|
|
}
|
|
if ((bug_title == null) || (bug_num == null)) {
|
|
response += TR({class: classVal, id: "tr-" + link },
|
|
TD("0"),
|
|
TD("0"),
|
|
TD("0"),
|
|
TD({style: spacer},
|
|
A({href: link}, link)));
|
|
} else {
|
|
var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
|
|
response += TR({class: classVal, id: "tr-" + link },
|
|
TD("0"),
|
|
TD("0"),
|
|
TD("0"),
|
|
TD({style: spacer},
|
|
A({href: link}, link), " - ",
|
|
A({href: bug_url}, "Bug " + bug_num)));
|
|
}
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
function arrayOfTestFiles(linkArray, fileArray, testPattern) {
|
|
for (var [link, value] in linkArray) {
|
|
if (value instanceof Object) {
|
|
arrayOfTestFiles(value, fileArray, testPattern);
|
|
} else if (isTest(link, testPattern)) {
|
|
fileArray.push(link)
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Produce a flat array of test file paths to be executed in the harness.
|
|
*/
|
|
function jsonArrayOfTestFiles(links)
|
|
{
|
|
var testFiles = [];
|
|
arrayOfTestFiles(links, testFiles);
|
|
testFiles = ['"' + file + '"' for each(file in testFiles)];
|
|
return "[" + testFiles.join(",\n") + "]";
|
|
}
|
|
|
|
/**
|
|
* Produce a normal directory listing.
|
|
*/
|
|
function regularListing(metadata, response)
|
|
{
|
|
var [links, count] = list(metadata.path,
|
|
metadata.getProperty("directory"),
|
|
false);
|
|
response.write(
|
|
HTML(
|
|
HEAD(
|
|
TITLE("mochitest index ", metadata.path)
|
|
),
|
|
BODY(
|
|
BR(),
|
|
A({href: ".."}, "Up a level"),
|
|
UL(linksToListItems(links))
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Produce a test harness page containing all the test cases
|
|
* below it, recursively.
|
|
*/
|
|
function testListing(metadata, response)
|
|
{
|
|
var [links, count] = list(metadata.path,
|
|
metadata.getProperty("directory"),
|
|
true);
|
|
dumpn("count: " + count);
|
|
var tests = jsonArrayOfTestFiles(links);
|
|
response.write(
|
|
HTML(
|
|
HEAD(
|
|
TITLE("MochiTest | ", metadata.path),
|
|
LINK({rel: "stylesheet",
|
|
type: "text/css", href: "/static/harness.css"}
|
|
),
|
|
SCRIPT({type: "text/javascript", src: "/MochiKit/packed.js"}),
|
|
SCRIPT({type: "text/javascript",
|
|
src: "/tests/SimpleTest/TestRunner.js"}),
|
|
SCRIPT({type: "text/javascript",
|
|
src: "/tests/SimpleTest/MozillaFileLogger.js"}),
|
|
SCRIPT({type: "text/javascript",
|
|
src: "/tests/SimpleTest/quit.js"}),
|
|
SCRIPT({type: "text/javascript",
|
|
src: "/tests/SimpleTest/setup.js"}),
|
|
SCRIPT({type: "text/javascript"},
|
|
"connect(window, 'onload', hookup); gTestList=" + tests + ";"
|
|
)
|
|
),
|
|
BODY(
|
|
DIV({class: "container"},
|
|
H2("--> ", A({href: "#", id: "runtests"}, "Run Tests"), " <--"),
|
|
P({style: "float: right;"},
|
|
SMALL(
|
|
"Based on the ",
|
|
A({href:"http://www.mochikit.com/"}, "MochiKit"),
|
|
" unit tests."
|
|
)
|
|
),
|
|
DIV({class: "status"},
|
|
H1({id: "indicator"}, "Status"),
|
|
H2({id: "pass"}, "Passed: ", SPAN({id: "pass-count"},"0")),
|
|
H2({id: "fail"}, "Failed: ", SPAN({id: "fail-count"},"0")),
|
|
H2({id: "fail"}, "Todo: ", SPAN({id: "todo-count"},"0"))
|
|
),
|
|
DIV({class: "clear"}),
|
|
DIV({id: "current-test"},
|
|
B("Currently Executing: ",
|
|
SPAN({id: "current-test-path"}, "_")
|
|
)
|
|
),
|
|
DIV({class: "clear"}),
|
|
DIV({class: "frameholder"},
|
|
IFRAME({scrolling: "no", id: "testframe", width: "500", height: "300"})
|
|
),
|
|
DIV({class: "clear"}),
|
|
DIV({class: "toggle"},
|
|
A({href: "#", id: "toggleNonTests"}, "Show Non-Tests"),
|
|
BR()
|
|
),
|
|
|
|
TABLE({cellpadding: 0, cellspacing: 0, id: "test-table"},
|
|
TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")),
|
|
linksToTableRows(links, 0)
|
|
),
|
|
DIV({class: "clear"})
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Respond to requests that match a file system directory.
|
|
* Under the tests/ directory, return a test harness page.
|
|
*/
|
|
function defaultDirHandler(metadata, response)
|
|
{
|
|
response.setStatusLine("1.1", 200, "OK");
|
|
response.setHeader("Content-type", "text/html", false);
|
|
try {
|
|
if (metadata.path.indexOf("/tests") != 0) {
|
|
regularListing(metadata, response);
|
|
} else {
|
|
testListing(metadata, response);
|
|
}
|
|
} catch (ex) {
|
|
response.write(ex);
|
|
}
|
|
}
|