You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1454 lines
40 KiB
1454 lines
40 KiB
/*==LICENSE==* |
|
|
|
CyanWorlds.com Engine - MMOG client, server and tools |
|
Copyright (C) 2011 Cyan Worlds, Inc. |
|
|
|
This program is free software: you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License as published by |
|
the Free Software Foundation, either version 3 of the License, or |
|
(at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
|
You can contact Cyan Worlds, Inc. by email legal@cyan.com |
|
or by snail mail at: |
|
Cyan Worlds, Inc. |
|
14617 N Newport Hwy |
|
Mead, WA 99021 |
|
|
|
*==LICENSE==*/ |
|
/***************************************************************************** |
|
* |
|
* $/Plasma20/Sources/Plasma/PubUtilLib/plNetGameLib/Private/plNglFile.cpp |
|
* |
|
***/ |
|
|
|
#include "../Pch.h" |
|
#pragma hdrstop |
|
|
|
// Define this if the file servers are running behind load-balancing hardware. |
|
// It changes the logic by which the decision to attempt a reconnect is made. |
|
#define LOAD_BALANCER_HARDWARE |
|
|
|
|
|
namespace Ngl { namespace File { |
|
/***************************************************************************** |
|
* |
|
* Private |
|
* |
|
***/ |
|
|
|
struct CliFileConn : AtomicRef { |
|
LINK(CliFileConn) link; |
|
CLock sockLock; // to protect the socket pointer so we don't nuke it while using it |
|
AsyncSocket sock; |
|
wchar name[MAX_PATH]; |
|
NetAddress addr; |
|
unsigned seq; |
|
ARRAY(byte) recvBuffer; |
|
AsyncCancelId cancelId; |
|
bool abandoned; |
|
unsigned buildId; |
|
unsigned serverType; |
|
|
|
CCritSect timerCritsect; // critsect for both timers |
|
|
|
// Reconnection |
|
AsyncTimer * reconnectTimer; |
|
unsigned reconnectStartMs; |
|
unsigned connectStartMs; |
|
unsigned numImmediateDisconnects; |
|
unsigned numFailedConnects; |
|
|
|
// Ping |
|
AsyncTimer * pingTimer; |
|
unsigned pingSendTimeMs; |
|
unsigned lastHeardTimeMs; |
|
|
|
CliFileConn (); |
|
~CliFileConn (); |
|
|
|
// This function should be called during object construction |
|
// to initiate connection attempts to the remote host whenever |
|
// the socket is disconnected. |
|
void AutoReconnect (); |
|
bool AutoReconnectEnabled () {return (reconnectTimer != nil);} |
|
void StopAutoReconnect (); // call before destruction |
|
void StartAutoReconnect (); |
|
void TimerReconnect (); |
|
|
|
// ping |
|
void AutoPing (); |
|
void StopAutoPing (); |
|
void TimerPing (); |
|
|
|
void Send (const void * data, unsigned bytes); |
|
|
|
void Destroy(); // cleans up the socket and buffer |
|
|
|
void Dispatch (const Cli2File_MsgHeader * msg); |
|
bool Recv_PingReply (const File2Cli_PingReply * msg); |
|
bool Recv_BuildIdReply (const File2Cli_BuildIdReply * msg); |
|
bool Recv_BuildIdUpdate (const File2Cli_BuildIdUpdate * msg); |
|
bool Recv_ManifestReply (const File2Cli_ManifestReply * msg); |
|
bool Recv_FileDownloadReply (const File2Cli_FileDownloadReply * msg); |
|
}; |
|
|
|
|
|
//============================================================================ |
|
// BuildIdRequestTrans |
|
//============================================================================ |
|
struct BuildIdRequestTrans : NetFileTrans { |
|
FNetCliFileBuildIdRequestCallback m_callback; |
|
void * m_param; |
|
|
|
unsigned m_buildId; |
|
|
|
BuildIdRequestTrans ( |
|
FNetCliFileBuildIdRequestCallback callback, |
|
void * param |
|
); |
|
|
|
bool Send (); |
|
void Post (); |
|
bool Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
); |
|
}; |
|
|
|
//============================================================================ |
|
// ManifestRequestTrans |
|
//============================================================================ |
|
struct ManifestRequestTrans : NetFileTrans { |
|
FNetCliFileManifestRequestCallback m_callback; |
|
void * m_param; |
|
wchar m_group[MAX_PATH]; |
|
unsigned m_buildId; |
|
|
|
ARRAY(NetCliFileManifestEntry) m_manifest; |
|
unsigned m_numEntriesReceived; |
|
|
|
ManifestRequestTrans ( |
|
FNetCliFileManifestRequestCallback callback, |
|
void * param, |
|
const wchar group[], |
|
unsigned buildId |
|
); |
|
|
|
bool Send (); |
|
void Post (); |
|
bool Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
); |
|
}; |
|
|
|
//============================================================================ |
|
// DownloadRequestTrans |
|
//============================================================================ |
|
struct DownloadRequestTrans : NetFileTrans { |
|
FNetCliFileDownloadRequestCallback m_callback; |
|
void * m_param; |
|
|
|
wchar m_filename[MAX_PATH]; |
|
hsStream * m_writer; |
|
unsigned m_buildId; |
|
|
|
unsigned m_totalBytesReceived; |
|
|
|
DownloadRequestTrans ( |
|
FNetCliFileDownloadRequestCallback callback, |
|
void * param, |
|
const wchar filename[], |
|
hsStream * writer, |
|
unsigned buildId |
|
); |
|
|
|
bool Send (); |
|
void Post (); |
|
bool Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
); |
|
}; |
|
|
|
//============================================================================ |
|
// RcvdFileDownloadChunkTrans |
|
//============================================================================ |
|
struct RcvdFileDownloadChunkTrans : NetNotifyTrans { |
|
|
|
unsigned bytes; |
|
byte * data; |
|
hsStream * writer; |
|
|
|
RcvdFileDownloadChunkTrans () : NetNotifyTrans (kFileRcvdFileDownloadChunkTrans) {} |
|
~RcvdFileDownloadChunkTrans (); |
|
void Post (); |
|
}; |
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* Private data |
|
* |
|
***/ |
|
|
|
enum { |
|
kPerfConnCount, |
|
kNumPerf |
|
}; |
|
|
|
static bool s_running; |
|
static CCritSect s_critsect; |
|
static LISTDECL(CliFileConn, link) s_conns; |
|
static CliFileConn * s_active; |
|
static long s_perf[kNumPerf]; |
|
static unsigned s_connectBuildId; |
|
static unsigned s_serverType; |
|
|
|
static FNetCliFileBuildIdUpdateCallback s_buildIdCallback = nil; |
|
|
|
const unsigned kMinValidConnectionMs = 25 * 1000; |
|
|
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* Internal functions |
|
* |
|
***/ |
|
|
|
//=========================================================================== |
|
static unsigned GetNonZeroTimeMs () { |
|
if (unsigned ms = TimeGetMs()) |
|
return ms; |
|
return 1; |
|
} |
|
|
|
//============================================================================ |
|
static CliFileConn * GetConnIncRef_CS (const char tag[]) { |
|
if (CliFileConn * conn = s_active) { |
|
conn->IncRef(tag); |
|
return conn; |
|
} |
|
return nil; |
|
} |
|
|
|
//============================================================================ |
|
static CliFileConn * GetConnIncRef (const char tag[]) { |
|
CliFileConn * conn; |
|
s_critsect.Enter(); |
|
{ |
|
conn = GetConnIncRef_CS(tag); |
|
} |
|
s_critsect.Leave(); |
|
return conn; |
|
} |
|
|
|
//============================================================================ |
|
static void UnlinkAndAbandonConn_CS (CliFileConn * conn) { |
|
s_conns.Unlink(conn); |
|
conn->abandoned = true; |
|
|
|
if (conn->AutoReconnectEnabled()) |
|
conn->StopAutoReconnect(); |
|
|
|
bool needsDecref = true; |
|
if (conn->cancelId) { |
|
AsyncSocketConnectCancel(nil, conn->cancelId); |
|
conn->cancelId = 0; |
|
needsDecref = false; |
|
} |
|
else { |
|
conn->sockLock.EnterRead(); |
|
if (conn->sock) { |
|
AsyncSocketDisconnect(conn->sock, true); |
|
needsDecref = false; |
|
} |
|
conn->sockLock.LeaveRead(); |
|
} |
|
if (needsDecref) { |
|
conn->DecRef("Lifetime"); |
|
} |
|
} |
|
|
|
//============================================================================ |
|
static void NotifyConnSocketConnect (CliFileConn * conn) { |
|
|
|
conn->TransferRef("Connecting", "Connected"); |
|
conn->connectStartMs = TimeGetMs(); |
|
conn->numFailedConnects = 0; |
|
|
|
// Make this the active server |
|
s_critsect.Enter(); |
|
{ |
|
if (!conn->abandoned) { |
|
conn->AutoPing(); |
|
s_active = conn; |
|
} |
|
else |
|
{ |
|
conn->sockLock.EnterRead(); |
|
AsyncSocketDisconnect(conn->sock, true); |
|
conn->sockLock.LeaveRead(); |
|
} |
|
} |
|
s_critsect.Leave(); |
|
} |
|
|
|
//============================================================================ |
|
static void NotifyConnSocketConnectFailed (CliFileConn * conn) { |
|
s_critsect.Enter(); |
|
{ |
|
conn->cancelId = 0; |
|
s_conns.Unlink(conn); |
|
|
|
if (conn == s_active) |
|
s_active = nil; |
|
} |
|
s_critsect.Leave(); |
|
|
|
// Cancel all transactions in progress on this connection. |
|
NetTransCancelByConnId(conn->seq, kNetErrTimeout); |
|
|
|
#ifndef SERVER |
|
// Client apps fail if unable to connect for a time |
|
if (++conn->numFailedConnects >= kMaxFailedConnects) { |
|
ReportNetError(kNetProtocolCli2File, kNetErrConnectFailed); |
|
} |
|
else |
|
#endif // ndef SERVER |
|
{ |
|
// start reconnect, if we are doing that |
|
if (s_running && conn->AutoReconnectEnabled()) |
|
conn->StartAutoReconnect(); |
|
else |
|
conn->DecRef("Lifetime"); // if we are not reconnecting, this socket is done, so remove the lifetime ref |
|
} |
|
conn->DecRef("Connecting"); |
|
} |
|
|
|
//============================================================================ |
|
static void NotifyConnSocketDisconnect (CliFileConn * conn) { |
|
conn->StopAutoPing(); |
|
s_critsect.Enter(); |
|
{ |
|
conn->cancelId = 0; |
|
s_conns.Unlink(conn); |
|
|
|
if (conn == s_active) |
|
s_active = nil; |
|
} |
|
s_critsect.Leave(); |
|
|
|
// Cancel all transactions in progress on this connection. |
|
NetTransCancelByConnId(conn->seq, kNetErrTimeout); |
|
|
|
|
|
bool notify = false; |
|
|
|
#ifdef SERVER |
|
{ |
|
if (TimeGetMs() - conn->connectStartMs > kMinValidConnectionMs) |
|
conn->reconnectStartMs = 0; |
|
else |
|
conn->reconnectStartMs = GetNonZeroTimeMs() + kMaxReconnectIntervalMs; |
|
} |
|
#else |
|
{ |
|
#ifndef LOAD_BALANCER_HARDWARE |
|
// If the connection to the remote server was open for longer than |
|
// kMinValidConnectionMs then assume that the connection was to |
|
// a valid server and try to perform reconnection immediately. If |
|
// less time elapsed then the connection was likely to a server |
|
// with an open port but with no notification procedure registered |
|
// for this type of communication channel. |
|
if (TimeGetMs() - conn->connectStartMs > kMinValidConnectionMs) { |
|
conn->reconnectStartMs = 0; |
|
} |
|
else { |
|
if (++conn->numImmediateDisconnects < kMaxImmediateDisconnects) |
|
conn->reconnectStartMs = GetNonZeroTimeMs() + kMaxReconnectIntervalMs; |
|
else |
|
notify = true; |
|
} |
|
#else |
|
// File server is running behind a load-balancer, so the next connection may |
|
// send us to a new server, therefore attempt a reconnection to the same |
|
// address even if the disconnect was immediate. This is safe because the |
|
// file server is stateless with respect to clients. |
|
if (TimeGetMs() - conn->connectStartMs <= kMinValidConnectionMs) { |
|
if (++conn->numImmediateDisconnects < kMaxImmediateDisconnects) |
|
conn->reconnectStartMs = GetNonZeroTimeMs() + kMaxReconnectIntervalMs; |
|
else |
|
notify = true; |
|
} |
|
else { |
|
// disconnect was not immediate. attempt a reconnect unless we're shutting down |
|
conn->numImmediateDisconnects = 0; |
|
conn->reconnectStartMs = 0; |
|
} |
|
#endif // LOAD_BALANCER |
|
} |
|
#endif // ndef SERVER |
|
|
|
if (notify) { |
|
ReportNetError(kNetProtocolCli2File, kNetErrDisconnected); |
|
} |
|
else { |
|
// clean up the socket and start reconnect, if we are doing that |
|
conn->Destroy(); |
|
if (conn->AutoReconnectEnabled()) |
|
conn->StartAutoReconnect(); |
|
else |
|
conn->DecRef("Lifetime"); // if we are not reconnecting, this socket is done, so remove the lifetime ref |
|
} |
|
|
|
conn->DecRef("Connected"); |
|
} |
|
|
|
//============================================================================ |
|
static bool NotifyConnSocketRead (CliFileConn * conn, AsyncNotifySocketRead * read) { |
|
conn->lastHeardTimeMs = GetNonZeroTimeMs(); |
|
conn->recvBuffer.Add(read->buffer, read->bytes); |
|
read->bytesProcessed += read->bytes; |
|
|
|
for (;;) { |
|
if (conn->recvBuffer.Count() < sizeof(dword)) |
|
return true; |
|
|
|
dword msgSize = *(dword *)conn->recvBuffer.Ptr(); |
|
if (conn->recvBuffer.Count() < msgSize) |
|
return true; |
|
|
|
const Cli2File_MsgHeader * msg = (const Cli2File_MsgHeader *) conn->recvBuffer.Ptr(); |
|
conn->Dispatch(msg); |
|
|
|
conn->recvBuffer.Move(0, msgSize, conn->recvBuffer.Count() - msgSize); |
|
conn->recvBuffer.ShrinkBy(msgSize); |
|
} |
|
} |
|
|
|
//============================================================================ |
|
static bool SocketNotifyCallback ( |
|
AsyncSocket sock, |
|
EAsyncNotifySocket code, |
|
AsyncNotifySocket * notify, |
|
void ** userState |
|
) { |
|
bool result = true; |
|
CliFileConn * conn; |
|
|
|
switch (code) { |
|
case kNotifySocketConnectSuccess: |
|
conn = (CliFileConn *) notify->param; |
|
*userState = conn; |
|
s_critsect.Enter(); |
|
{ |
|
conn->sockLock.EnterWrite(); |
|
conn->sock = sock; |
|
conn->sockLock.LeaveWrite(); |
|
conn->cancelId = 0; |
|
} |
|
s_critsect.Leave(); |
|
NotifyConnSocketConnect(conn); |
|
break; |
|
|
|
case kNotifySocketConnectFailed: |
|
conn = (CliFileConn *) notify->param; |
|
NotifyConnSocketConnectFailed(conn); |
|
break; |
|
|
|
case kNotifySocketDisconnect: |
|
conn = (CliFileConn *) *userState; |
|
NotifyConnSocketDisconnect(conn); |
|
break; |
|
|
|
case kNotifySocketRead: |
|
conn = (CliFileConn *) *userState; |
|
result = NotifyConnSocketRead(conn, (AsyncNotifySocketRead *) notify); |
|
break; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
//============================================================================ |
|
static void Connect (CliFileConn * conn) { |
|
ASSERT(s_running); |
|
|
|
conn->pingSendTimeMs = 0; |
|
|
|
s_critsect.Enter(); |
|
{ |
|
while (CliFileConn * oldConn = s_conns.Head()) { |
|
if (oldConn != conn) |
|
UnlinkAndAbandonConn_CS(oldConn); |
|
else |
|
s_conns.Unlink(oldConn); |
|
} |
|
s_conns.Link(conn); |
|
} |
|
s_critsect.Leave(); |
|
|
|
Cli2File_Connect connect; |
|
connect.hdr.connType = kConnTypeCliToFile; |
|
connect.hdr.hdrBytes = sizeof(connect.hdr); |
|
connect.hdr.buildId = kFileSrvBuildId; |
|
connect.hdr.buildType = BuildType(); |
|
connect.hdr.branchId = BranchId(); |
|
connect.hdr.productId = ProductId(); |
|
connect.data.buildId = conn->buildId; |
|
connect.data.serverType = conn->serverType; |
|
connect.data.dataBytes = sizeof(connect.data); |
|
|
|
AsyncSocketConnect( |
|
&conn->cancelId, |
|
conn->addr, |
|
SocketNotifyCallback, |
|
conn, |
|
&connect, |
|
sizeof(connect), |
|
0, |
|
0 |
|
); |
|
} |
|
|
|
//============================================================================ |
|
static void Connect ( |
|
const wchar name[], |
|
const NetAddress & addr |
|
) { |
|
ASSERT(s_running); |
|
|
|
CliFileConn * conn = NEWZERO(CliFileConn); |
|
StrCopy(conn->name, name, arrsize(conn->name)); |
|
conn->addr = addr; |
|
conn->buildId = s_connectBuildId; |
|
conn->serverType = s_serverType; |
|
conn->seq = ConnNextSequence(); |
|
conn->lastHeardTimeMs = GetNonZeroTimeMs(); // used in connect timeout, and ping timeout |
|
|
|
conn->IncRef("Lifetime"); |
|
conn->AutoReconnect(); |
|
} |
|
|
|
//============================================================================ |
|
static void AsyncLookupCallback ( |
|
void * param, |
|
const wchar name[], |
|
unsigned addrCount, |
|
const NetAddress addrs[] |
|
) { |
|
if (!addrCount) { |
|
ReportNetError(kNetProtocolCli2File, kNetErrNameLookupFailed); |
|
return; |
|
} |
|
|
|
for (unsigned i = 0; i < addrCount; ++i) { |
|
Connect(name, addrs[i]); |
|
} |
|
} |
|
|
|
/***************************************************************************** |
|
* |
|
* CliFileConn |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
CliFileConn::CliFileConn () { |
|
AtomicAdd(&s_perf[kPerfConnCount], 1); |
|
} |
|
|
|
//============================================================================ |
|
CliFileConn::~CliFileConn () { |
|
ASSERT(!cancelId); |
|
ASSERT(!reconnectTimer); |
|
Destroy(); |
|
AtomicAdd(&s_perf[kPerfConnCount], -1); |
|
} |
|
|
|
//=========================================================================== |
|
void CliFileConn::TimerReconnect () { |
|
ASSERT(!sock); |
|
ASSERT(!cancelId); |
|
|
|
if (!s_running) { |
|
s_critsect.Enter(); |
|
UnlinkAndAbandonConn_CS(this); |
|
s_critsect.Leave(); |
|
} |
|
else { |
|
IncRef("Connecting"); |
|
|
|
// Remember the time we started the reconnect attempt, guarding against |
|
// TimeGetMs() returning zero (unlikely), as a value of zero indicates |
|
// a first-time connect condition to StartAutoReconnect() |
|
reconnectStartMs = GetNonZeroTimeMs(); |
|
|
|
Connect(this); |
|
} |
|
} |
|
|
|
//=========================================================================== |
|
static unsigned CliFileConnTimerReconnectProc (void * param) { |
|
((CliFileConn *) param)->TimerReconnect(); |
|
return kAsyncTimeInfinite; |
|
} |
|
|
|
//=========================================================================== |
|
// This function is called when after a disconnect to start a new connection |
|
void CliFileConn::StartAutoReconnect () { |
|
timerCritsect.Enter(); |
|
if (reconnectTimer) { |
|
// Make reconnect attempts at regular intervals. If the last attempt |
|
// took more than the specified max interval time then reconnect |
|
// immediately; otherwise wait until the time interval is up again |
|
// then reconnect. |
|
unsigned remainingMs = 0; |
|
if (reconnectStartMs) { |
|
remainingMs = reconnectStartMs - GetNonZeroTimeMs(); |
|
if ((signed)remainingMs < 0) |
|
remainingMs = 0; |
|
} |
|
AsyncTimerUpdate(reconnectTimer, remainingMs); |
|
} |
|
timerCritsect.Leave(); |
|
} |
|
|
|
//=========================================================================== |
|
// This function should be called during object construction |
|
// to initiate connection attempts to the remote host whenever |
|
// the socket is disconnected. |
|
void CliFileConn::AutoReconnect () { |
|
timerCritsect.Enter(); |
|
{ |
|
ASSERT(!reconnectTimer); |
|
IncRef("ReconnectTimer"); |
|
AsyncTimerCreate( |
|
&reconnectTimer, |
|
CliFileConnTimerReconnectProc, |
|
0, // immediate callback |
|
this |
|
); |
|
} |
|
timerCritsect.Leave(); |
|
} |
|
|
|
//=========================================================================== |
|
static unsigned CliFileConnTimerDestroyed (void * param) { |
|
CliFileConn * sock = (CliFileConn *) param; |
|
sock->DecRef("TimerDestroyed"); |
|
return kAsyncTimeInfinite; |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::StopAutoReconnect () { |
|
timerCritsect.Enter(); |
|
{ |
|
if (AsyncTimer * timer = reconnectTimer) { |
|
reconnectTimer = nil; |
|
AsyncTimerDeleteCallback(timer, CliFileConnTimerDestroyed); |
|
} |
|
} |
|
timerCritsect.Leave(); |
|
} |
|
|
|
//=========================================================================== |
|
static unsigned CliFileConnPingTimerProc (void * param) { |
|
((CliFileConn *) param)->TimerPing(); |
|
return kPingIntervalMs; |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::AutoPing () { |
|
ASSERT(!pingTimer); |
|
IncRef("PingTimer"); |
|
timerCritsect.Enter(); |
|
{ |
|
sockLock.EnterRead(); |
|
unsigned timerPeriod = sock ? 0 : kAsyncTimeInfinite; |
|
sockLock.LeaveRead(); |
|
|
|
AsyncTimerCreate( |
|
&pingTimer, |
|
CliFileConnPingTimerProc, |
|
timerPeriod, |
|
this |
|
); |
|
} |
|
timerCritsect.Leave(); |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::StopAutoPing () { |
|
timerCritsect.Enter(); |
|
{ |
|
if (AsyncTimer * timer = pingTimer) { |
|
pingTimer = nil; |
|
AsyncTimerDeleteCallback(timer, CliFileConnTimerDestroyed); |
|
} |
|
} |
|
timerCritsect.Leave(); |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::TimerPing () { |
|
sockLock.EnterRead(); |
|
for (;;) { |
|
if (!sock) // make sure it exists |
|
break; |
|
#if 0 |
|
// if the time difference between when we last sent a ping and when we last |
|
// heard from the server is >= 3x the ping interval, the socket is stale. |
|
if (pingSendTimeMs && abs(int(pingSendTimeMs - lastHeardTimeMs)) >= kPingTimeoutMs) { |
|
// ping timed out, disconnect the socket |
|
AsyncSocketDisconnect(sock, true); |
|
} |
|
else |
|
#endif |
|
{ |
|
// Send a ping request |
|
pingSendTimeMs = GetNonZeroTimeMs(); |
|
|
|
Cli2File_PingRequest msg; |
|
msg.messageId = kCli2File_PingRequest; |
|
msg.messageBytes = sizeof(msg); |
|
msg.pingTimeMs = pingSendTimeMs; |
|
|
|
// read locks are reentrant, so calling Send is ok here within the read lock |
|
Send(&msg, msg.messageBytes); |
|
} |
|
break; |
|
} |
|
sockLock.LeaveRead(); |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::Destroy () { |
|
AsyncSocket oldSock = nil; |
|
|
|
sockLock.EnterWrite(); |
|
{ |
|
SWAP(oldSock, sock); |
|
} |
|
sockLock.LeaveWrite(); |
|
|
|
if (oldSock) |
|
AsyncSocketDelete(oldSock); |
|
recvBuffer.Clear(); |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::Send (const void * data, unsigned bytes) { |
|
sockLock.EnterRead(); |
|
if (sock) { |
|
AsyncSocketSend(sock, data, bytes); |
|
} |
|
sockLock.LeaveRead(); |
|
} |
|
|
|
//============================================================================ |
|
void CliFileConn::Dispatch (const Cli2File_MsgHeader * msg) { |
|
|
|
#define DISPATCH(a) case kFile2Cli_##a: Recv_##a((const File2Cli_##a *) msg); break |
|
switch (msg->messageId) { |
|
DISPATCH(PingReply); |
|
DISPATCH(BuildIdReply); |
|
DISPATCH(BuildIdUpdate); |
|
DISPATCH(ManifestReply); |
|
DISPATCH(FileDownloadReply); |
|
DEFAULT_FATAL(msg->messageId) |
|
} |
|
#undef DISPATCH |
|
} |
|
|
|
//============================================================================ |
|
bool CliFileConn::Recv_PingReply ( |
|
const File2Cli_PingReply * msg |
|
) { |
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
bool CliFileConn::Recv_BuildIdReply ( |
|
const File2Cli_BuildIdReply * msg |
|
) { |
|
NetTransRecv(msg->transId, (const byte *)msg, msg->messageBytes); |
|
|
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
bool CliFileConn::Recv_BuildIdUpdate ( |
|
const File2Cli_BuildIdUpdate * msg |
|
) { |
|
if (s_buildIdCallback) |
|
s_buildIdCallback(msg->buildId); |
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
bool CliFileConn::Recv_ManifestReply ( |
|
const File2Cli_ManifestReply * msg |
|
) { |
|
NetTransRecv(msg->transId, (const byte *)msg, msg->messageBytes); |
|
|
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
bool CliFileConn::Recv_FileDownloadReply ( |
|
const File2Cli_FileDownloadReply * msg |
|
) { |
|
NetTransRecv(msg->transId, (const byte *)msg, msg->messageBytes); |
|
|
|
return true; |
|
} |
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* BuildIdRequestTrans |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
BuildIdRequestTrans::BuildIdRequestTrans ( |
|
FNetCliFileBuildIdRequestCallback callback, |
|
void * param |
|
) : NetFileTrans(kBuildIdRequestTrans) |
|
, m_callback(callback) |
|
, m_param(param) |
|
{} |
|
|
|
//============================================================================ |
|
bool BuildIdRequestTrans::Send () { |
|
if (!AcquireConn()) |
|
return false; |
|
|
|
Cli2File_BuildIdRequest buildIdReq; |
|
buildIdReq.messageId = kCli2File_BuildIdRequest; |
|
buildIdReq.transId = m_transId; |
|
buildIdReq.messageBytes = sizeof(buildIdReq); |
|
|
|
m_conn->Send(&buildIdReq, buildIdReq.messageBytes); |
|
|
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
void BuildIdRequestTrans::Post () { |
|
m_callback(m_result, m_param, m_buildId); |
|
} |
|
|
|
//============================================================================ |
|
bool BuildIdRequestTrans::Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
) { |
|
const File2Cli_BuildIdReply & reply = *(const File2Cli_BuildIdReply *) msg; |
|
|
|
if (IS_NET_ERROR(reply.result)) { |
|
// we have a problem... |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
return true; |
|
} |
|
|
|
m_buildId = reply.buildId; |
|
|
|
// mark as complete |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
|
|
return true; |
|
} |
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* ManifestRequestTrans |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
ManifestRequestTrans::ManifestRequestTrans ( |
|
FNetCliFileManifestRequestCallback callback, |
|
void * param, |
|
const wchar group[], |
|
unsigned buildId |
|
) : NetFileTrans(kManifestRequestTrans) |
|
, m_callback(callback) |
|
, m_param(param) |
|
, m_numEntriesReceived(0) |
|
, m_buildId(buildId) |
|
{ |
|
if (group) |
|
StrCopy(m_group, group, arrsize(m_group)); |
|
else |
|
m_group[0] = L'\0'; |
|
} |
|
|
|
//============================================================================ |
|
bool ManifestRequestTrans::Send () { |
|
if (!AcquireConn()) |
|
return false; |
|
|
|
Cli2File_ManifestRequest manifestReq; |
|
StrCopy(manifestReq.group, m_group, arrsize(manifestReq.group)); |
|
manifestReq.messageId = kCli2File_ManifestRequest; |
|
manifestReq.transId = m_transId; |
|
manifestReq.messageBytes = sizeof(manifestReq); |
|
manifestReq.buildId = m_buildId; |
|
|
|
m_conn->Send(&manifestReq, manifestReq.messageBytes); |
|
|
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
void ManifestRequestTrans::Post () { |
|
m_callback(m_result, m_param, m_group, m_manifest.Ptr(), m_manifest.Count()); |
|
} |
|
|
|
//============================================================================ |
|
void ReadStringFromMsg(const wchar* curMsgPtr, wchar str[], unsigned maxStrLen, unsigned* length) { |
|
StrCopy(str, curMsgPtr, maxStrLen); |
|
str[maxStrLen - 1] = L'\0'; // make sure it's terminated |
|
|
|
(*length) = StrLen(str); |
|
} |
|
|
|
//============================================================================ |
|
void ReadUnsignedFromMsg(const wchar* curMsgPtr, unsigned* val) { |
|
(*val) = ((*curMsgPtr) << 16) + (*(curMsgPtr + 1)); |
|
} |
|
|
|
//============================================================================ |
|
bool ManifestRequestTrans::Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
) { |
|
m_timeoutAtMs = TimeGetMs() + NetTransGetTimeoutMs(); // Reset the timeout counter |
|
|
|
const File2Cli_ManifestReply & reply = *(const File2Cli_ManifestReply *) msg; |
|
|
|
dword numFiles = reply.numFiles; |
|
dword wcharCount = reply.wcharCount; |
|
const wchar* curChar = reply.manifestData; |
|
|
|
// tell the server we got the data |
|
Cli2File_ManifestEntryAck manifestAck; |
|
manifestAck.messageId = kCli2File_ManifestEntryAck; |
|
manifestAck.transId = reply.transId; |
|
manifestAck.messageBytes = sizeof(manifestAck); |
|
manifestAck.readerId = reply.readerId; |
|
|
|
m_conn->Send(&manifestAck, manifestAck.messageBytes); |
|
|
|
// if wcharCount is 2, the data only contains the terminator "\0\0" and we |
|
// don't need to convert anything (and we are done) |
|
if ((IS_NET_ERROR(reply.result)) || (wcharCount == 2)) { |
|
// we have a problem... or we have nothing to so, so we're done |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
return true; |
|
} |
|
|
|
if (numFiles > m_manifest.Count()) |
|
m_manifest.SetCount(numFiles); // reserve the space ahead of time |
|
|
|
// manifestData format: "clientFile\0downloadFile\0md5\0filesize\0zipsize\0flags\0...\0\0" |
|
bool done = false; |
|
while (!done) { |
|
if (wcharCount == 0) |
|
{ |
|
done = true; |
|
break; |
|
} |
|
|
|
// copy the data over to our array (m_numEntriesReceived is the current index) |
|
NetCliFileManifestEntry& entry = m_manifest[m_numEntriesReceived]; |
|
|
|
// -------------------------------------------------------------------- |
|
// read in the clientFilename |
|
unsigned filenameLen; |
|
ReadStringFromMsg(curChar, entry.clientName, arrsize(entry.clientName), &filenameLen); |
|
curChar += filenameLen; // advance the pointer |
|
wcharCount -= filenameLen; // keep track of the amount remaining |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // something is screwy, abort and disconnect |
|
|
|
// point it at the downloadFile |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
// read in the downloadFilename |
|
ReadStringFromMsg(curChar, entry.downloadName, arrsize(entry.downloadName), &filenameLen); |
|
curChar += filenameLen; // advance the pointer |
|
wcharCount -= filenameLen; // keep track of the amount remaining |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // something is screwy, abort and disconnect |
|
|
|
// point it at the md5 |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
// read in the md5 |
|
ReadStringFromMsg(curChar, entry.md5, arrsize(entry.md5), &filenameLen); |
|
curChar += filenameLen; // advance the pointer |
|
wcharCount -= filenameLen; // keep track of the amount remaining |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // something is screwy, abort and disconnect |
|
|
|
// point it at the md5 for compressed files |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
// read in the md5 for compressed files |
|
ReadStringFromMsg(curChar, entry.md5compressed, arrsize(entry.md5compressed), &filenameLen); |
|
curChar += filenameLen; // advance the pointer |
|
wcharCount -= filenameLen; // keep track of the amount remaining |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // something is screwy, abort and disconnect |
|
|
|
// point it at the first part of the filesize value (format: 0xHHHHLLLL) |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
if (wcharCount < 2) // we have to have 2 chars for the size |
|
return false; // screwy data |
|
ReadUnsignedFromMsg(curChar, &entry.fileSize); |
|
curChar += 2; |
|
wcharCount -= 2; |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // screwy data |
|
|
|
// point it at the first part of the zipsize value (format: 0xHHHHLLLL) |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
if (wcharCount < 2) // we have to have 2 chars for the size |
|
return false; // screwy data |
|
ReadUnsignedFromMsg(curChar, &entry.zipSize); |
|
curChar += 2; |
|
wcharCount -= 2; |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // screwy data |
|
|
|
// point it at the first part of the flags value (format: 0xHHHHLLLL) |
|
curChar++; |
|
wcharCount--; |
|
|
|
// -------------------------------------------------------------------- |
|
if (wcharCount < 2) // we have to have 2 chars for the size |
|
return false; // screwy data |
|
ReadUnsignedFromMsg(curChar, &entry.flags); |
|
curChar += 2; |
|
wcharCount -= 2; |
|
if ((*curChar != L'\0') || (wcharCount <= 0)) |
|
return false; // screwy data |
|
|
|
// -------------------------------------------------------------------- |
|
// point it at either the second part of the terminator, or the next filename |
|
curChar++; |
|
wcharCount--; |
|
|
|
// do sanity checking |
|
if (*curChar == L'\0') { |
|
// we hit the terminator |
|
if (wcharCount != 1) |
|
return false; // invalid data, we shouldn't have any more |
|
done = true; // we're done |
|
} |
|
else if (wcharCount < 14) |
|
// we must have at least three 1-char strings, three nulls, three 32-bit ints, and 2-char terminator left (3+3+6+2) |
|
return false; // screwy data |
|
|
|
// increment entries received |
|
m_numEntriesReceived++; |
|
if ((m_numEntriesReceived >= numFiles) && !done) { |
|
// too much data, abort |
|
return false; |
|
} |
|
} |
|
|
|
// check for completion |
|
if (m_numEntriesReceived >= numFiles) |
|
{ |
|
// all entires received, mark as complete |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
} |
|
return true; |
|
} |
|
|
|
/***************************************************************************** |
|
* |
|
* FileDownloadRequestTrans |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
DownloadRequestTrans::DownloadRequestTrans ( |
|
FNetCliFileDownloadRequestCallback callback, |
|
void * param, |
|
const wchar filename[], |
|
hsStream * writer, |
|
unsigned buildId |
|
) : NetFileTrans(kDownloadRequestTrans) |
|
, m_callback(callback) |
|
, m_param(param) |
|
, m_writer(writer) |
|
, m_totalBytesReceived(0) |
|
, m_buildId(buildId) |
|
{ |
|
StrCopy(m_filename, filename, arrsize(m_filename)); |
|
// This transaction issues "sub transactions" which must complete |
|
// before this one even though they were issued after us. |
|
m_hasSubTrans = true; |
|
} |
|
|
|
//============================================================================ |
|
bool DownloadRequestTrans::Send () { |
|
if (!AcquireConn()) |
|
return false; |
|
|
|
Cli2File_FileDownloadRequest filedownloadReq; |
|
StrCopy(filedownloadReq.filename, m_filename, arrsize(m_filename)); |
|
filedownloadReq.messageId = kCli2File_FileDownloadRequest; |
|
filedownloadReq.transId = m_transId; |
|
filedownloadReq.messageBytes = sizeof(filedownloadReq); |
|
filedownloadReq.buildId = m_buildId; |
|
|
|
m_conn->Send(&filedownloadReq, sizeof(filedownloadReq)); |
|
|
|
return true; |
|
} |
|
|
|
//============================================================================ |
|
void DownloadRequestTrans::Post () { |
|
m_callback(m_result, m_param, m_filename, m_writer); |
|
} |
|
|
|
//============================================================================ |
|
bool DownloadRequestTrans::Recv ( |
|
const byte msg[], |
|
unsigned bytes |
|
) { |
|
m_timeoutAtMs = TimeGetMs() + NetTransGetTimeoutMs(); // Reset the timeout counter |
|
|
|
const File2Cli_FileDownloadReply & reply = *(const File2Cli_FileDownloadReply *) msg; |
|
|
|
dword byteCount = reply.byteCount; |
|
const byte* data = reply.fileData; |
|
|
|
// tell the server we got the data |
|
Cli2File_FileDownloadChunkAck fileAck; |
|
fileAck.messageId = kCli2File_FileDownloadChunkAck; |
|
fileAck.transId = reply.transId; |
|
fileAck.messageBytes = sizeof(fileAck); |
|
fileAck.readerId = reply.readerId; |
|
|
|
m_conn->Send(&fileAck, fileAck.messageBytes); |
|
|
|
if (IS_NET_ERROR(reply.result)) { |
|
// we have a problem... indicate we are done and abort |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
return true; |
|
} |
|
|
|
// we have data to write, so queue it for write in the main thread (we're |
|
// currently in a net recv thread) |
|
if (byteCount > 0) { |
|
RcvdFileDownloadChunkTrans * writeTrans = NEW(RcvdFileDownloadChunkTrans); |
|
writeTrans->writer = m_writer; |
|
writeTrans->bytes = byteCount; |
|
writeTrans->data = (byte *)ALLOC(byteCount); |
|
MemCopy(writeTrans->data, data, byteCount); |
|
NetTransSend(writeTrans); |
|
} |
|
m_totalBytesReceived += byteCount; |
|
|
|
if (m_totalBytesReceived >= reply.totalFileSize) { |
|
// all bytes received, mark as complete |
|
m_result = reply.result; |
|
m_state = kTransStateComplete; |
|
} |
|
return true; |
|
} |
|
|
|
/***************************************************************************** |
|
* |
|
* RcvdFileDownloadChunkTrans |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
RcvdFileDownloadChunkTrans::~RcvdFileDownloadChunkTrans () { |
|
FREE(data); |
|
} |
|
|
|
//============================================================================ |
|
void RcvdFileDownloadChunkTrans::Post () { |
|
writer->Write(bytes, data); |
|
m_result = kNetSuccess; |
|
m_state = kTransStateComplete; |
|
} |
|
|
|
|
|
} using namespace File; |
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* NetFileTrans |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
NetFileTrans::NetFileTrans (ETransType transType) |
|
: NetTrans(kNetProtocolCli2File, transType) |
|
, m_conn(nil) |
|
{ |
|
} |
|
|
|
//============================================================================ |
|
NetFileTrans::~NetFileTrans () { |
|
ReleaseConn(); |
|
} |
|
|
|
//============================================================================ |
|
bool NetFileTrans::AcquireConn () { |
|
if (!m_conn) |
|
m_conn = GetConnIncRef("AcquireConn"); |
|
return m_conn != nil; |
|
} |
|
|
|
//============================================================================ |
|
void NetFileTrans::ReleaseConn () { |
|
if (m_conn) { |
|
m_conn->DecRef("AcquireConn"); |
|
m_conn = nil; |
|
} |
|
} |
|
|
|
|
|
/***************************************************************************** |
|
* |
|
* Protected functions |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
void FileInitialize () { |
|
s_running = true; |
|
} |
|
|
|
//============================================================================ |
|
void FileDestroy (bool wait) { |
|
s_running = false; |
|
|
|
NetTransCancelByProtocol( |
|
kNetProtocolCli2File, |
|
kNetErrRemoteShutdown |
|
); |
|
NetMsgProtocolDestroy( |
|
kNetProtocolCli2File, |
|
false |
|
); |
|
|
|
s_critsect.Enter(); |
|
{ |
|
while (CliFileConn * conn = s_conns.Head()) |
|
UnlinkAndAbandonConn_CS(conn); |
|
s_active = nil; |
|
} |
|
s_critsect.Leave(); |
|
|
|
if (!wait) |
|
return; |
|
|
|
while (s_perf[kPerfConnCount]) { |
|
NetTransUpdate(); |
|
AsyncSleep(10); |
|
} |
|
} |
|
|
|
//============================================================================ |
|
bool FileQueryConnected () { |
|
bool result; |
|
s_critsect.Enter(); |
|
result = s_active != nil; |
|
s_critsect.Leave(); |
|
return result; |
|
} |
|
|
|
//============================================================================ |
|
unsigned FileGetConnId () { |
|
unsigned connId; |
|
s_critsect.Enter(); |
|
connId = (s_active) ? s_active->seq : 0; |
|
s_critsect.Leave(); |
|
return connId; |
|
} |
|
|
|
} using namespace Ngl; |
|
|
|
/***************************************************************************** |
|
* |
|
* Exported functions |
|
* |
|
***/ |
|
|
|
//============================================================================ |
|
void NetCliFileStartConnect ( |
|
const wchar * fileAddrList[], |
|
unsigned fileAddrCount, |
|
bool isPatcher /* = false */ |
|
) { |
|
// TEMP: Only connect to one file server until we fill out this module |
|
// to choose the "best" file connection. |
|
fileAddrCount = min(fileAddrCount, 1); |
|
s_connectBuildId = isPatcher ? kFileSrvBuildId : BuildId(); |
|
s_serverType = kSrvTypeNone; |
|
|
|
for (unsigned i = 0; i < fileAddrCount; ++i) { |
|
// Do we need to lookup the address? |
|
const wchar * name = fileAddrList[i]; |
|
while (unsigned ch = *name) { |
|
++name; |
|
if (!(isdigit(ch) || ch == L'.' || ch == L':')) { |
|
AsyncCancelId cancelId; |
|
AsyncAddressLookupName( |
|
&cancelId, |
|
AsyncLookupCallback, |
|
fileAddrList[i], |
|
kNetDefaultClientPort, |
|
nil |
|
); |
|
break; |
|
} |
|
} |
|
if (!name[0]) { |
|
NetAddress addr; |
|
NetAddressFromString(&addr, fileAddrList[i], kNetDefaultClientPort); |
|
Connect(fileAddrList[i], addr); |
|
} |
|
} |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileStartConnectAsServer ( |
|
const wchar * fileAddrList[], |
|
unsigned fileAddrCount, |
|
unsigned serverType, |
|
unsigned serverBuildId |
|
) { |
|
// TEMP: Only connect to one file server until we fill out this module |
|
// to choose the "best" file connection. |
|
fileAddrCount = min(fileAddrCount, 1); |
|
s_connectBuildId = serverBuildId; |
|
s_serverType = serverType; |
|
|
|
for (unsigned i = 0; i < fileAddrCount; ++i) { |
|
// Do we need to lookup the address? |
|
const wchar * name = fileAddrList[i]; |
|
while (unsigned ch = *name) { |
|
++name; |
|
if (!(isdigit(ch) || ch == L'.' || ch == L':')) { |
|
AsyncCancelId cancelId; |
|
AsyncAddressLookupName( |
|
&cancelId, |
|
AsyncLookupCallback, |
|
fileAddrList[i], |
|
kNetDefaultClientPort, |
|
nil |
|
); |
|
break; |
|
} |
|
} |
|
if (!name[0]) { |
|
NetAddress addr; |
|
NetAddressFromString(&addr, fileAddrList[i], kNetDefaultServerPort); |
|
Connect(fileAddrList[i], addr); |
|
} |
|
} |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileDisconnect () { |
|
s_critsect.Enter(); |
|
{ |
|
while (CliFileConn * conn = s_conns.Head()) |
|
UnlinkAndAbandonConn_CS(conn); |
|
s_active = nil; |
|
} |
|
s_critsect.Leave(); |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileBuildIdRequest ( |
|
FNetCliFileBuildIdRequestCallback callback, |
|
void * param |
|
) { |
|
BuildIdRequestTrans * trans = NEW(BuildIdRequestTrans)( |
|
callback, |
|
param |
|
); |
|
NetTransSend(trans); |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileRegisterBuildIdUpdate (FNetCliFileBuildIdUpdateCallback callback) { |
|
s_buildIdCallback = callback; |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileManifestRequest ( |
|
FNetCliFileManifestRequestCallback callback, |
|
void * param, |
|
const wchar group[], |
|
unsigned buildId /* = 0 */ |
|
) { |
|
ManifestRequestTrans * trans = NEW(ManifestRequestTrans)( |
|
callback, |
|
param, |
|
group, |
|
buildId |
|
); |
|
NetTransSend(trans); |
|
} |
|
|
|
//============================================================================ |
|
void NetCliFileDownloadRequest ( |
|
const wchar filename[], |
|
hsStream * writer, |
|
FNetCliFileDownloadRequestCallback callback, |
|
void * param, |
|
unsigned buildId /* = 0 */ |
|
) { |
|
DownloadRequestTrans * trans = NEW(DownloadRequestTrans)( |
|
callback, |
|
param, |
|
filename, |
|
writer, |
|
buildId |
|
); |
|
NetTransSend(trans); |
|
}
|
|
|