Exemplo n.º 1
0
        /// <summary>
        /// Main thread to host the client process.
        /// </summary>
        /// <param name="threadContext">A ZMQ.Context instance.</param>
        public void Run(object threadContext)
        {
            try
            {
                Context ctx = (Context)threadContext;

                // This would be a using statement on data and ctl but we need to close and reopen sockets, which means
                // we have to Dispose() of a socket early and create a new instance. C#, wisely so, prevents you from
                // reassigning a using variable.
                Socket data = null, ctl = null, disk = null, diskACK = null;
                try
                {
                    data = ctx.Socket(SocketType.SUB);
                    ctl = ctx.Socket(SocketType.REQ);
                    disk = new Socket(SocketType.PUSH);
                    diskACK = new Socket(SocketType.PULL);

                    // Set the HWM for the disk PUSH so that the PUSH blocks if the PULL can't keep up writing:
                    if (diskHWM > 0) disk.SNDHWM = diskHWM;
                    //disk.SndBuf = 1048576 * diskHWM * 4;

                    // inproc:// binds are immediate.
                    disk.Bind("inproc://disk");
                    diskACK.Bind("inproc://diskack");

                    // Create the disk PULL thread:
                    Thread diskWriterThread = new Thread(new ParameterizedThreadStart(DiskWriterThread));
                    diskWriterThread.Start(ctx);

                    data.RCVHWM = networkHWM;

                    //data.RcvBuf = 1048576 * hwm * 4;
                    // NOTE: work-around for MS bug in WinXP's networking stack. See http://support.microsoft.com/kb/201213 for details.
                    data.RcvBuf = 0;

                    if (tsp == Transport.EPGM || tsp == Transport.PGM)
                    {
                        data.Rate = pgmRate;
                    }

                    string address = tsp.ToString().ToLower() + "://" + deviceData + ":" + portData;
                    Console.WriteLine("Connect(\"{0}\")", address);
                    data.Connect(address);
                    data.Subscribe(subscription, Encoding.Unicode);

                    // Connect to the control request socket:
                    ctl.RcvBuf = 1048576 * 4;
                    ctl.SndBuf = 1048576 * 4;
                    address = "tcp://" + deviceCtl + ":" + portCtl.ToString();
                    Console.WriteLine("Connect(\"{0}\")", address);
                    ctl.Connect(address);

                    Guid myIdentity = Guid.NewGuid();
                    ctl.Identity = new byte[1] { (byte)'@' }.Concat(myIdentity.ToByteArray()).ToArray();

                    Thread.Sleep(500);

                    // Begin client logic:

                    naks = null;
                    numBytes = 0;
                    int ackCount = -1;
                    byte[] nakBuf = null;

                    bool shuttingDown = false;

                    Stopwatch recvTimer = Stopwatch.StartNew();
                    long lastElapsedMilliseconds = recvTimer.ElapsedMilliseconds;
                    int msgsRecv = 0, msgsWritten = 0;

                    try
                    {
                        // Poll for incoming messages on the data SUB socket:
                        PollItem[] pollItems = new PollItem[3];
                        pollItems[0] = data.CreatePollItem(IOMultiPlex.POLLIN);
                        pollItems[1] = ctl.CreatePollItem(IOMultiPlex.POLLIN /* | IOMultiPlex.POLLOUT */);
                        pollItems[2] = diskACK.CreatePollItem(IOMultiPlex.POLLIN);

                        ZMQStateMasheen<DataSUBState> dataFSM;
                        ZMQStateMasheen<ControlREQState> controlFSM;
                        Queue<QueuedControlMessage> controlStateQueue = new Queue<QueuedControlMessage>();

                        DateTimeOffset lastSentNAKs = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(60));

                        // We create a state machine to handle our send/recv state:
                        dataFSM = new ZMQStateMasheen<DataSUBState>(
                            // Set the initial state:
                            DataSUBState.Recv,
                            // Initial state handler:
                            new ZMQStateMasheen<DataSUBState>.State(DataSUBState.Recv, (sock, revents) =>
                            {
                                Queue<byte[]> packet = sock.RecvAll();

                                if (packet.Count == 0) { trace("FAIL!"); return DataSUBState.Recv; }
                                string sub = Encoding.Unicode.GetString(packet.Dequeue());
                                if (packet.Count == 0) { trace("FAIL!"); return DataSUBState.Recv; }
                                string cmd = Encoding.Unicode.GetString(packet.Dequeue());

                                if ((naks != null) && (cmd == "DATA"))
                                {
                                    if (packet.Count == 0) { trace("FAIL!"); return DataSUBState.Recv; }
                                    int chunkIdx = BitConverter.ToInt32(packet.Dequeue(), 0);

                                    // Count the number of messages received from the network:
                                    ++msgsRecv;
                                    if (ChunkReceived != null) ChunkReceived(this, chunkIdx);

                                    // Already received this chunk?
                                    if (!naks[chunkIdx])
                                    {
                                        trace("ALREADY RECV {0}", chunkIdx);
                                        return DataSUBState.Recv;
                                    }

                                    trace("RECV {0}", chunkIdx);

                                    // Queue up the disk writes with PUSH/PULL and an HWM on a separate thread to maintain as
                                    // constant disk write throughput as we can get... The HWM will enforce the PUSHer to block
                                    // when the PULLer cannot receive yet.
                                    if (packet.Count == 0) return DataSUBState.Recv;
                                    byte[] chunk = packet.Dequeue();
                                    disk.SendMore(cmdWrite);
                                    disk.SendMore(BitConverter.GetBytes(chunkIdx));
                                    // This will block if the disk queue gets full:
                                    disk.Send(chunk);
                                    chunk = null;

                                    return DataSUBState.Recv;
                                }
                                else if (cmd == "PING")
                                {
                                    if (shuttingDown) return DataSUBState.Recv;

                                    controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendALIVE, null));
                                    return DataSUBState.Recv;
                                }
                                else
                                {
                                    return DataSUBState.Recv;
                                }
                            })
                        );

                        // We create a state machine to handle our send/recv state:
                        object tmpControlState = null;
                        controlFSM = new ZMQStateMasheen<ControlREQState>(
                            ControlREQState.Nothing,
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.Nothing, (sock, revents) =>
                            {
                                // Do nothing state.
                                return ControlREQState.Nothing;
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.SendALIVE, (sock, revents) =>
                            {
                                if (shuttingDown) return ControlREQState.Nothing;

                                ctl.SendMore(ctl.Identity);
                                ctl.Send("ALIVE", Encoding.Unicode);

                                return new ZMQStateMasheen<ControlREQState>.MoveOperation(ControlREQState.RecvALIVE, false);
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.RecvALIVE, (sock, revents) =>
                            {
                                Queue<byte[]> packet = ctl.RecvAll();

                                if (packet.Count == 0) return ControlREQState.Nothing;
                                string cmd = Encoding.Unicode.GetString(packet.Dequeue());
                                //trace("Server: '{0}'", cmd);

                                if (cmd == "") return ControlREQState.Nothing;
                                else if (cmd == "WHOAREYOU") return new ZMQStateMasheen<ControlREQState>.MoveOperation(ControlREQState.SendJOIN, true);

                                return ControlREQState.Nothing;
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.SendJOIN, (sock, revents) =>
                            {
                                if (shuttingDown) return ControlREQState.Nothing;

                                // Send a JOIN request:
                                ctl.SendMore(ctl.Identity);
                                ctl.Send("JOIN", Encoding.Unicode);

                                return ControlREQState.RecvJOIN;
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.RecvJOIN, (sock, revents) =>
                            {
                                Queue<byte[]> packet = ctl.RecvAll();
                                if (packet.Count == 0) return ControlREQState.Nothing;

                                string resp = Encoding.Unicode.GetString(packet.Dequeue());
                                if (resp != "JOINED")
                                {
                                    // TODO: handle this failure.
                                    trace("FAIL!"); return ControlREQState.Nothing;
                                }

                                // TODO: make this atomically succeed or fail without corrupting state
                                if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                numChunks = BitConverter.ToInt32(packet.Dequeue(), 0);
                                numBytes = ((numChunks + 7) & ~7) >> 3;
                                nakBuf = new byte[numBytes];
                                if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                chunkSize = BitConverter.ToInt32(packet.Dequeue(), 0);
                                if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                int numFiles = BitConverter.ToInt32(packet.Dequeue(), 0);

                                List<TarballEntry> tbes = new List<TarballEntry>(numFiles);
                                for (int i = 0; i < numFiles; ++i)
                                {
                                    if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                    string fiName = Encoding.Unicode.GetString(packet.Dequeue());
                                    if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                    long length = BitConverter.ToInt64(packet.Dequeue(), 0);
                                    if (packet.Count == 0) { trace("FAIL!"); return ControlREQState.Nothing; }
                                    byte[] hash = packet.Dequeue();

                                    TarballEntry tbe = new TarballEntry(fiName, length, hash);
                                    tbes.Add(tbe);
                                }

                                lock (tarballLock)
                                {
                                    if (tarball != null)
                                    {
                                        tarball.Close();
                                        tarball.Dispose();
                                        tarball = null;
                                    }
                                }

                                lock (tarballLock)
                                {
                                    // Create the tarball reader that writes the files locally:
                                    tarball = new TarballStreamReader(downloadDirectory, tbes);

                                    // Get our local download state:
                                    naks = getClientState(this, tarball);
                                }
                                ackCount = naks.Cast<bool>().Take(numChunks).Count(b => !b);

                                return new ZMQStateMasheen<ControlREQState>.MoveOperation(ControlREQState.SendNAKS, true);
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.SendNAKS, (sock, revents) =>
                            {
                                if (shuttingDown) return ControlREQState.Nothing;

                                if (nakBuf == null) return ControlREQState.Nothing;

                                // Send our NAKs:
                                ctl.SendMore(ctl.Identity);
                                ctl.SendMore("NAKS", Encoding.Unicode);
                                // TODO: RLE!
                                naks.CopyTo(nakBuf, 0);
                                trace("SEND NAK");
                                ctl.Send(nakBuf);

                                return ControlREQState.RecvNAKS;
                            }),
                            new ZMQStateMasheen<ControlREQState>.State(ControlREQState.RecvNAKS, (sock, revents) =>
                            {
                                Queue<byte[]> packet = ctl.RecvAll();
                                // Don't care what the response is for now.

                                return ControlREQState.Nothing;
                            })
                        );

                        // Disk-write acknowledgement poll input handler:
                        pollItems[2].PollInHandler += new PollHandler((Socket sock, IOMultiPlex revents) =>
                        {
                            byte[] idxPkt = sock.RecvAll().Dequeue();

                            int chunkIdx = BitConverter.ToInt32(idxPkt, 0);
                            naks[chunkIdx] = false;

                            // Count the number of messages written to disk:
                            ++msgsWritten;
                            ++ackCount;

                            // Profiled - terrible performance. Not a big surprise here.
                            //ackCount = naks.Cast<bool>().Take(numChunks).Count(b => !b);

                            // If we ran up the timer, send more ACKs:
                            if (DateTimeOffset.UtcNow.Subtract(lastSentNAKs).TotalMilliseconds >= 250d)
                            {
                                lastSentNAKs = DateTimeOffset.UtcNow;
                                controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendNAKS, null));
                            }
                        });

                        pollItems[0].PollInHandler += new PollHandler(dataFSM.StateMasheen);
                        pollItems[1].PollInHandler += new PollHandler(controlFSM.StateMasheen);

                        DateTimeOffset lastRecv = DateTimeOffset.UtcNow;

                        // Start the control state machine with a JOIN:
                        controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendJOIN, null));

                        // Create a socket poller for the data socket:
                        while (true)
                        {
                            // If we disposed of the previous control socket, create a new one:
                            if (ctl == null)
                            {
                                trace("Creating new CONTROL socket");
                                // Set up new socket:
                                ctl = ctx.Socket(SocketType.REQ);
                                // Connect to the control request socket:
                                ctl.Connect("tcp://" + deviceCtl + ":" + portCtl.ToString());
                                ctl.Identity = new byte[1] { (byte)'@' }.Concat(myIdentity.ToByteArray()).ToArray();
                            }

                            if (!shuttingDown && IsSendState(controlFSM.CurrentState))
                            {
                                // If we ran up the timer, send more NAKs:
                                if (DateTimeOffset.UtcNow.Subtract(lastSentNAKs).TotalMilliseconds >= 200d)
                                {
                                    lastSentNAKs = DateTimeOffset.UtcNow;
                                    controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendNAKS, null));
                                }

                                if (controlStateQueue.Count > 0)
                                {
                                    var msg = controlStateQueue.Dequeue();

                                    // Roll up similar state requests:
                                    int dupes = 0;
                                    while (controlStateQueue.Count > 0)
                                    {
                                        if (controlStateQueue.Peek().NewState != msg.NewState) break;

                                        msg = controlStateQueue.Dequeue();
                                        ++dupes;
                                    }

                                    if (dupes > 0)
                                    {
                                        trace("Removed {0} redundant queued states", dupes);
                                    }

                                    tmpControlState = msg.Object;

                                    // Run the control state machine to SEND:
                                    controlFSM.CurrentState = msg.NewState;
                                    controlFSM.StateMasheen(ctl, IOMultiPlex.POLLOUT);
                                }
                            }

                            // Measure our message send rate per minute:
                            long elapsed = recvTimer.ElapsedMilliseconds - lastElapsedMilliseconds;
                            if (elapsed >= 100L)
                            {
                                NetworkRecvChunksPerMinute = (int)((msgsRecv * 60000L) / elapsed);
                                DiskWriteChunksPerMinute = (int)((msgsWritten * 60000L) / elapsed);
                                lastElapsedMilliseconds = recvTimer.ElapsedMilliseconds;
                                msgsRecv = 0;
                                msgsWritten = 0;
                            }

                            if (!recvTimer.IsRunning)
                            {
                                recvTimer.Reset();
                                recvTimer.Start();
                                lastElapsedMilliseconds = 0L;
                            }

                            if (ackCount >= numChunks)
                            {
                                disk.Send(cmdExit);
                                break;
                            }

                            if (ctx.Poll(pollItems, 100L) == 0)
                                Thread.Sleep(1);
                        }

                        // Wait for the disk writer to finish up:
                        shuttingDown = true;
                        diskWriterThread.Join();

                        // If we were last to await some response, receive it:
                        if (controlFSM.CurrentState == ControlREQState.RecvALIVE || controlFSM.CurrentState == ControlREQState.RecvJOIN || controlFSM.CurrentState == ControlREQState.RecvNAKS)
                        {
                            PollItem[] recvPoll = new PollItem[1];
                            recvPoll[0] = ctl.CreatePollItem(IOMultiPlex.POLLIN);
                            recvPoll[0].PollInHandler += new PollHandler(controlFSM.StateMasheen);

                            // Wait a bit to receive the response:
                            for (int i = 0; (i < 20) && ((ctx.Poll(recvPoll, 100) == 1) && (ctl != null)); ++i)
                            {
                            }
                        }

                        // Send the LEAVE message:
                        ctl.SendMore(ctl.Identity);
                        ctl.Send("LEAVE", Encoding.Unicode);
                        Completed = true;
                    }
                    finally
                    {
                        if (tarball != null) tarball.Dispose();
                    }
                }
                finally
                {
                    if (data != null) data.Dispose();
                    if (ctl != null) ctl.Dispose();
                }
            }
            catch (System.Exception ex)
            {
                Console.Error.WriteLine(ex.ToString());
            }
        }
Exemplo n.º 2
0
        /// <summary>
        /// Main thread to host the client process.
        /// </summary>
        /// <param name="threadContext">A ZMQ.Context instance.</param>
        public void Run(object threadContext)
        {
            try
            {
                Context ctx = (Context)threadContext;

                // This would be a using statement on data and ctl but we need to close and reopen sockets, which means
                // we have to Dispose() of a socket early and create a new instance. C#, wisely so, prevents you from
                // reassigning a using variable.
                Socket data = null, ctl = null, disk = null, diskACK = null;
                try
                {
                    data    = ctx.Socket(SocketType.SUB);
                    ctl     = ctx.Socket(SocketType.REQ);
                    disk    = new Socket(SocketType.PUSH);
                    diskACK = new Socket(SocketType.PULL);

                    // Set the HWM for the disk PUSH so that the PUSH blocks if the PULL can't keep up writing:
                    if (diskHWM > 0)
                    {
                        disk.SNDHWM = diskHWM;
                    }
                    //disk.SndBuf = 1048576 * diskHWM * 4;

                    // inproc:// binds are immediate.
                    disk.Bind("inproc://disk");
                    diskACK.Bind("inproc://diskack");

                    // Create the disk PULL thread:
                    Thread diskWriterThread = new Thread(new ParameterizedThreadStart(DiskWriterThread));
                    diskWriterThread.Start(ctx);

                    data.RCVHWM = networkHWM;

                    //data.RcvBuf = 1048576 * hwm * 4;
                    // NOTE: work-around for MS bug in WinXP's networking stack. See http://support.microsoft.com/kb/201213 for details.
                    data.RcvBuf = 0;

                    if (tsp == Transport.EPGM || tsp == Transport.PGM)
                    {
                        data.Rate = pgmRate;
                    }

                    string address = tsp.ToString().ToLower() + "://" + deviceData + ":" + portData;
                    Console.WriteLine("Connect(\"{0}\")", address);
                    data.Connect(address);
                    data.Subscribe(subscription, Encoding.Unicode);

                    // Connect to the control request socket:
                    ctl.RcvBuf = 1048576 * 4;
                    ctl.SndBuf = 1048576 * 4;
                    address    = "tcp://" + deviceCtl + ":" + portCtl.ToString();
                    Console.WriteLine("Connect(\"{0}\")", address);
                    ctl.Connect(address);

                    Guid myIdentity = Guid.NewGuid();
                    ctl.Identity = new byte[1] {
                        (byte)'@'
                    }.Concat(myIdentity.ToByteArray()).ToArray();

                    Thread.Sleep(500);

                    // Begin client logic:

                    naks     = null;
                    numBytes = 0;
                    int    ackCount = -1;
                    byte[] nakBuf   = null;

                    bool shuttingDown = false;

                    Stopwatch recvTimer = Stopwatch.StartNew();
                    long      lastElapsedMilliseconds = recvTimer.ElapsedMilliseconds;
                    int       msgsRecv = 0, msgsWritten = 0;

                    try
                    {
                        // Poll for incoming messages on the data SUB socket:
                        PollItem[] pollItems = new PollItem[3];
                        pollItems[0] = data.CreatePollItem(IOMultiPlex.POLLIN);
                        pollItems[1] = ctl.CreatePollItem(IOMultiPlex.POLLIN /* | IOMultiPlex.POLLOUT */);
                        pollItems[2] = diskACK.CreatePollItem(IOMultiPlex.POLLIN);

                        ZMQStateMasheen <DataSUBState>    dataFSM;
                        ZMQStateMasheen <ControlREQState> controlFSM;
                        Queue <QueuedControlMessage>      controlStateQueue = new Queue <QueuedControlMessage>();

                        DateTimeOffset lastSentNAKs = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(60));

                        // We create a state machine to handle our send/recv state:
                        dataFSM = new ZMQStateMasheen <DataSUBState>(
                            // Set the initial state:
                            DataSUBState.Recv,
                            // Initial state handler:
                            new ZMQStateMasheen <DataSUBState> .State(DataSUBState.Recv, (sock, revents) =>
                        {
                            Queue <byte[]> packet = sock.RecvAll();

                            if (packet.Count == 0)
                            {
                                trace("FAIL!"); return(DataSUBState.Recv);
                            }
                            string sub = Encoding.Unicode.GetString(packet.Dequeue());
                            if (packet.Count == 0)
                            {
                                trace("FAIL!"); return(DataSUBState.Recv);
                            }
                            string cmd = Encoding.Unicode.GetString(packet.Dequeue());

                            if ((naks != null) && (cmd == "DATA"))
                            {
                                if (packet.Count == 0)
                                {
                                    trace("FAIL!"); return(DataSUBState.Recv);
                                }
                                int chunkIdx = BitConverter.ToInt32(packet.Dequeue(), 0);

                                // Count the number of messages received from the network:
                                ++msgsRecv;
                                if (ChunkReceived != null)
                                {
                                    ChunkReceived(this, chunkIdx);
                                }

                                // Already received this chunk?
                                if (!naks[chunkIdx])
                                {
                                    trace("ALREADY RECV {0}", chunkIdx);
                                    return(DataSUBState.Recv);
                                }

                                trace("RECV {0}", chunkIdx);

                                // Queue up the disk writes with PUSH/PULL and an HWM on a separate thread to maintain as
                                // constant disk write throughput as we can get... The HWM will enforce the PUSHer to block
                                // when the PULLer cannot receive yet.
                                if (packet.Count == 0)
                                {
                                    return(DataSUBState.Recv);
                                }
                                byte[] chunk = packet.Dequeue();
                                disk.SendMore(cmdWrite);
                                disk.SendMore(BitConverter.GetBytes(chunkIdx));
                                // This will block if the disk queue gets full:
                                disk.Send(chunk);
                                chunk = null;

                                return(DataSUBState.Recv);
                            }
                            else if (cmd == "PING")
                            {
                                if (shuttingDown)
                                {
                                    return(DataSUBState.Recv);
                                }

                                controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendALIVE, null));
                                return(DataSUBState.Recv);
                            }
                            else
                            {
                                return(DataSUBState.Recv);
                            }
                        })
                            );

                        // We create a state machine to handle our send/recv state:
                        object tmpControlState = null;
                        controlFSM = new ZMQStateMasheen <ControlREQState>(
                            ControlREQState.Nothing,
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.Nothing, (sock, revents) =>
                        {
                            // Do nothing state.
                            return(ControlREQState.Nothing);
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.SendALIVE, (sock, revents) =>
                        {
                            if (shuttingDown)
                            {
                                return(ControlREQState.Nothing);
                            }

                            ctl.SendMore(ctl.Identity);
                            ctl.Send("ALIVE", Encoding.Unicode);

                            return(new ZMQStateMasheen <ControlREQState> .MoveOperation(ControlREQState.RecvALIVE, false));
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.RecvALIVE, (sock, revents) =>
                        {
                            Queue <byte[]> packet = ctl.RecvAll();

                            if (packet.Count == 0)
                            {
                                return(ControlREQState.Nothing);
                            }
                            string cmd = Encoding.Unicode.GetString(packet.Dequeue());
                            //trace("Server: '{0}'", cmd);

                            if (cmd == "")
                            {
                                return(ControlREQState.Nothing);
                            }
                            else if (cmd == "WHOAREYOU")
                            {
                                return(new ZMQStateMasheen <ControlREQState> .MoveOperation(ControlREQState.SendJOIN, true));
                            }

                            return(ControlREQState.Nothing);
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.SendJOIN, (sock, revents) =>
                        {
                            if (shuttingDown)
                            {
                                return(ControlREQState.Nothing);
                            }

                            // Send a JOIN request:
                            ctl.SendMore(ctl.Identity);
                            ctl.Send("JOIN", Encoding.Unicode);

                            return(ControlREQState.RecvJOIN);
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.RecvJOIN, (sock, revents) =>
                        {
                            Queue <byte[]> packet = ctl.RecvAll();
                            if (packet.Count == 0)
                            {
                                return(ControlREQState.Nothing);
                            }

                            string resp = Encoding.Unicode.GetString(packet.Dequeue());
                            if (resp != "JOINED")
                            {
                                // TODO: handle this failure.
                                trace("FAIL!"); return(ControlREQState.Nothing);
                            }

                            // TODO: make this atomically succeed or fail without corrupting state
                            if (packet.Count == 0)
                            {
                                trace("FAIL!"); return(ControlREQState.Nothing);
                            }
                            numChunks = BitConverter.ToInt32(packet.Dequeue(), 0);
                            numBytes  = ((numChunks + 7) & ~7) >> 3;
                            nakBuf    = new byte[numBytes];
                            if (packet.Count == 0)
                            {
                                trace("FAIL!"); return(ControlREQState.Nothing);
                            }
                            chunkSize = BitConverter.ToInt32(packet.Dequeue(), 0);
                            if (packet.Count == 0)
                            {
                                trace("FAIL!"); return(ControlREQState.Nothing);
                            }
                            int numFiles = BitConverter.ToInt32(packet.Dequeue(), 0);

                            List <TarballEntry> tbes = new List <TarballEntry>(numFiles);
                            for (int i = 0; i < numFiles; ++i)
                            {
                                if (packet.Count == 0)
                                {
                                    trace("FAIL!"); return(ControlREQState.Nothing);
                                }
                                string fiName = Encoding.Unicode.GetString(packet.Dequeue());
                                if (packet.Count == 0)
                                {
                                    trace("FAIL!"); return(ControlREQState.Nothing);
                                }
                                long length = BitConverter.ToInt64(packet.Dequeue(), 0);
                                if (packet.Count == 0)
                                {
                                    trace("FAIL!"); return(ControlREQState.Nothing);
                                }
                                byte[] hash = packet.Dequeue();

                                TarballEntry tbe = new TarballEntry(fiName, length, hash);
                                tbes.Add(tbe);
                            }

                            lock (tarballLock)
                            {
                                if (tarball != null)
                                {
                                    tarball.Close();
                                    tarball.Dispose();
                                    tarball = null;
                                }
                            }

                            lock (tarballLock)
                            {
                                // Create the tarball reader that writes the files locally:
                                tarball = new TarballStreamReader(downloadDirectory, tbes);

                                // Get our local download state:
                                naks = getClientState(this, tarball);
                            }
                            ackCount = naks.Cast <bool>().Take(numChunks).Count(b => !b);

                            return(new ZMQStateMasheen <ControlREQState> .MoveOperation(ControlREQState.SendNAKS, true));
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.SendNAKS, (sock, revents) =>
                        {
                            if (shuttingDown)
                            {
                                return(ControlREQState.Nothing);
                            }

                            if (nakBuf == null)
                            {
                                return(ControlREQState.Nothing);
                            }

                            // Send our NAKs:
                            ctl.SendMore(ctl.Identity);
                            ctl.SendMore("NAKS", Encoding.Unicode);
                            // TODO: RLE!
                            naks.CopyTo(nakBuf, 0);
                            trace("SEND NAK");
                            ctl.Send(nakBuf);

                            return(ControlREQState.RecvNAKS);
                        }),
                            new ZMQStateMasheen <ControlREQState> .State(ControlREQState.RecvNAKS, (sock, revents) =>
                        {
                            Queue <byte[]> packet = ctl.RecvAll();
                            // Don't care what the response is for now.

                            return(ControlREQState.Nothing);
                        })
                            );

                        // Disk-write acknowledgement poll input handler:
                        pollItems[2].PollInHandler += new PollHandler((Socket sock, IOMultiPlex revents) =>
                        {
                            byte[] idxPkt = sock.RecvAll().Dequeue();

                            int chunkIdx   = BitConverter.ToInt32(idxPkt, 0);
                            naks[chunkIdx] = false;

                            // Count the number of messages written to disk:
                            ++msgsWritten;
                            ++ackCount;

                            // Profiled - terrible performance. Not a big surprise here.
                            //ackCount = naks.Cast<bool>().Take(numChunks).Count(b => !b);

                            // If we ran up the timer, send more ACKs:
                            if (DateTimeOffset.UtcNow.Subtract(lastSentNAKs).TotalMilliseconds >= 250d)
                            {
                                lastSentNAKs = DateTimeOffset.UtcNow;
                                controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendNAKS, null));
                            }
                        });

                        pollItems[0].PollInHandler += new PollHandler(dataFSM.StateMasheen);
                        pollItems[1].PollInHandler += new PollHandler(controlFSM.StateMasheen);

                        DateTimeOffset lastRecv = DateTimeOffset.UtcNow;

                        // Start the control state machine with a JOIN:
                        controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendJOIN, null));

                        // Create a socket poller for the data socket:
                        while (true)
                        {
                            // If we disposed of the previous control socket, create a new one:
                            if (ctl == null)
                            {
                                trace("Creating new CONTROL socket");
                                // Set up new socket:
                                ctl = ctx.Socket(SocketType.REQ);
                                // Connect to the control request socket:
                                ctl.Connect("tcp://" + deviceCtl + ":" + portCtl.ToString());
                                ctl.Identity = new byte[1] {
                                    (byte)'@'
                                }.Concat(myIdentity.ToByteArray()).ToArray();
                            }

                            if (!shuttingDown && IsSendState(controlFSM.CurrentState))
                            {
                                // If we ran up the timer, send more NAKs:
                                if (DateTimeOffset.UtcNow.Subtract(lastSentNAKs).TotalMilliseconds >= 200d)
                                {
                                    lastSentNAKs = DateTimeOffset.UtcNow;
                                    controlStateQueue.Enqueue(new QueuedControlMessage(ControlREQState.SendNAKS, null));
                                }

                                if (controlStateQueue.Count > 0)
                                {
                                    var msg = controlStateQueue.Dequeue();

                                    // Roll up similar state requests:
                                    int dupes = 0;
                                    while (controlStateQueue.Count > 0)
                                    {
                                        if (controlStateQueue.Peek().NewState != msg.NewState)
                                        {
                                            break;
                                        }

                                        msg = controlStateQueue.Dequeue();
                                        ++dupes;
                                    }

                                    if (dupes > 0)
                                    {
                                        trace("Removed {0} redundant queued states", dupes);
                                    }

                                    tmpControlState = msg.Object;

                                    // Run the control state machine to SEND:
                                    controlFSM.CurrentState = msg.NewState;
                                    controlFSM.StateMasheen(ctl, IOMultiPlex.POLLOUT);
                                }
                            }

                            // Measure our message send rate per minute:
                            long elapsed = recvTimer.ElapsedMilliseconds - lastElapsedMilliseconds;
                            if (elapsed >= 100L)
                            {
                                NetworkRecvChunksPerMinute = (int)((msgsRecv * 60000L) / elapsed);
                                DiskWriteChunksPerMinute   = (int)((msgsWritten * 60000L) / elapsed);
                                lastElapsedMilliseconds    = recvTimer.ElapsedMilliseconds;
                                msgsRecv    = 0;
                                msgsWritten = 0;
                            }

                            if (!recvTimer.IsRunning)
                            {
                                recvTimer.Reset();
                                recvTimer.Start();
                                lastElapsedMilliseconds = 0L;
                            }

                            if (ackCount >= numChunks)
                            {
                                disk.Send(cmdExit);
                                break;
                            }

                            if (ctx.Poll(pollItems, 100L) == 0)
                            {
                                Thread.Sleep(1);
                            }
                        }

                        // Wait for the disk writer to finish up:
                        shuttingDown = true;
                        diskWriterThread.Join();

                        // If we were last to await some response, receive it:
                        if (controlFSM.CurrentState == ControlREQState.RecvALIVE || controlFSM.CurrentState == ControlREQState.RecvJOIN || controlFSM.CurrentState == ControlREQState.RecvNAKS)
                        {
                            PollItem[] recvPoll = new PollItem[1];
                            recvPoll[0] = ctl.CreatePollItem(IOMultiPlex.POLLIN);
                            recvPoll[0].PollInHandler += new PollHandler(controlFSM.StateMasheen);

                            // Wait a bit to receive the response:
                            for (int i = 0; (i < 20) && ((ctx.Poll(recvPoll, 100) == 1) && (ctl != null)); ++i)
                            {
                            }
                        }

                        // Send the LEAVE message:
                        ctl.SendMore(ctl.Identity);
                        ctl.Send("LEAVE", Encoding.Unicode);
                        Completed = true;
                    }
                    finally
                    {
                        if (tarball != null)
                        {
                            tarball.Dispose();
                        }
                    }
                }
                finally
                {
                    if (data != null)
                    {
                        data.Dispose();
                    }
                    if (ctl != null)
                    {
                        ctl.Dispose();
                    }
                }
            }
            catch (System.Exception ex)
            {
                Console.Error.WriteLine(ex.ToString());
            }
        }