diff --git a/stepmania/src/archutils/Win32/CrashHandlerNetworking.cpp b/stepmania/src/archutils/Win32/CrashHandlerNetworking.cpp new file mode 100644 index 0000000000..4b407192c5 --- /dev/null +++ b/stepmania/src/archutils/Win32/CrashHandlerNetworking.cpp @@ -0,0 +1,725 @@ +#include "global.h" +#include "CrashHandlerNetworking.h" +#include "RageThreads.h" +#include "RageLog.h" +#include "RageThreads.h" +#include "RageTimer.h" +#include "RageUtil.h" + +#if defined(WINDOWS) +#include +#include +#pragma comment(lib, "wsock32.lib") +#pragma comment(lib, "ws2_32.lib") +#endif + +/* + * This has an arch-like abstraction layout, since it's intended to become one. + * It'll be moved out of here as things get polished further. The Winsock and + * BSD implementations will be kept separate, since while they look similar, + * they're different in the details, especially when you want to cleanly support + * cancellation. + * + * Design notes: + * + * - Allow cancellation at any time. The only thing more annoying than a laggy + * network, is an application that won't immediately respond to cancelling. + * (This is even more important here, since we're using this after a crash.) + * - Assume some operations and/or some architectures are going to have problems + * doing everything nonblockingly, and do everything in a thread. This also allows + * more complex network interactions, since it doesn't need to maintain an equally + * complex state machine. + * - When an operation is cancelled or an error occurs, all operations until the + * next call to Close() become no-ops. This allows lenient error checking; it's + * guaranteed that further calls to Read or Write will not reset the error state. + * - The only function that may be called from another thread without serialization is + * Cancel(). + * + * All calls are blocking, except for: + * + * - Cancel(), which is always nonblocking. The operation in progress, if any, + * will be aborted immediately. (The only exception here is incomplete + * implementations, which may block); + * - Close(), if Shutdown() was called first, since the necessary blocking occurs + * during Shutdown; + * - Close(), if an error or cancellation occurred. + * + * Accepting incoming TCP connections is beyond the immediate scope of this interface + * (that would involve a second listener class, which would be a factory for NetworkStream). + * UDP is probably within the scope of this class, but there are some outstanding design + * issues and it's beyond the work I'm doing right now. + */ + +class NetworkStream +{ +public: + enum ConnectionType + { + CONN_TCP, + CONN_UDP, + }; + NetworkStream() { } + virtual ~NetworkStream() { } + + /* + * Open a connection. Must be in STATE_IDLE. + */ + virtual void Open( const RString &sHost, int iPort, ConnectionType ct = CONN_TCP ) = 0; + + /* + * Close down a connection. Returns to STATE_IDLE. + */ + virtual void Close() = 0; + + /* + * Wait for all sent data to be flushed, and shut down the connection. + */ + virtual void Shutdown() = 0; + + /* + * Read data. Block until any data is received, then return all data + * available. Return the number of bytes read. The return value will + * always be >= 0, unless an error or cancellation occured. + */ + virtual int Read( void *pBuffer, size_t iSize ) = 0; + + /* + * Write data to the socket. (Design note: we always write all of the + * data unless an error or cancellation occurs, and those states are checked + * with GetState(). If that happens, the number of bytes written is + * meaningless, since it may have simply been buffered and never sent. So, + * this function returns no value.) + */ + virtual void Write( const void *pBuffer, size_t iSize ) = 0; + + /* + * Cancel the connection. This operation can clear an error state, aborts + * any blocking calls, never fails, and will always result in the socket + * being in STATE_CANCELLED. + * + * This is true even if the state was STATE_IDLE. For example, the user + * thread may be closing one connection on this object and opening another, + * and the UI thread may call Cancel() in between these operations. + * + * This operation is threadsafe: a socket can be cancelled from any thread + * while another thread is reading or writing. This is the only function + * that can be called without serialization. + */ + virtual void Cancel() = 0; + + enum State + { + /* The stream is closed, and is ready to be opened. */ + STATE_IDLE, + + /* The stream is connected and able to send and receive data. */ + STATE_CONNECTED, + + /* The stream has been shut down on either end (either an EOF from + * the other end, or shutdown() being called). */ + STATE_SHUTDOWN, + + /* When in CANCELLED, all blocking calls fail immediately; this state must be + * cleared explicitly by calling Close(). */ + STATE_CANCELLED, + STATE_ERROR + }; + + State GetState() const { return m_State; } + RString GetError() const { return m_sError; } + +protected: + State m_State; + RString m_sError; +}; + +class NetworkStream_Win32: public NetworkStream +{ +public: + NetworkStream_Win32(); + ~NetworkStream_Win32(); + + void Open( const RString &sHost, int iPort, ConnectionType ct = CONN_TCP ); + void Shutdown(); + void Close(); + int Read( void *pBuffer, size_t iSize ); + void Write( const void *pBuffer, size_t iSize ); + + void Cancel(); + +private: + int WaitForCompletionOrCancellation( int iEvent ); + void SetError( const RString &sError ); + static RString WinSockErrorToString( int iError ); + + SOCKET m_Socket; + HANDLE m_hResolve; + HWND m_hResolveHwnd; + + /* This event is signalled on cancellation, to wake us up if we're blocking. */ + HANDLE m_hCompletionEvent; + + RString m_sHost; + int m_iPort; + + RageMutex m_Mutex; +}; + +NetworkStream *CreateNetworkStream() +{ + static bool bInitted = false; + if( !bInitted ) + { + bInitted = true; + WSADATA WSAData; + WORD iVersionRequested = MAKEWORD(2,0); + if( WSAStartup(iVersionRequested, &WSAData) != 0 ) + return NULL; + } + + return new NetworkStream_Win32; +} + +/* WinSock implementation of NetworkStream. */ +NetworkStream_Win32::NetworkStream_Win32(): + m_Mutex( "NetworkTCPSocket" ) +{ + m_iPort = -1; + m_State = STATE_IDLE; + m_Socket = NULL; +#if defined(WINDOWS) + m_hResolve = NULL; + m_hResolveHwnd = NULL; + m_hCompletionEvent = CreateEvent( NULL, true, false, NULL ); +#endif +} + +NetworkStream_Win32::~NetworkStream_Win32() +{ + Close(); + CloseHandle( m_hCompletionEvent ); +} + +/* Wait for the specified network event to occur, or cancellation, whichever + * happens first. On cancellation, return -1; on error, return the error + * code; on success, return 0. */ +int NetworkStream_Win32::WaitForCompletionOrCancellation( int iEvent ) +{ + while(1) + { + int iRet = WaitForSingleObject( m_hCompletionEvent, INFINITE ); + if( iRet != WAIT_OBJECT_0 ) + continue; + + m_Mutex.Lock(); + + /* This will reset the event. Do this while we hold the lock. */ + WSANETWORKEVENTS events; + WSAEnumNetworkEvents( m_Socket, m_hCompletionEvent, &events ); + + /* Was the event signalled due to cancellation? */ + if( m_State == STATE_CANCELLED ) + { + m_Mutex.Unlock(); + return -1; + } + + m_Mutex.Unlock(); + + /* If the socket was closed while we were waiting, stop. */ + if( (events.lNetworkEvents & (1<h_addr_list[0]; + addr.sin_family = PF_INET; + addr.sin_port = htons( (uint16_t) iPort ); + + m_Mutex.Lock(); + m_Socket = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ); + if( m_Socket == INVALID_SOCKET ) + { + int iError = WSAGetLastError(); + SetError( ssprintf("Error creating socket: %s", WinSockErrorToString(iError).c_str() ) ); + return; + } + + /* Set up the completion event to be signalled when these events occur. This + * also sets the socket to nonblocking. */ + WSAEventSelect( m_Socket, m_hCompletionEvent, FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ); + +// fcntl( m_Socket, O_NONBLOCK, 1 ); + + /* Start opening the connection. */ + int iResult = connect(m_Socket, (SOCKADDR *) &addr, sizeof(addr)); + + m_Mutex.Unlock(); + + /* We expect EINPROGRESS/WSAEWOULDBLOCK. */ + if( iResult == SOCKET_ERROR ) + { + /* Block until the connection attempt completes. */ + int iError = WSAGetLastError(); + if( iError == WSAEWOULDBLOCK ) + iError = WaitForCompletionOrCancellation( FD_CONNECT_BIT ); + if( iError ) + { + SetError( ssprintf("Couldn't connect: %s", WinSockErrorToString(iError).c_str() ) ); + return; + } + } + } + + m_State = STATE_CONNECTED; +} + +void NetworkStream_Win32::Close() +{ + if( m_State == STATE_IDLE ) + return; + + /* If we have an active, stable connection, make sure we flush any data + * completely before closing. If you don't want to do this, call Cancel() + * first. */ + Shutdown(); + + m_Mutex.Lock(); + if( m_State == STATE_CANCELLED ) + return; + + if( m_Socket != INVALID_SOCKET ) + closesocket( m_Socket ); + m_Socket = INVALID_SOCKET; + m_State = STATE_IDLE; + m_Mutex.Unlock(); +} + +void NetworkStream_Win32::Shutdown() +{ + /* If we're not in a normal, stable state (eg. an error occurred, or we were + * cancelled), don't wait to flush. */ + if( m_State != STATE_CONNECTED ) + return; + + shutdown( m_Socket, SD_BOTH ); +} + +void NetworkStream_Win32::Cancel() +{ + m_Mutex.Lock(); + + /* Mark cancellation. */ + m_State = STATE_CANCELLED; + + /* If resolving, abort the resolve. */ + if( m_hResolve != NULL ) + { + /* When we cancel the request, no message at all will be sent to the window, + * so we need to do it ourself to inform it that it was cancelled. Be sure + * to only do this on successful cancel. */ + if( WSACancelAsyncRequest(m_hResolve) == 0 ) + PostMessage( m_hResolveHwnd, WM_USER+1, 0, 0 ); + } + + /* Break out if we're waiting in WaitForCompletionOrCancellation(). */ + SetEvent( m_hCompletionEvent ); + + m_Mutex.Unlock(); +} + +int NetworkStream_Win32::Read( void *pBuffer, size_t iSize ) +{ + if( m_State != STATE_CONNECTED ) + return 0; + + char *p = (char *) pBuffer; + int iRead = 0; + while( iSize > 0 ) + { + int iRet = recv( m_Socket, p, iSize, 0 ); + if( iRet > 0 ) + { + p += iRet; + iSize -= iRet; + iRead += iRet; + continue; + } + + if( iRet == 0 ) + { + /* We hit EOF. */ + Shutdown(); + break; + } + + int iError = WSAGetLastError(); + if( iError == WSAEWOULDBLOCK ) + { + /* There's no date to read. If we've already received some data, return it. */ + if( iRead > 0 ) + break; + + iError = WaitForCompletionOrCancellation( FD_READ_BIT ); + if( iError == 0 ) + continue; + } + + /* If we're cancelled or hit an error while reading, do return the data we managed + * to get. Be sure not to overwrite CANCELLED with ERROR. */ + SetError( ssprintf("Error reading: %s", WinSockErrorToString(iError).c_str() ) ); + break; + } + + return iRead; +} + +void NetworkStream_Win32::Write( const void *pBuffer, size_t iSize ) +{ + if( m_State != STATE_CONNECTED ) + return; + + const char *p = (const char *) pBuffer; + while( iSize > 0 ) + { + int iRet = send( m_Socket, p, iSize, 0 ); + ASSERT( iRet != 0 ); + if( iRet > 0 ) + { + p += iRet; + iSize -= iRet; + continue; + } + + int iError = WSAGetLastError(); + if( iError == WSAEWOULDBLOCK ) + { + iError = WaitForCompletionOrCancellation( FD_WRITE_BIT ); + if( iError == 0 ) + continue; + } + + SetError( ssprintf("Error writing: %s", WinSockErrorToString(iError).c_str() ) ); + return; + } +} + +/* + * Send a set of data over HTTP, as a POST form. + */ + +NetworkPostData::NetworkPostData(): + m_Mutex( "NetworkPostData" ) +{ + m_pStream = CreateNetworkStream(); +} + +NetworkPostData::~NetworkPostData() +{ + delete m_pStream; +} + +/* Create a MIME multipart data block from the given set of fields. */ +void NetworkPostData::CreateMimeData( const map &mapNameToData, RString &sOut, RString &sMimeBoundaryOut ) +{ + /* Find a non-conflicting mime boundary. */ + while(1) + { + sMimeBoundaryOut = ssprintf( "--%08i", rand() ); + FOREACHM_CONST( RString, RString, mapNameToData, d ) + if( d->second.find(sMimeBoundaryOut) != RString::npos ) + continue; + break; + } + + FOREACHM_CONST( RString, RString, mapNameToData, d ) + { + sOut += "--" + sMimeBoundaryOut + "\r\n"; + sOut += ssprintf( "Content-Disposition: form-data; name=\"%s\"\r\n", d->first.c_str() ); + sOut += "\r\n"; + sOut += d->second; + sOut += "\r\n"; + } + if( sOut.size() ) + sOut += "--" + sMimeBoundaryOut + "--\r\n"; + +} + +void NetworkPostData::HttpThread() +{ + RString sData, sMimeBoundary; + CreateMimeData( m_Data, sData, sMimeBoundary ); + + // Stick to HTTP/1.0, since the protocol is simpler. + RString sBuf = ssprintf( + "%s %s HTTP/1.0\r\n" + "Accept: */*\r\n" + "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\r\n" + "Host: %s\r\n" + "Cache-Control: no-cache\r\n", + sData.size()? "POST":"GET", + m_sPath.c_str(), + m_sHost.c_str() ); + + if( sData.size() ) + { + // sBuf += "Content-Type: application/x-www-form-urlencoded\r\n" + sBuf += "Content-Type: multipart/form-data; boundary=" + sMimeBoundary + "\r\n"; + sBuf += ssprintf( "Content-Length: %i\r\n", sData.size() ); + } + sBuf += "\r\n"; + + if( sData.size() ) + sBuf += sData; + + /* The "progress" is currently faked; it shows when we've connected, and when + * we've received data. We send and receive too little data to do more. */ + SetProgress( 0 ); + + /* + * Begin connecting. + */ + m_pStream->Open( m_sHost, m_iPort ); + SetProgress( 0.25f ); + + /* Send the form. */ + m_pStream->Write( sBuf.data(), sBuf.size() ); + + /* Read the result. */ + RString sResult; + while( m_pStream->GetState() == NetworkStream::STATE_CONNECTED ) + { + sBuf.clear(); + void *p = sBuf.GetBuffer( 1024 ); + int iGot = m_pStream->Read( p, 1024 ); + if( iGot >= 0 ) + sBuf.ReleaseBuffer( iGot ); + if( iGot <= 0 ) + break; + sResult += sBuf; + } + + SetProgress( 1.0f ); + + /* Parse the results. */ + int iStart = 0, iSize = -1; + map mapHeaders; + while( 1 ) + { + split( sResult, "\n", iStart, iSize, false ); + if( iStart == (int) sResult.size() ) + break; + + RString sLine = sResult.substr( iStart, iSize ); + StripCrnl( sLine ); + if( sLine.empty() ) + { + m_sResult = sResult.substr( iStart+iSize+1 ); + break; + } + } + + m_bFinished = true; +} + +void NetworkPostData::Start( const RString &sHost, int iPort, const RString &sPath ) +{ + m_bFinished = false; + m_sHost = sHost; + m_iPort = iPort; + m_sPath = sPath; + m_sStatus.clear(); + m_fProgress = 0; + + m_Thread.SetName( "HTTP thread" ); + m_Thread.Create( HttpThread_Start, this ); +} + +void NetworkPostData::Cancel() +{ + m_pStream->Cancel(); + m_Thread.Wait(); +} + +bool NetworkPostData::IsFinished() +{ + if( !m_bFinished ) + return false; + + m_Thread.Wait(); + m_bFinished = false; + return true; +} + +RString NetworkPostData::GetStatus() const +{ + LockMut( m_Mutex ); + return m_sStatus; +} + +float NetworkPostData::GetProgress() const +{ + LockMut( m_Mutex ); + return m_fProgress; +} + +void NetworkPostData::SetProgress( float fProgress ) +{ + m_Mutex.Lock(); + m_fProgress = fProgress; + m_Mutex.Unlock(); +} + +void NetworkPostData::SetData( const RString &sKey, const RString &sData ) +{ + m_Data[sKey] = sData; +} + +/* + * (c) 2006 Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/stepmania/src/archutils/Win32/CrashHandlerNetworking.h b/stepmania/src/archutils/Win32/CrashHandlerNetworking.h new file mode 100644 index 0000000000..bca2013e27 --- /dev/null +++ b/stepmania/src/archutils/Win32/CrashHandlerNetworking.h @@ -0,0 +1,82 @@ +#ifndef CRASH_HANDLER_NETWORKING_H +#define CRASH_HANDLER_NETWORKING_H + +#include "RageThreads.h" +#include + +class NetworkStream; + +/* + * Send a set of data over HTTP, as a POST form. + */ +class NetworkPostData +{ +public: + NetworkPostData(); + ~NetworkPostData(); + + void SetData( const RString &sKey, const RString &sData ); + + /* For simplicity, we don't parse URLs here. */ + void Start( const RString &sHost, int iPort, const RString &sPath ); + + /* Cancel the running operation, and close the thread. */ + void Cancel(); + + /* If the operation is unfinished, return false. Otherwise, close the thread and return true. */ + bool IsFinished(); + + RString GetStatus() const; + float GetProgress() const; + RString GetResult() const { return m_sResult; } + +private: + static void CreateMimeData( const map &mapNameToData, RString &sOut, RString &sMimeBoundaryOut ); + void SetProgress( float fProgress ); + + RageThread m_Thread; + void HttpThread(); + static int HttpThread_Start( void *p ) { ((NetworkPostData *) p)->HttpThread(); return 0; } + + mutable RageMutex m_Mutex; + RString m_sStatus; + float m_fProgress; + + /* When the thread exists, it owns the rest of the data, regardless of m_Mutex. */ + map m_Data; + + bool m_bFinished; + RString m_sHost; + int m_iPort; + RString m_sPath; + RString m_sResult; + + NetworkStream *m_pStream; +}; + +#endif + +/* + * (c) 2006 Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */