Introduction
You have surely tried remote debugging with Visual Studio. Here are my thoughts, the problems I encountered, the solutions I propose and a small free tool that you can use if you are bored to implement all the stuff yourself.
Remote debugging configuration
Remote debugging is not difficult. You deploy the binaries, along with their pdbs to the target machine, then connect to the remote debugger (msvsmon.exe) from Visual Studio and start debugging. However there are a few significant problems and, solving them will make your life much more easy, especially if the target computer user is a newbie.
Even if you have control of the remote computer (say, via a TeamViewer or my own HMN session), the ideas presented here will help you become more productive in remote debugging.
Problem #1: Are Visual Studio remote tools installed?
This is easy. The remote debugger installation path is found in either
- HKLM\SOFTWARE\Microsoft\VisualStudio\14.0 - InstallDir, or
- HKLM\SOFTWARE\Microsoft\VisualStudio\14.0\EnterpriseTools\QualityTools - InstallDir
Append to this directory:
- \Remote Debugger\x64\msvsmon.exe or
- \Remote Debugger\x86\msvsmon.exe
If the executable is not found there, then you need to install the remote tools. As of this writing, you can get them from
- https://download.microsoft.com/download/E/7/A/E7AEA696-A4EB-48DD-BA4A-9BE41A402400/rtools_setup_x64.exe
- https://download.microsoft.com/download/E/7/A/E7AEA696-A4EB-48DD-BA4A-9BE41A402400/rtools_setup_x86.exe
They have nice command line parameters for silent/unattended installation.
Problem #2: MSVSMON requires an incoming connection
The target PC is usually behind some stupid non UPnP NAT or some restrictive firewall or antivirus, so you will have a tough time guiding your client to configure all the stuff, or you will be forced to manipulate an unknown router over a remote session.
How much easier it would be if the connection could be reversed, so msvsmon would connect to your pc instead. However msvsmon does not support that.
The solution is to use my connection reverse method (a simple man in the middle). Let's say that your client listening endpoint is A1 (msvsmon) and you would like to connect from your B1 (Visual Studio) but NAT prevents it. You then start the following setup:
- A B2 listening point, reachable over your nat.
- A B3 listening point, used inside your lan and communicating internally with B2
- A A2 point in the remote machine.
Then, what happens?
- A2 connects to B2
- A2 connects to A1
- B1 connects to B3. All traffic that B3 receives is relayed to B2, which relays it to A2, which relays it to A1. A1 and B1 (the endpoints out of your control, msvsmon and Visual Studio) are unaware of this setup and think that they communicate directly.
This is my implementation of the above setup:
class CREV
{
public:
XSOCKET s2;
XSOCKET c1;
bool Compression = false;
#pragma pack(push,1)
enum P2PTYPE
{
CONNECT,
CONNECTRESULT,
DATA,
CDATA,
TERMINATE,
};
struct P2P
{
int Type = 0;
unsigned long long ID = 0;
int sz = 0;
int res = 0;
};
#pragma pack(pop)
CREV()
{
__nop();
}
int Port1 = 0;
int Port2 = 0;
unordered_map<unsigned long long, tuple<XSOCKET>> sessions;
size_t EraseSessionFromThread(unsigned long long id)
{
sessions.erase(id);
return 0;
};
void CompressB(vector<char>& d)
{
ZIPUTILS::ZIP z(d.data(), d.size());
vector<char> dd;
unsigned long long beforecompression = d.size();
z.MemCompress(d.data(), d.size(), dd);
dd.resize(dd.size() + 8);
memcpy(dd.data() + dd.size() - 8, &beforecompression, 8);
d = dd;
}
void UncompressB(vector<char>& d)
{
if (d.size() <= 8)
return;
unsigned long long bc = 0;
memcpy(&bc, d.data() + d.size() - 8, 8);
d.resize(d.size() - 8);
ZIPUTILS::ZIP z(d.data(), d.size());
vector<char> dd;
dd.resize(bc + 100);
z.MemUncompress(d.data(), d.size(), dd);
d = dd;
}
unsigned long long TotalBytesTransmitted = 0;
void EstablishedConnection(unsigned long long sid, int Mode)
{
auto& s = sessions[sid];
auto& x = get<0>(s);
vector<char> b(10000);
for (;;)
{
b.resize(10000);
int rv = x.receive(b.data(), 10000);
if (rv == 0 || rv == -1)
break;
b.resize(rv);
if (Compression)
CompressB(b);
XSOCKET& aa = (Mode == 0) ? s2 : c1;
P2P p;
p.Type = DATA;
if (Compression)
p.Type = CDATA;
p.ID = sid;
p.sz = b.size();
if (aa.transmit((char*)&p, sizeof(p), true) != sizeof(p))
break;
if (aa.transmit((char*)b.data(), b.size(), true) != b.size())
break;
TotalBytesTransmitted += b.size();
}
XSOCKET& aa = (Mode == 0) ? s2 : c1;
P2P p;
p.Type = TERMINATE;
p.ID = sid;
p.sz = 0;
aa.transmit((char*)&p, sizeof(p), true);
x.Close();
UWL::InterlockedCall2<size_t, CREV*,unsigned long long>(std::forward<std::function<size_t(CREV*,unsigned long long)>>(&CREV::EraseSessionFromThread), this,std::forward<unsigned long long>(sid));
}
void ServerListen1(int p)
{
UWL::XSOCKET s;
if (!s.BindAndListen(p))
return;
for (;;)
{
auto y2 = s.Accept();
if (y2 == INVALID_SOCKET)
break;
XSOCKET y = y2;
if (!s2.Valid())
{
y.Close();
continue;
}
P2P p;
p.Type = CONNECT;
p.ID = GetTickCount64();
p.sz = 0;
sessions[p.ID] = make_tuple<>(y);
if (s2.transmit((char*)&p, sizeof(p), true) != sizeof(p))
{
y.Close();
continue;
}
}
}
void ServerListen2(int port)
{
UWL::XSOCKET s;
if (!s.BindAndListen(port))
return;
s2 = s.Accept();
if (s2.operator SOCKET() == INVALID_SOCKET)
return;
s.Close();
P2P p;
vector<char> b;
for (;;)
{
int rv = s2.receive((char*)&p, sizeof(P2P));
if (rv == 0 || rv == -1)
break;
if (p.sz)
{
b.resize(p.sz);
rv = s2.receive((char*)b.data(), p.sz, true);
if (rv != p.sz)
break;
}
if (p.Type == TERMINATE)
{
get<0>(sessions[p.ID]).Close();
}
if (p.Type == DATA)
{
auto& sock = get<0>(sessions[p.ID]);
sock.transmit((char*)b.data(), b.size(), true);
}
if (p.Type == CDATA)
{
UncompressB(b);
auto& sock = get<0>(sessions[p.ID]);
sock.transmit((char*)b.data(), b.size(), true);
}
if (p.Type == CONNECTRESULT)
{
if (p.res == 0)
{
std::thread t(&CREV::EstablishedConnection, this,p.ID, 0);
t.detach();
}
else
{
UWL::InterlockedCall2<size_t, CREV*, unsigned long long>(std::forward<std::function<size_t(CREV*, unsigned long long)>>(&CREV::EraseSessionFromThread), this, std::forward<unsigned long long>(p.ID));
}
}
}
s2.Close();
}
void Server(int p1,int p2)
{
std::thread t1(&CREV::ServerListen1,this, p1);
t1.detach();
std::thread t2(&CREV::ServerListen2,this, p2);
t2.detach();
}
void Client(string server, int sport, string lserver, int lport)
{
XSOCKET Link;
if (!Link.ConnectTo(server.c_str(), sport))
return;
c1 = Link;
Link.CloseIf();
P2P p;
vector<char> b;
for (;;)
{
int rv = c1.receive((char*)&p, sizeof(P2P));
if (rv == 0 || rv == -1)
break;
if (p.sz)
{
b.resize(p.sz);
rv = c1.receive((char*)b.data(), p.sz, true);
if (rv != p.sz)
break;
}
if (p.Type == CONNECT)
{
XSOCKET a;
if (!a.ConnectTo(lserver.c_str(), lport))
{
P2P p2;
p2.Type = CONNECTRESULT;
p2.ID = p.ID;
p2.sz = 0;
p2.res = -1;
if (c1.transmit((char*)&p2, sizeof(p2), true) != sizeof(p2))
break;
continue;
}
sessions[p.ID] = make_tuple<>(a);
P2P p2;
p2.Type = CONNECTRESULT;
p2.ID = p.ID;
p2.sz = 0;
p2.res = 0;
if (c1.transmit((char*)&p2, sizeof(p2), true) != sizeof(p2))
break;
std::thread t(&CREV::EstablishedConnection, this,p.ID, 1);
t.detach();
}
if (p.Type == TERMINATE)
{
get<0>(sessions[p.ID]).Close();
}
if (p.Type == DATA)
{
auto& sock = get<0>(sessions[p.ID]);
sock.transmit((char*)b.data(), b.size(), true);
}
if (p.Type == CDATA)
{
UncompressB(b);
auto& sock = get<0>(sessions[p.ID]);
sock.transmit((char*)b.data(), b.size(), true);
}
}
c1.Close();
}
};
This implementation uses various components: Zip Utils, my own Atomic calls and a simple SOCKET wrapper.
Problem #3: MSVSMON traffic is uncompressed
This is easily solved with my above setup. Traffic between A2 and B2 (the network bottleneck) can be compressed and then it can be decompressed before passing it to A1 and B3. This way, uncompressed traffic only passes through the local networks.
In the above code you only need to enable the compress flag. Compression is transparent to A1 or B1.
Problem #4: Deployment is awfully slow.
A simple executable may be some 1MB along with an 6MB pdb file. Larger binaries can have lots of MB that have to be transferred, and even the slightest change in the code (something very common in debugging) will need the entire PDB to be deployed.
You have already saved lots with compression, but nevertheless, something must be done to avoid entire redeployment of the files when only small changes are present.
This time my DIFF library saves you. Based on the Remote Differential Compression API, it deploys only the changes in the binary files, saving lots of bandwidth and time.
Here is the algorithm, for each file you want to deploy:
- The server requests the client for the signature of an existing file.
- If the file is not found, the server uploads the entire file to the client.
- If the client finds the file, it calculates the signature and transmits it to the server:
-
DIFFLIB::DIFF diff;
HANDLE hX = CreateFile(LastFile.c_str() ,
GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
DIFFLIB::FileRdcFileReader fi(hX);
DIFFLIB::MemoryDiffWriter wr;
diff.GenerateSignature(&fi, wr);
CloseHandle(hX);
- The server generates the signature for the local (newer file), then calculates the diff and sends it to the client:
-
HANDLE hX = CreateFile(a.local.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
DIFFLIB::FileRdcFileReader MyFile(hX);
DIFFLIB::MemoryDiffWriter MySignature;
DIFFLIB::MemoryRdcFileReader RemoteSig(d.data(), d.size());
DIFFLIB::MemoryDiffWriter diffw;
diff.GenerateDiff(&RemoteSig, MySignature.GetReader(), &MyFile, diffw);
- The client then reconstructs the file from the old file and the diff:
-
HANDLE hY = CreateFile(File.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
DIFFLIB::FileRdcFileReader r1(hY);
DIFFLIB::MemoryRdcFileReader diffx(d.data(), d.size());
DIFFLIB::MemoryDiffWriter reco;
diff.Reconstruct(&r1, &diffx, 0, reco);
CloseHandle(hY);
hY = CreateFile(LastFile.c_str(), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
DWORD A = 0;
WriteFile(hY, reco.p().data(), (int)reco.sz(), &A, 0);
CloseHandle(hY);
and you are ready. You patched the file successfully.
My tool
I have implemented all this in a free for individual programmers tool, Remote Debugging Easy. Feel free to use it!
GOOD LUCK.
History
20/3/2016: First release.