Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pipes for parent process IPC. #311

Merged
merged 3 commits into from
Aug 31, 2016
Merged

Add pipes for parent process IPC. #311

merged 3 commits into from
Aug 31, 2016

Conversation

jrick
Copy link
Member

@jrick jrick commented Aug 9, 2016

Rewrite startup/shutdown logic to simplify shutdown signaling. All
cleanup is now run from deferred functions in the main function.

Add two new config options to set the read and write ends of a pair of
pipes. This is used as a simple mechanism for a parent process to
communicate with, observe, and manage the lifetime of a child dcrd
process. When the RX (read end) pipe is closed, clean shutdown
automatically begins.

Add a new flag --lifetimeevents to create and send lifetime event
notifications over the TX (write end) pipe during bringup and
shutdown. This allows the parent process to observe which subsystems
are currently starting, running, and stopping.

Fixes #297.
Fixes #298.

@jrick
Copy link
Member Author

jrick commented Aug 9, 2016

Example parent process that uses the rx pipe to signal clean shutdown (does NOT work on windows because the os/exec package thinks it's somehow ok to try and call fork+exec):

$ cat pipeipc.go
package main

import (
        "fmt"
        "os"
        "os/exec"
        "time"
)

func main() {
        rx, tx, err := os.Pipe()
        if err != nil {
                fmt.Println(err)
                return
        }

        cmd := exec.Command("dcrd", "--testnet", fmt.Sprintf("--piperx=%v", rx.Fd()))
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.ExtraFiles = make([]*os.File, rx.Fd()-2)
        cmd.ExtraFiles[rx.Fd()-3] = rx
        err = cmd.Start()
        if err != nil {
                fmt.Println(err)
                return
        }

        // Signal shutdown after 3 seconds
        go func() {
                time.Sleep(3 * time.Second)
                tx.Close()
        }()

        err = cmd.Wait()
        if err != nil {
                fmt.Println(err)
                return
        }
}
$ go run pipeipc.go
19:48:58 2016-08-09 [INF] DCRD: Version 0.1.6-beta
19:48:58 2016-08-09 [INF] DCRD: Loading block database from '/home/jrick/.dcrd/data/testnet/blocks_leveldb'
19:48:58 2016-08-09 [INF] BCDB: Address index synced and loaded to height 137247
19:48:58 2016-08-09 [INF] DCRD: Block database loaded with block height 137247
19:48:58 2016-08-09 [INF] DCRD: Loading ticket database from disk
19:49:01 2016-08-09 [INF] DCRD: Shutdown requested.  Shutting down...
19:49:01 2016-08-09 [INF] DCRD: Gracefully shutting down the database...
19:49:01 2016-08-09 [INF] DCRD: Shutdown complete

@jrick
Copy link
Member Author

jrick commented Aug 10, 2016

Here is a similar example in C#:

PS C:\Users\jrick\Documents\pipeipc> gc .\Program.cs
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;

namespace PipeIPCExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var pipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);
            var procInfo = new ProcessStartInfo
            {
                FileName = "dcrd",
                Arguments = $"--testnet --piperx {pipe.GetClientHandleAsString()}",
            };
            var procHandle = Process.Start(procInfo);

            // Signal shutdown after 3 seconds
            Task.Delay(3000).ContinueWith(_ => pipe.Dispose());

            procHandle.WaitForExit();
            Console.WriteLine($"Exited with exit code {procHandle.ExitCode}.");
        }
    }
}
PS C:\Users\jrick\Documents\pipeipc> dotnet run
Project pipeipc (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
21:48:34 2016-08-09 [INF] DCRD: Version 0.1.6-beta
21:48:34 2016-08-09 [INF] DCRD: Loading block database from 'C:\Users\jrick\AppData\Local\Dcrd\data\testnet\blocks_leveldb'
21:48:34 2016-08-09 [INF] BCDB: Address index synced and loaded to height 135572
21:48:34 2016-08-09 [INF] DCRD: Block database loaded with block height 135572
21:48:34 2016-08-09 [INF] DCRD: Loading ticket database from disk
21:48:36 2016-08-09 [INF] DCRD: Shutdown requested.  Shutting down...
21:48:36 2016-08-09 [INF] DCRD: Gracefully shutting down the database...
21:48:36 2016-08-09 [INF] DCRD: Shutdown complete
Exited with exit code 0.

@jrick
Copy link
Member Author

jrick commented Aug 10, 2016

Here is a more advanced parent process that also reads the lifetime events over the other pipe, and requests dcrd shutdown when startup has completed:

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading.Tasks;

namespace PipeIPCExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var outPipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);
            var inPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
            var procInfo = new ProcessStartInfo
            {
                FileName = "dcrd",
                Arguments = $"--testnet --piperx {outPipe.GetClientHandleAsString()} --pipetx {inPipe.GetClientHandleAsString()} --lifetimeevents",
            };
            var procHandle = Process.Start(procInfo);

            // Read lifetime events
            Task.Run(async () =>
            {
                var headerBuffer = new byte[1 + 255 + 4];
                while (true)
                {
                    await inPipe.ReadAsync(headerBuffer, 0, 1);
                    await inPipe.ReadAsync(headerBuffer, 1, headerBuffer[0]);
                    var msgType = Encoding.UTF8.GetString(headerBuffer, 1, headerBuffer[0]);
                    await inPipe.ReadAsync(headerBuffer, 1 + headerBuffer[0], 4);
                    var payloadSize = ReadUInt32LE(headerBuffer, 1 + headerBuffer[0]);
                    var payload = new byte[payloadSize];
                    await inPipe.ReadAsync(payload, 0, (int)payloadSize);

                    Console.WriteLine($"{msgType}: {BitConverter.ToString(payload)}");

                    // Signal shutdown when startup finishes
                    if (msgType == "lifetimeevent" && payload[0] == 1)
                    {
                        Console.WriteLine("Startup finished: signaling shutdown...");
                        outPipe.Dispose();
                    }

                    Array.Clear(headerBuffer, 0, headerBuffer.Length);
                }
            });

            procHandle.WaitForExit();
            Console.WriteLine($"Exited with exit code {procHandle.ExitCode}.");
        }

        static uint ReadUInt32LE(byte[] array, int offset)
        {
            return (uint)(array[offset++] | array[offset++] << 8 | array[offset++] << 16 | array[offset] << 24);
        }
    }
}

Output:

PS C:\Users\jrick\Documents\pipeipc> dotnet run
Project pipeipc (.NETCoreApp,Version=v1.0) will be compiled because inputs were modified
Compiling pipeipc for .NETCoreApp,Version=v1.0

Compilation succeeded.
    0 Warning(s)
    0 Error(s)

Time elapsed 00:00:00.9631163


lifetimeevent: 00-00
01:01:59 2016-08-10 [INF] DCRD: Version 0.1.6-beta
01:01:59 2016-08-10 [INF] DCRD: Loading block database from 'C:\Users\jrick\AppData\Local\Dcrd\data\testnet\blocks_leveldb'
lifetimeevent: 00-01
01:01:59 2016-08-10 [INF] BCDB: Address index synced and loaded to height 135587
01:01:59 2016-08-10 [INF] DCRD: Block database loaded with block height 135587
01:01:59 2016-08-10 [INF] DCRD: Loading ticket database from disk
01:02:33 2016-08-10 [INF] DCRD: Ticket DB loaded with top block height 135587
lifetimeevent: 00-02
01:02:33 2016-08-10 [INF] BMGR: Generating initial block node index.  This may take a while...
01:02:35 2016-08-10 [INF] BMGR: Block index generation complete
lifetimeevent: 01-00
Startup finished: signaling shutdown...
lifetimeevent: 02-02
lifetimeevent: 02-01
01:02:35 2016-08-10 [INF] SRVR: Server listening on 0.0.0.0:19108
01:02:35 2016-08-10 [INF] SRVR: Server listening on [::]:19108
01:02:35 2016-08-10 [INF] RPCS: RPC server listening on [::1]:19109
01:02:35 2016-08-10 [INF] RPCS: RPC server listening on 127.0.0.1:19109
01:02:35 2016-08-10 [INF] DCRD: Shutdown requested.  Shutting down...
01:02:35 2016-08-10 [INF] DCRD: Gracefully shutting down the server...
01:02:35 2016-08-10 [WRN] SRVR: Server shutting down
01:02:35 2016-08-10 [WRN] RPCS: RPC server shutting down
01:02:35 2016-08-10 [INF] RPCS: RPC server shutdown complete
01:02:35 2016-08-10 [INF] AMGR: Loaded 677 addresses from file 'C:\Users\jrick\AppData\Local\Dcrd\data\testnet\peers.json'
01:02:35 2016-08-10 [INF] ADXR: Address indexer shutting down
01:02:35 2016-08-10 [INF] BMGR: Block manager shutting down
01:02:35 2016-08-10 [INF] AMGR: Address manager shutting down
01:02:35 2016-08-10 [INF] SRVR: Server shutdown complete
01:02:35 2016-08-10 [INF] STKE: Storing the ticket database to disk
01:02:36 2016-08-10 [INF] DISC: 3 addresses found from DNS seed testnet-seed.decred.netpurgatory.org
01:02:36 2016-08-10 [INF] DISC: 4 addresses found from DNS seed testnet.decredseed.org
01:02:36 2016-08-10 [INF] DISC: 1 addresses found from DNS seed testnet-seed.decred.mindcry.org
01:02:36 2016-08-10 [INF] DISC: 1 addresses found from DNS seed testnet-seed.decred.org
01:02:48 2016-08-10 [INF] DCRD: Gracefully shutting down the database...
lifetimeevent: 02-00
01:02:48 2016-08-10 [INF] DCRD: Shutdown complete
Exited with exit code 0.

Rewrite startup/shutdown logic to simplify shutdown signaling.  All
cleanup is now run from deferred functions in the main function.

Add two new config options to set the read and write ends of a pair of
pipes.  This is used as a simple mechanism for a parent process to
communicate with, observe, and manage the lifetime of a child dcrd
process.  When the RX (read end) pipe is closed, clean shutdown
automatically begins.

Add a new flag --lifetimeevents to create and send lifetime event
notifications over the TX (write end) pipe during bringup and
shutdown.  This allows the parent process to observe which subsystems
are currently starting, running, and stopping.

Fixes btcsuite#297.
Fixes btcsuite#298.
@jrick
Copy link
Member Author

jrick commented Aug 29, 2016

Rebased over master. Once this is reviewed and merged I will open a new PR that backports this to the 0.4.0 release branch.

@marcopeereboom
Copy link
Member

I love this code but I am having a bit of heartburn about the encoding. I really think we should use xdr or something alike. So let's at the very least add a version byte in front of the entire message in case we want to change it in the future.


// Messages sent over a pipe are encoded using a simple binary message format:
//
// - Protocol version (1 byte, currently 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't ever use 0 for a version. Experience tells us that always ends in tears.

@marcopeereboom
Copy link
Member

ok

@jrick jrick merged commit 6c1aa07 into decred:master Aug 31, 2016
@jrick jrick mentioned this pull request Aug 31, 2016
@jrick jrick deleted the pipeipc branch August 31, 2016 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Shutdown dcrd.exe via RPC Lanch RPC server earlier during bring-up
2 participants