Bug 572975, bug 573227, websocket fixes, r=smaug
This commit is contained in:
@@ -102,7 +102,7 @@ static nsIThread *gWebSocketThread = nsnull;
|
||||
|
||||
#define TIMEOUT_TRY_CONNECT_AGAIN 1000
|
||||
#define TIMEOUT_WAIT_FOR_SERVER_RESPONSE 20000
|
||||
#define TIMEOUT_WAIT_FOR_CLOSING 5000
|
||||
#define TIMEOUT_WAIT_FOR_CLOSING 20000
|
||||
|
||||
#define ENSURE_TRUE_AND_FAIL_IF_FAILED(x, ret) \
|
||||
PR_BEGIN_MACRO \
|
||||
@@ -240,7 +240,10 @@ public:
|
||||
nsresult Init(nsWebSocket *aOwner);
|
||||
nsresult Disconnect();
|
||||
|
||||
// these are called always on the main thread (they dispatch themselves)
|
||||
// These are called always on the main thread (they dispatch themselves).
|
||||
// ATTENTION, this method when called can release both the WebSocket object
|
||||
// (i.e. mOwner) and its connection (i.e. *this*) if there are no strong event
|
||||
// listeners.
|
||||
DECL_RUNNABLE_ON_MAIN_THREAD_METHOD(Close)
|
||||
DECL_RUNNABLE_ON_MAIN_THREAD_METHOD(FailConnection)
|
||||
|
||||
@@ -253,6 +256,17 @@ public:
|
||||
static nsTArray<nsRefPtr<nsWebSocketEstablishedConnection> >* sWSsConnecting;
|
||||
|
||||
private:
|
||||
enum WSFrameType {
|
||||
eConnectFrame,
|
||||
eUTF8MessageFrame,
|
||||
eCloseFrame
|
||||
};
|
||||
|
||||
struct nsWSFrame {
|
||||
WSFrameType mType;
|
||||
nsAutoPtr<nsCString> mData;
|
||||
};
|
||||
|
||||
// We can only establish one connection at a time per IP address.
|
||||
// TryConnect ensures this by checking sWSsConnecting.
|
||||
// If there is a IP address entry there it tries again after
|
||||
@@ -291,7 +305,8 @@ private:
|
||||
nsresult Reset();
|
||||
void RemoveFromLoadGroup();
|
||||
nsresult ProcessHeaders();
|
||||
nsresult PostData(nsCString *aBuffer, PRBool aIsMessage);
|
||||
nsresult PostData(nsCString *aBuffer,
|
||||
WSFrameType aWSFrameType);
|
||||
nsresult PrintErrorOnConsole(const char *aBundleURI,
|
||||
const PRUnichar *aError,
|
||||
const PRUnichar **aFormatStrings,
|
||||
@@ -324,7 +339,7 @@ private:
|
||||
nsCOMPtr<nsIAsyncInputStream> mSocketInput;
|
||||
nsCOMPtr<nsIAsyncOutputStream> mSocketOutput;
|
||||
nsCOMPtr<nsIProxyInfo> mProxyInfo;
|
||||
nsDeque mOutgoingMessages; // has nsCString* which need to be sent
|
||||
nsDeque mOutgoingMessages; // has nsWSFrame* which need to be sent
|
||||
PRUint32 mBytesAlreadySentOfFirstOutString;
|
||||
PRUint32 mOutgoingBufferedAmount; // not really necessary, but it is
|
||||
// here for fast access.
|
||||
@@ -652,14 +667,17 @@ nsWebSocketEstablishedConnection::nsWebSocketEstablishedConnection() :
|
||||
|
||||
nsWebSocketEstablishedConnection::~nsWebSocketEstablishedConnection()
|
||||
{
|
||||
NS_ASSERTION(!mOwner, "Disconnect wasn't called!");
|
||||
}
|
||||
|
||||
nsresult
|
||||
nsWebSocketEstablishedConnection::PostData(nsCString *aBuffer,
|
||||
PRBool aIsMessage)
|
||||
WSFrameType aWSFrameType)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Not running on main thread");
|
||||
|
||||
nsAutoPtr<nsCString> data(aBuffer);
|
||||
|
||||
if (mStatus == CONN_CLOSED) {
|
||||
NS_ASSERTION(mOwner, "Posting data after disconnecting the websocket!");
|
||||
// the tcp connection has been closed, but the main thread hasn't received
|
||||
@@ -669,14 +687,21 @@ nsWebSocketEstablishedConnection::PostData(nsCString *aBuffer,
|
||||
|
||||
MutexAutoLock lockOut(mLockOutgoingMessages);
|
||||
|
||||
nsAutoPtr<nsWSFrame> frame(new nsWSFrame());
|
||||
NS_ENSURE_TRUE(frame.get(), NS_ERROR_OUT_OF_MEMORY);
|
||||
frame->mType = aWSFrameType;
|
||||
frame->mData = data.forget();
|
||||
|
||||
nsresult rv;
|
||||
PRInt32 sizeBefore = mOutgoingMessages.GetSize();
|
||||
mOutgoingMessages.Push(aBuffer);
|
||||
mOutgoingMessages.Push(frame.forget());
|
||||
NS_ENSURE_TRUE(mOutgoingMessages.GetSize() == sizeBefore + 1,
|
||||
NS_ERROR_OUT_OF_MEMORY);
|
||||
if (aIsMessage) {
|
||||
if (aWSFrameType == eUTF8MessageFrame) {
|
||||
// without the START_BYTE_OF_MESSAGE and END_BYTE_OF_MESSAGE bytes
|
||||
mOutgoingBufferedAmount += aBuffer->Length() - 2;
|
||||
} else if (aWSFrameType == eCloseFrame) {
|
||||
mPostedCloseFrame = PR_TRUE;
|
||||
}
|
||||
|
||||
if (sizeBefore == 0) {
|
||||
@@ -752,7 +777,7 @@ nsWebSocketEstablishedConnection::PostMessage(const nsString& aMessage)
|
||||
ENSURE_TRUE_AND_FAIL_IF_FAILED(buf->Length() == static_cast<PRUint32>(outLen),
|
||||
NS_ERROR_UNEXPECTED);
|
||||
|
||||
rv = PostData(buf.forget(), PR_TRUE);
|
||||
rv = PostData(buf.forget(), eUTF8MessageFrame);
|
||||
ENSURE_SUCCESS_AND_FAIL_IF_FAILED(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
@@ -873,7 +898,7 @@ IMPL_RUNNABLE_ON_MAIN_THREAD_METHOD_BEGIN(DoInitialRequest)
|
||||
|
||||
mStatus = CONN_SENDING_INITIAL_REQUEST;
|
||||
|
||||
rv = PostData(buf.forget(), PR_FALSE);
|
||||
rv = PostData(buf.forget(), eConnectFrame);
|
||||
CHECK_SUCCESS_AND_FAIL_IF_FAILED(rv);
|
||||
}
|
||||
IMPL_RUNNABLE_ON_MAIN_THREAD_METHOD_END
|
||||
@@ -1455,11 +1480,11 @@ nsWebSocketEstablishedConnection::Reset()
|
||||
mSocketOutput = nsnull;
|
||||
|
||||
while (mOutgoingMessages.GetSize() != 0) {
|
||||
delete static_cast<nsCString*>(mOutgoingMessages.PopFront());
|
||||
delete static_cast<nsWSFrame*>(mOutgoingMessages.PopFront());
|
||||
}
|
||||
|
||||
while (mReceivedMessages.GetSize() != 0) {
|
||||
delete static_cast<nsString*>(mReceivedMessages.PopFront());
|
||||
delete static_cast<nsCString*>(mReceivedMessages.PopFront());
|
||||
}
|
||||
|
||||
mBytesAlreadySentOfFirstOutString = 0;
|
||||
@@ -1685,7 +1710,7 @@ nsWebSocketEstablishedConnection::DoConnect()
|
||||
|
||||
mStatus = CONN_CONNECTING_TO_HTTP_PROXY;
|
||||
|
||||
rv = PostData(buf.forget(), PR_FALSE);
|
||||
rv = PostData(buf.forget(), eConnectFrame);
|
||||
ENSURE_SUCCESS_AND_FAIL_IF_FAILED(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
@@ -1945,6 +1970,10 @@ IMPL_RUNNABLE_ON_MAIN_THREAD_METHOD_BEGIN(Close)
|
||||
{
|
||||
nsresult rv;
|
||||
|
||||
// Disconnect() can release this object, so we keep a
|
||||
// reference until the end of the method
|
||||
nsRefPtr<nsWebSocketEstablishedConnection> kungfuDeathGrip = this;
|
||||
|
||||
if (mOwner->mReadyState == nsIWebSocket::CONNECTING) {
|
||||
// we must not convey any failure information to scripts, so we just
|
||||
// disconnect and maintain the owner WebSocket object in the CONNECTING
|
||||
@@ -1987,13 +2016,11 @@ IMPL_RUNNABLE_ON_MAIN_THREAD_METHOD_BEGIN(Close)
|
||||
closeFrame->SetCharAt(START_BYTE_OF_CLOSE_FRAME, 0);
|
||||
closeFrame->SetCharAt(END_BYTE_OF_CLOSE_FRAME, 1);
|
||||
|
||||
rv = PostData(closeFrame.forget(), PR_FALSE);
|
||||
rv = PostData(closeFrame.forget(), eCloseFrame);
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("Failed to post the close frame");
|
||||
return;
|
||||
}
|
||||
|
||||
mPostedCloseFrame = PR_TRUE;
|
||||
} else {
|
||||
// Probably failed to send the close frame. Just disconnect.
|
||||
Disconnect();
|
||||
@@ -2004,6 +2031,10 @@ IMPL_RUNNABLE_ON_MAIN_THREAD_METHOD_END
|
||||
void
|
||||
nsWebSocketEstablishedConnection::ForceClose()
|
||||
{
|
||||
// Disconnect() can release this object, so we keep a
|
||||
// reference until the end of the method
|
||||
nsRefPtr<nsWebSocketEstablishedConnection> kungfuDeathGrip = this;
|
||||
|
||||
if (mOwner->mReadyState == nsIWebSocket::CONNECTING) {
|
||||
// we must not convey any failure information to scripts, so we just
|
||||
// disconnect and maintain the owner WebSocket object in the CONNECTING
|
||||
@@ -2061,6 +2092,12 @@ nsWebSocketEstablishedConnection::Disconnect()
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If mOwner is deleted when calling mOwner->DontKeepAliveAnyMore()
|
||||
// then this method can be called again, and we will get a deadlock.
|
||||
nsRefPtr<nsWebSocket> kungfuDeathGrip = mOwner;
|
||||
|
||||
mOwner->DontKeepAliveAnyMore();
|
||||
|
||||
RemoveWSConnecting();
|
||||
|
||||
mStatus = CONN_CLOSED;
|
||||
@@ -2111,11 +2148,11 @@ nsWebSocketEstablishedConnection::Disconnect()
|
||||
mProxyInfo = nsnull;
|
||||
|
||||
while (mOutgoingMessages.GetSize() != 0) {
|
||||
delete static_cast<nsCString*>(mOutgoingMessages.PopFront());
|
||||
delete static_cast<nsWSFrame*>(mOutgoingMessages.PopFront());
|
||||
}
|
||||
|
||||
while (mReceivedMessages.GetSize() != 0) {
|
||||
delete static_cast<nsString*>(mReceivedMessages.PopFront());
|
||||
delete static_cast<nsCString*>(mReceivedMessages.PopFront());
|
||||
}
|
||||
|
||||
// Remove ourselves from the document's load group. nsIRequest expects
|
||||
@@ -2606,13 +2643,13 @@ nsWebSocketEstablishedConnection::OnInputStreamReady(nsIAsyncInputStream *aStrea
|
||||
// closed. In this case we have to reset the WebSocket, not Close it.
|
||||
if (mStatus != CONN_RETRYING_TO_AUTHENTICATE) {
|
||||
mStatus = CONN_CLOSED;
|
||||
mFailureStatus = NS_BASE_STREAM_CLOSED;
|
||||
if (mStatus < CONN_CONNECTED_AND_READY) {
|
||||
FailConnection();
|
||||
} else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
mFailureStatus = NS_BASE_STREAM_CLOSED;
|
||||
return NS_BASE_STREAM_CLOSED;
|
||||
}
|
||||
|
||||
@@ -2659,13 +2696,14 @@ nsWebSocketEstablishedConnection::OnOutputStreamReady(nsIAsyncOutputStream *aStr
|
||||
|
||||
// send what we can of the 1st string
|
||||
|
||||
nsCString *strToSend =
|
||||
static_cast<nsCString*>(mOutgoingMessages.PeekFront());
|
||||
nsWSFrame *frameToSend =
|
||||
static_cast<nsWSFrame*>(mOutgoingMessages.PeekFront());
|
||||
nsCString *strToSend = frameToSend->mData;
|
||||
PRUint32 sizeToSend =
|
||||
strToSend->Length() - mBytesAlreadySentOfFirstOutString;
|
||||
PRBool currentStrHasStartFrameByte =
|
||||
(mBytesAlreadySentOfFirstOutString == 0);
|
||||
PRBool strIsMessage = (mStatus >= CONN_CONNECTED_AND_READY);
|
||||
PRBool strIsMessage = (frameToSend->mType == eUTF8MessageFrame);
|
||||
|
||||
if (sizeToSend != 0) {
|
||||
PRUint32 written;
|
||||
@@ -2691,12 +2729,12 @@ nsWebSocketEstablishedConnection::OnOutputStreamReady(nsIAsyncOutputStream *aStr
|
||||
|
||||
if (written == 0) {
|
||||
mStatus = CONN_CLOSED;
|
||||
mFailureStatus = NS_BASE_STREAM_CLOSED;
|
||||
if (mStatus < CONN_CONNECTED_AND_READY) {
|
||||
FailConnection();
|
||||
} else {
|
||||
Close();
|
||||
}
|
||||
mFailureStatus = NS_BASE_STREAM_CLOSED;
|
||||
return NS_BASE_STREAM_CLOSED;
|
||||
}
|
||||
|
||||
@@ -2731,7 +2769,7 @@ nsWebSocketEstablishedConnection::OnOutputStreamReady(nsIAsyncOutputStream *aStr
|
||||
|
||||
// ok, send the next string
|
||||
mOutgoingMessages.PopFront();
|
||||
delete strToSend;
|
||||
delete frameToSend;
|
||||
mBytesAlreadySentOfFirstOutString = 0;
|
||||
}
|
||||
|
||||
@@ -2803,13 +2841,19 @@ nsWebSocketEstablishedConnection::GetInterface(const nsIID &aIID,
|
||||
// nsWebSocket
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
nsWebSocket::nsWebSocket() : mReadyState(nsIWebSocket::CONNECTING),
|
||||
nsWebSocket::nsWebSocket() : mHasStrongEventListeners(PR_FALSE),
|
||||
mCheckThereAreStrongEventListeners(PR_TRUE),
|
||||
mReadyState(nsIWebSocket::CONNECTING),
|
||||
mOutgoingBufferedAmount(0)
|
||||
{
|
||||
}
|
||||
|
||||
nsWebSocket::~nsWebSocket()
|
||||
{
|
||||
if (mConnection) {
|
||||
mConnection->Disconnect();
|
||||
mConnection = nsnull;
|
||||
}
|
||||
if (mListenerManager) {
|
||||
mListenerManager->Disconnect();
|
||||
mListenerManager = nsnull;
|
||||
@@ -2832,16 +2876,16 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsWebSocket,
|
||||
nsDOMEventTargetWrapperCache)
|
||||
if (tmp->mConnection) {
|
||||
tmp->mConnection->Disconnect();
|
||||
tmp->mConnection = nsnull;
|
||||
}
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnOpenListener)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnMessageListener)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnCloseListener)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mOnErrorListener)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mPrincipal)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mURI)
|
||||
if (tmp->mConnection) {
|
||||
tmp->mConnection->Disconnect();
|
||||
tmp->mConnection = nsnull;
|
||||
}
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
|
||||
DOMCI_DATA(WebSocket, nsWebSocket)
|
||||
@@ -2955,7 +2999,9 @@ public:
|
||||
|
||||
NS_IMETHOD Run()
|
||||
{
|
||||
return mWebSocket->CreateAndDispatchCloseEvent(mWasClean);
|
||||
nsresult rv = mWebSocket->CreateAndDispatchCloseEvent(mWasClean);
|
||||
mWebSocket->UpdateMustKeepAlive();
|
||||
return rv;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -3073,6 +3119,7 @@ nsWebSocket::SetReadyState(PRUint16 aNewReadyState)
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("Failed to dispatch the open event");
|
||||
}
|
||||
UpdateMustKeepAlive();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3099,6 +3146,7 @@ nsWebSocket::SetReadyState(PRUint16 aNewReadyState)
|
||||
rv = NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("Failed to dispatch the close event");
|
||||
UpdateMustKeepAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3210,6 +3258,94 @@ nsWebSocket::SetProtocol(const nsString& aProtocol)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Methods that keep alive the WebSocket object when there are
|
||||
// onopen/onmessage event listeners.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
nsWebSocket::UpdateMustKeepAlive()
|
||||
{
|
||||
if (!mCheckThereAreStrongEventListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mHasStrongEventListeners) {
|
||||
if (!mListenerManager ||
|
||||
((mReadyState != nsIWebSocket::CONNECTING ||
|
||||
!mListenerManager->HasListenersFor(NS_LITERAL_STRING("open"))) &&
|
||||
(mReadyState == nsIWebSocket::CLOSED ||
|
||||
!mListenerManager->HasListenersFor(NS_LITERAL_STRING("message"))))) {
|
||||
mHasStrongEventListeners = PR_FALSE;
|
||||
static_cast<nsPIDOMEventTarget*>(this)->Release();
|
||||
}
|
||||
} else {
|
||||
if ((mReadyState == nsIWebSocket::CONNECTING && mListenerManager &&
|
||||
mListenerManager->HasListenersFor(NS_LITERAL_STRING("open"))) ||
|
||||
(mReadyState != nsIWebSocket::CLOSED && mListenerManager &&
|
||||
mListenerManager->HasListenersFor(NS_LITERAL_STRING("message")))) {
|
||||
mHasStrongEventListeners = PR_TRUE;
|
||||
static_cast<nsPIDOMEventTarget*>(this)->AddRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
nsWebSocket::DontKeepAliveAnyMore()
|
||||
{
|
||||
if (mHasStrongEventListeners) {
|
||||
mCheckThereAreStrongEventListeners = PR_FALSE;
|
||||
mHasStrongEventListeners = PR_FALSE;
|
||||
static_cast<nsPIDOMEventTarget*>(this)->Release();
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsWebSocket::AddEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener* aListener,
|
||||
PRBool aUseCapture)
|
||||
{
|
||||
nsresult rv = nsDOMEventTargetHelper::AddEventListener(aType,
|
||||
aListener,
|
||||
aUseCapture);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
UpdateMustKeepAlive();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsWebSocket::RemoveEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener* aListener,
|
||||
PRBool aUseCapture)
|
||||
{
|
||||
nsresult rv = nsDOMEventTargetHelper::RemoveEventListener(aType,
|
||||
aListener,
|
||||
aUseCapture);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
UpdateMustKeepAlive();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsWebSocket::AddEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener *aListener,
|
||||
PRBool aUseCapture,
|
||||
PRBool aWantsUntrusted,
|
||||
PRUint8 optional_argc)
|
||||
{
|
||||
nsresult rv = nsDOMEventTargetHelper::AddEventListener(aType,
|
||||
aListener,
|
||||
aUseCapture,
|
||||
aWantsUntrusted,
|
||||
optional_argc);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
UpdateMustKeepAlive();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// nsWebSocket::nsIWebSocket methods:
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -3303,6 +3439,10 @@ nsWebSocket::Close()
|
||||
}
|
||||
|
||||
if (mReadyState == nsIWebSocket::CONNECTING) {
|
||||
// FailConnection() can release the object if there are no strong event
|
||||
// listeners, so we keep a reference before calling it
|
||||
nsRefPtr<nsWebSocket> kungfuDeathGrip = this;
|
||||
|
||||
mConnection->FailConnection();
|
||||
|
||||
// We need to set the readyState here because mConnection would set it
|
||||
|
||||
@@ -86,6 +86,21 @@ public:
|
||||
NS_IMETHOD Initialize(nsISupports* aOwner, JSContext* aContext,
|
||||
JSObject* aObject, PRUint32 aArgc, jsval* aArgv);
|
||||
|
||||
// nsIDOMEventTarget
|
||||
NS_IMETHOD AddEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener* aListener,
|
||||
PRBool aUseCapture);
|
||||
NS_IMETHOD RemoveEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener* aListener,
|
||||
PRBool aUseCapture);
|
||||
|
||||
// nsIDOMNSEventTarget
|
||||
NS_IMETHOD AddEventListener(const nsAString& aType,
|
||||
nsIDOMEventListener *aListener,
|
||||
PRBool aUseCapture,
|
||||
PRBool aWantsUntrusted,
|
||||
PRUint8 optional_argc);
|
||||
|
||||
static void ReleaseGlobals();
|
||||
|
||||
protected:
|
||||
@@ -100,6 +115,14 @@ protected:
|
||||
// called from mConnection accordingly to the situation
|
||||
void SetReadyState(PRUint16 aNewReadyState);
|
||||
|
||||
// if there are onopen or onmessage event listeners ("strong event listeners")
|
||||
// then this method keeps the object alive when js doesn't have strong
|
||||
// references to it.
|
||||
void UpdateMustKeepAlive();
|
||||
// Releases, if necessary, the strong event listeners. ATTENTION, when calling
|
||||
// this method the object can be released (and possibly collected).
|
||||
void DontKeepAliveAnyMore();
|
||||
|
||||
nsRefPtr<nsDOMEventListenerWrapper> mOnOpenListener;
|
||||
nsRefPtr<nsDOMEventListenerWrapper> mOnErrorListener;
|
||||
nsRefPtr<nsDOMEventListenerWrapper> mOnMessageListener;
|
||||
@@ -109,6 +132,10 @@ protected:
|
||||
nsString mOriginalURL;
|
||||
PRPackedBool mSecure; // if true it is using SSL and the wss scheme,
|
||||
// otherwise it is using the ws scheme with no SSL
|
||||
|
||||
PRPackedBool mHasStrongEventListeners;
|
||||
PRPackedBool mCheckThereAreStrongEventListeners;
|
||||
|
||||
nsCString mAsciiHost; // hostname
|
||||
PRUint32 mPort;
|
||||
nsCString mResource; // [filepath[?query]]
|
||||
|
||||
@@ -6,25 +6,56 @@ import sys
|
||||
# see the list of tests in test_websocket.html
|
||||
|
||||
def web_socket_do_extra_handshake(request):
|
||||
if request.ws_protocol == "test 6":
|
||||
sys.exit(0)
|
||||
elif request.ws_protocol == "test 19":
|
||||
time.sleep(180)
|
||||
pass
|
||||
elif request.ws_protocol == "test 8":
|
||||
if request.ws_protocol == "test 2.1":
|
||||
time.sleep(5)
|
||||
pass
|
||||
elif request.ws_protocol == "test 9":
|
||||
time.sleep(5)
|
||||
pass
|
||||
elif request.ws_protocol == "test 10.1":
|
||||
elif request.ws_protocol == "test 10":
|
||||
time.sleep(5)
|
||||
pass
|
||||
elif request.ws_protocol == "test 19":
|
||||
raise ValueError('Aborting (test 19)')
|
||||
elif request.ws_protocol == "test 20" or request.ws_protocol == "test 17":
|
||||
time.sleep(10)
|
||||
pass
|
||||
elif request.ws_protocol == "test 22":
|
||||
time.sleep(60)
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
def web_socket_transfer_data(request):
|
||||
if request.ws_protocol == "test 9":
|
||||
if request.ws_protocol == "test 2.1" or request.ws_protocol == "test 2.2":
|
||||
msgutil.close_connection(request)
|
||||
elif request.ws_protocol == "test 6":
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "1":
|
||||
resp = "2"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "3":
|
||||
resp = "4"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "5":
|
||||
resp = "あいうえお"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
elif request.ws_protocol == "test 7":
|
||||
try:
|
||||
while not request.client_terminated:
|
||||
msgutil.receive_message(request)
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
pass
|
||||
msgutil.send_message(request, "server data")
|
||||
msgutil.send_message(request, "server data")
|
||||
msgutil.send_message(request, "server data")
|
||||
msgutil.send_message(request, "server data")
|
||||
msgutil.send_message(request, "server data")
|
||||
time.sleep(30)
|
||||
msgutil.close_connection(request, True)
|
||||
elif request.ws_protocol == "test 10":
|
||||
msgutil.close_connection(request)
|
||||
elif request.ws_protocol == "test 11":
|
||||
resp = "wrong message"
|
||||
@@ -41,27 +72,19 @@ def web_socket_transfer_data(request):
|
||||
request.connection.write('\xff\x00')
|
||||
msgutil.send_message(request, "server data")
|
||||
elif request.ws_protocol == "test 15":
|
||||
sys.exit (0)
|
||||
elif request.ws_protocol == "test 17":
|
||||
while not request.client_terminated:
|
||||
msgutil.send_message(request, "server data")
|
||||
time.sleep(1)
|
||||
msgutil.close_connection(request, True)
|
||||
return
|
||||
elif request.ws_protocol == "test 17" or request.ws_protocol == "test 21":
|
||||
time.sleep(5)
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "client data":
|
||||
resp = "server data"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
time.sleep(5)
|
||||
msgutil.close_connection(request)
|
||||
time.sleep(5)
|
||||
elif request.ws_protocol == "test 20":
|
||||
msgutil.send_message(request, "server data")
|
||||
sys.exit(0)
|
||||
elif request.ws_protocol == "test 18":
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "1":
|
||||
resp = "2"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "3":
|
||||
resp = "4"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
resp = "wrong message"
|
||||
if msgutil.receive_message(request) == "5":
|
||||
resp = "あいうえお"
|
||||
msgutil.send_message(request, resp.decode('utf-8'))
|
||||
elif request.ws_protocol == "test 10.1" or request.ws_protocol == "test 10.2":
|
||||
msgutil.close_connection(request)
|
||||
while not request.client_terminated:
|
||||
msgutil.receive_message(request)
|
||||
|
||||
@@ -18,16 +18,17 @@
|
||||
/*
|
||||
* tests:
|
||||
* 1. client tries to connect to a http scheme location;
|
||||
* 2. client tries to connect to an http resource;
|
||||
* 2. assure serialization of the connections;
|
||||
* 3. client tries to connect to an non-existent ws server;
|
||||
* 4. client tries to connect using a relative url;
|
||||
* 5. client uses an invalid protocol value;
|
||||
* 6. server closes the tcp connection before establishing the ws connection;
|
||||
* 7. client calls close() and the server sends the close frame in
|
||||
* 6. counter and encoding check;
|
||||
* 7. client calls close() and the server keeps sending messages and it doesn't
|
||||
* send the close frame;
|
||||
* 8. client calls close() and the server sends the close frame in
|
||||
* acknowledgement;
|
||||
* 8. client closes the connection before the ws connection is established;
|
||||
* 9. client sends a message before the ws connection is established;
|
||||
* 10. assure serialization of the connections;
|
||||
* 9. client closes the connection before the ws connection is established;
|
||||
* 10. client sends a message before the ws connection is established;
|
||||
* 11. a simple hello echo;
|
||||
* 12. client sends a message with bad bytes;
|
||||
* 13. server sends an invalid message;
|
||||
@@ -35,54 +36,46 @@
|
||||
* it keeps sending normal ws messages;
|
||||
* 15. server closes the tcp connection, but it doesn't send the close frame;
|
||||
* 16. client calls close() and tries to send a message;
|
||||
* 17. client calls close() and the server keeps sending messages and it doesn't
|
||||
* send the close frame;
|
||||
* 18. counter and encoding check;
|
||||
* 19. server takes too long to establish the ws connection;
|
||||
* 17. see bug 572975 - all event listeners set
|
||||
* 18. client tries to connect to an http resource;
|
||||
* 19. server closes the tcp connection before establishing the ws connection;
|
||||
* 20. see bug 572975 - only on error and onclose event listeners set
|
||||
* 21. see bug 572975 - same as test 17, but delete strong event listeners when
|
||||
* receiving the message event;
|
||||
* 22. server takes too long to establish the ws connection;
|
||||
*/
|
||||
|
||||
var first_test = 1;
|
||||
var last_test = 19;
|
||||
var last_test = 22;
|
||||
|
||||
var current_test = 1;
|
||||
var current_test = first_test;
|
||||
|
||||
var timeoutToAbortTest = 60000;
|
||||
var timeoutToOpenWS = 25000;
|
||||
var all_ws = [];
|
||||
|
||||
function shouldNotOpen(e)
|
||||
{
|
||||
var ws = e.target;
|
||||
ok(false, "onopen shouldn't be called on test " + ws._testNumber + "!");
|
||||
if (ws._timeoutToSucceed != undefined) {
|
||||
clearTimeout(ws._timeoutToSucceed);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldNotReceiveCloseEvent(e)
|
||||
{
|
||||
var ws = e.target;
|
||||
ok(false, "onclose shouldn't be called on test " + ws._testNumber + "!");
|
||||
if (ws._timeoutToSucceed != undefined) {
|
||||
clearTimeout(ws._timeoutToSucceed);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldCloseCleanly(e)
|
||||
{
|
||||
var ws = e.target;
|
||||
//ok(e.wasClean, "the ws connection in test " + ws._testNumber + " should be closed cleanly");
|
||||
if (ws._timeoutToSucceed != undefined) {
|
||||
clearTimeout(ws._timeoutToSucceed);
|
||||
}
|
||||
ok(e.wasClean, "the ws connection in test " + ws._testNumber + " should be closed cleanly");
|
||||
}
|
||||
|
||||
function shouldCloseNotCleanly(e)
|
||||
{
|
||||
var ws = e.target;
|
||||
//ok(!e.wasClean, "the ws connection in test " + ws._testNumber + " shouldn't be closed cleanly");
|
||||
if (ws._timeoutToSucceed != undefined) {
|
||||
clearTimeout(ws._timeoutToSucceed);
|
||||
}
|
||||
ok(!e.wasClean, "the ws connection in test " + ws._testNumber + " shouldn't be closed cleanly");
|
||||
}
|
||||
|
||||
function CreateTestWS(ws_location, ws_protocol)
|
||||
@@ -119,6 +112,20 @@ function CreateTestWS(ws_location, ws_protocol)
|
||||
return ws;
|
||||
}
|
||||
|
||||
function forcegc()
|
||||
{
|
||||
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
|
||||
Components.utils.forceGC();
|
||||
var wu = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
wu.garbageCollect();
|
||||
setTimeout(function()
|
||||
{
|
||||
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
|
||||
wu.garbageCollect();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function doTest(number)
|
||||
{
|
||||
if (doTest.timeoutId !== null) {
|
||||
@@ -163,12 +170,28 @@ function test1()
|
||||
doTest(2);
|
||||
}
|
||||
|
||||
// this test expects that the serialization list to connect to the proxy
|
||||
// is empty
|
||||
function test2()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket_http_resource.txt");
|
||||
ws.onopen = shouldNotOpen;
|
||||
ws.onclose = shouldNotReceiveCloseEvent;
|
||||
doTest(3);
|
||||
var ws1 = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 2.1");
|
||||
current_test--; // CreateTestWS incremented this
|
||||
var ws2 = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 2.2");
|
||||
|
||||
var ws2CanConnect = false;
|
||||
|
||||
// the server will delay ws1 for 5 seconds
|
||||
|
||||
ws1.onopen = function()
|
||||
{
|
||||
ws2CanConnect = true;
|
||||
}
|
||||
|
||||
ws2.onopen = function()
|
||||
{
|
||||
ok(ws2CanConnect, "shouldn't connect yet in test 2!");
|
||||
doTest(3);
|
||||
}
|
||||
}
|
||||
|
||||
function test3()
|
||||
@@ -183,10 +206,10 @@ function test4()
|
||||
{
|
||||
try {
|
||||
var ws = CreateTestWS("file_websocket");
|
||||
ok(false, "test4 failed");
|
||||
ok(false, "test 4 failed");
|
||||
}
|
||||
catch (e) {
|
||||
ok(true, "test4 failed");
|
||||
ok(true, "test 4 failed");
|
||||
}
|
||||
doTest(5);
|
||||
}
|
||||
@@ -214,9 +237,25 @@ function test5()
|
||||
function test6()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 6");
|
||||
ws.onopen = shouldNotOpen;
|
||||
ws.onclose = shouldNotReceiveCloseEvent;
|
||||
doTest(7);
|
||||
var counter = 1;
|
||||
ws.onopen = function()
|
||||
{
|
||||
ws.send(counter);
|
||||
}
|
||||
ws.onmessage = function(e)
|
||||
{
|
||||
if (counter == 5) {
|
||||
ok(e.data == "あいうえお");
|
||||
ws.close();
|
||||
doTest(7);
|
||||
} else {
|
||||
ok(e.data == counter+1, "bad counter");
|
||||
counter += 2;
|
||||
ws.send(counter);
|
||||
}
|
||||
}
|
||||
ws.onclose = shouldCloseCleanly;
|
||||
ws._receivedCloseEvent = false;
|
||||
}
|
||||
|
||||
function test7()
|
||||
@@ -228,7 +267,7 @@ function test7()
|
||||
}
|
||||
ws.onclose = function(e)
|
||||
{
|
||||
shouldCloseCleanly(e);
|
||||
shouldCloseNotCleanly(e);
|
||||
doTest(8);
|
||||
};
|
||||
ws._receivedCloseEvent = false;
|
||||
@@ -237,20 +276,35 @@ function test7()
|
||||
function test8()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 8");
|
||||
ws.onopen = function()
|
||||
{
|
||||
ws.close();
|
||||
}
|
||||
ws.onclose = function(e)
|
||||
{
|
||||
shouldCloseCleanly(e);
|
||||
doTest(9);
|
||||
};
|
||||
ws._receivedCloseEvent = false;
|
||||
}
|
||||
|
||||
function test9()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 9");
|
||||
ws.onopen = shouldNotOpen;
|
||||
ws.onclose = function(e)
|
||||
{
|
||||
shouldCloseNotCleanly(e);
|
||||
doTest(9);
|
||||
doTest(10);
|
||||
};
|
||||
|
||||
ws._receivedCloseEvent = false;
|
||||
ws.close();
|
||||
}
|
||||
|
||||
function test9()
|
||||
function test10()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 9");
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 10");
|
||||
ws.onclose = shouldCloseCleanly;
|
||||
ws._receivedCloseEvent = false;
|
||||
|
||||
@@ -261,27 +315,8 @@ function test9()
|
||||
catch (e) {
|
||||
ok(true, "Couldn't send data before connecting!");
|
||||
}
|
||||
doTest(10);
|
||||
}
|
||||
|
||||
function test10()
|
||||
{
|
||||
var ws1 = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 10.1");
|
||||
current_test--; // CreateTestWS incremented this
|
||||
var ws2 = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 10.2");
|
||||
|
||||
var ws2CanConnect = false;
|
||||
|
||||
// the server will delay ws1 for 5 seconds
|
||||
|
||||
ws1.onopen = function()
|
||||
ws.onopen = function()
|
||||
{
|
||||
ws2CanConnect = true;
|
||||
}
|
||||
|
||||
ws2.onopen = function()
|
||||
{
|
||||
ok(ws2CanConnect, "shouldn't connect yet in test 10!");
|
||||
doTest(11);
|
||||
}
|
||||
}
|
||||
@@ -336,8 +371,8 @@ function test13()
|
||||
{
|
||||
ws._timesCalledOnError++;
|
||||
if (ws._timesCalledOnError == 2) {
|
||||
ok(true, "test 13 succeeded");
|
||||
doTest(14);
|
||||
ok(true, "test13 succeeded");
|
||||
}
|
||||
}
|
||||
ws.onclose = shouldCloseCleanly;
|
||||
@@ -387,43 +422,63 @@ function test16()
|
||||
ws._receivedCloseEvent = false;
|
||||
}
|
||||
|
||||
var status_test17 = "not started";
|
||||
|
||||
window._test17 = function()
|
||||
{
|
||||
var local_ws = new WebSocket("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 17");
|
||||
|
||||
status_test17 = "started";
|
||||
|
||||
local_ws.onopen = function(e)
|
||||
{
|
||||
status_test17 = "opened";
|
||||
e.target.send("client data");
|
||||
forcegc();
|
||||
};
|
||||
|
||||
local_ws.onerror = function()
|
||||
{
|
||||
ok(false, "onerror called on test " + e.target._testNumber + "!");
|
||||
};
|
||||
|
||||
local_ws.onmessage = function(e)
|
||||
{
|
||||
ok(e.data == "server data", "Bad message in test 17");
|
||||
status_test17 = "got message";
|
||||
forcegc();
|
||||
};
|
||||
|
||||
local_ws.onclose = function(e)
|
||||
{
|
||||
ok(status_test17 == "got message", "Didn't got message in test 17!");
|
||||
shouldCloseCleanly(e);
|
||||
status_test17 = "closed";
|
||||
forcegc();
|
||||
doTest(18);
|
||||
forcegc();
|
||||
};
|
||||
|
||||
local_ws = null;
|
||||
window._test17 = null;
|
||||
forcegc();
|
||||
}
|
||||
|
||||
function test17()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 17");
|
||||
ws.onopen = function()
|
||||
{
|
||||
ws.close();
|
||||
}
|
||||
ws.onclose = function(e)
|
||||
{
|
||||
shouldCloseNotCleanly(e);
|
||||
doTest(18);
|
||||
};
|
||||
ws._receivedCloseEvent = false;
|
||||
window._test17();
|
||||
}
|
||||
|
||||
// The tests that expects that their websockets neither open nor close MUST
|
||||
// be in the end of the tests, i.e. HERE, in order to prevent blocking the other
|
||||
// tests.
|
||||
|
||||
function test18()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 18");
|
||||
var counter = 1;
|
||||
ws.onopen = function()
|
||||
{
|
||||
ws.send(counter);
|
||||
}
|
||||
ws.onmessage = function(e)
|
||||
{
|
||||
if (counter == 5) {
|
||||
ok(e.data == "あいうえお");
|
||||
ws.close();
|
||||
doTest(19);
|
||||
} else {
|
||||
ok(e.data == counter+1, "bad counter");
|
||||
counter += 2;
|
||||
ws.send(counter);
|
||||
}
|
||||
}
|
||||
ws.onclose = shouldCloseCleanly;
|
||||
ws._receivedCloseEvent = false;
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket_http_resource.txt");
|
||||
ws.onopen = shouldNotOpen;
|
||||
ws.onclose = shouldNotReceiveCloseEvent;
|
||||
doTest(19);
|
||||
}
|
||||
|
||||
function test19()
|
||||
@@ -434,6 +489,81 @@ function test19()
|
||||
doTest(20);
|
||||
}
|
||||
|
||||
window._test20 = function()
|
||||
{
|
||||
var local_ws = new WebSocket("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 20");
|
||||
|
||||
local_ws.onerror = function()
|
||||
{
|
||||
ok(false, "onerror called on test " + e.target._testNumber + "!");
|
||||
};
|
||||
|
||||
local_ws.onclose = shouldNotReceiveCloseEvent;
|
||||
|
||||
local_ws = null;
|
||||
window._test20 = null;
|
||||
forcegc();
|
||||
}
|
||||
|
||||
function test20()
|
||||
{
|
||||
window._test20();
|
||||
doTest(21);
|
||||
}
|
||||
|
||||
var timeoutTest21;
|
||||
|
||||
window._test21 = function()
|
||||
{
|
||||
var local_ws = new WebSocket("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 21");
|
||||
|
||||
local_ws.onopen = function(e)
|
||||
{
|
||||
e.target.send("client data");
|
||||
timeoutTest21 = setTimeout(function()
|
||||
{
|
||||
ok(false, "Didn't received message on test 21!");
|
||||
}, 15000);
|
||||
forcegc();
|
||||
e.target.onopen = null;
|
||||
forcegc();
|
||||
};
|
||||
|
||||
local_ws.onerror = function()
|
||||
{
|
||||
ok(false, "onerror called on test " + e.target._testNumber + "!");
|
||||
};
|
||||
|
||||
local_ws.onmessage = function(e)
|
||||
{
|
||||
clearTimeout(timeoutTest21);
|
||||
ok(e.data == "server data", "Bad message in test 21");
|
||||
forcegc();
|
||||
e.target.onmessage = null;
|
||||
forcegc();
|
||||
};
|
||||
|
||||
local_ws.onclose = shouldNotReceiveCloseEvent;
|
||||
|
||||
local_ws = null;
|
||||
window._test21 = null;
|
||||
forcegc();
|
||||
}
|
||||
|
||||
function test21()
|
||||
{
|
||||
window._test21();
|
||||
doTest(22);
|
||||
}
|
||||
|
||||
function test22()
|
||||
{
|
||||
var ws = CreateTestWS("ws://mochi.test:8888/tests/content/base/test/file_websocket", "test 22");
|
||||
ws.onopen = shouldNotOpen;
|
||||
ws.onclose = shouldNotReceiveCloseEvent;
|
||||
doTest(23);
|
||||
}
|
||||
|
||||
function finishWSTest()
|
||||
{
|
||||
for (i = 0; i < all_ws.length; ++i) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import Queue
|
||||
import threading
|
||||
|
||||
from mod_pywebsocket import util
|
||||
from time import time,sleep
|
||||
|
||||
|
||||
class MsgUtilException(Exception):
|
||||
@@ -71,7 +72,7 @@ def _write(request, bytes):
|
||||
raise
|
||||
|
||||
|
||||
def close_connection(request):
|
||||
def close_connection(request, abort=False):
|
||||
"""Close connection.
|
||||
|
||||
Args:
|
||||
@@ -83,10 +84,25 @@ def close_connection(request):
|
||||
# running through the following steps:
|
||||
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the start
|
||||
# of the closing handshake.
|
||||
_write(request, '\xff\x00')
|
||||
got_exception = False
|
||||
if not abort:
|
||||
_write(request, '\xff\x00')
|
||||
# timeout of 20 seconds to get the client's close frame ack
|
||||
initial_time = time()
|
||||
end_time = initial_time + 20
|
||||
while time() < end_time:
|
||||
try:
|
||||
receive_message(request)
|
||||
except ConnectionTerminatedException, e:
|
||||
got_exception = True
|
||||
sleep(1)
|
||||
request.server_terminated = True
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set, or
|
||||
# until a server-defined timeout expires.
|
||||
if got_exception:
|
||||
util.prepend_message_to_exception(
|
||||
'client initiated closing handshake for %s: ' % (
|
||||
request.ws_resource),
|
||||
e)
|
||||
raise ConnectionTerminatedException
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user