Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

My remote debugging ideas with Visual Studio

5.00/5 (7 votes)
20 Mar 2016CPOL4 min read 19.6K  
My ideas about remote debugging and my implementation.

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);

            // Notify of data
            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();
            }

        // Notify of termination
        XSOCKET& aa = (Mode == 0) ? s2 : c1;
        P2P p;
        p.Type = TERMINATE;
        p.ID = sid;
        p.sz = 0;
        aa.transmit((char*)&p, sizeof(p), true);

        // Kill session
        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;

            // We order s2 to send a "connect request"
            if (!s2.Valid())
                {
                y.Close();
                continue;
                }

            //    UWL::debugprint("s1 accepted client, we will order a connect request\r\n");

            P2P p;
            p.Type = CONNECT;
            p.ID = GetTickCount64();
            p.sz = 0;

            // Create the session
            sessions[p.ID] = make_tuple<>(y);
            if (s2.transmit((char*)&p, sizeof(p), true) != sizeof(p))
                {
                y.Close();
                continue;
                }
            }
        }

    void ServerListen2(int port)
        {
        // This accepts connections from remote peer (control)
        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)
                {
                // Server orders us to terminate
                get<0>(sessions[p.ID]).Close();
                }

            if (p.Type == DATA)
                {
                // Send the received data from the server to our client
                auto& sock = get<0>(sessions[p.ID]);
                sock.transmit((char*)b.data(), b.size(), true);
                }

            if (p.Type == CDATA)
                {
                // Send the received data from the server to our client
                UncompressB(b);
                auto& sock = get<0>(sessions[p.ID]);
                sock.transmit((char*)b.data(), b.size(), true);
                }

            if (p.Type == CONNECTRESULT)
                {
                // Client notifies us that connection succeeded or failed
                if (p.res == 0)
                    {
                    // OK
                    std::thread t(&CREV::EstablishedConnection, this,p.ID, 0);
                    t.detach();
                    }
                else
                    {
                    // Kill session
                    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)
        {
        // Connect to server
        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)
                {
                // Server orders us to connect
                XSOCKET a;
                if (!a.ConnectTo(lserver.c_str(), lport))
                    {
                    // Failed ....
                    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;
                    }
                // Success
                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)
                {
                // Server orders us to terminate
                get<0>(sessions[p.ID]).Close();
                }

            if (p.Type == DATA)
                {
                // Send the received data from the server to our client
                auto& sock = get<0>(sessions[p.ID]);
                sock.transmit((char*)b.data(), b.size(), true);
                }

            if (p.Type == CDATA)
                {
                // Send the received data from the server to our client
                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); // wr.p() contains the signature in a vector  of char.
    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;
      // say it was stored in some buffer d
      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);
      // And now save reco to the file
      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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)