Skip to content
Permalink
Browse files

Attempt to listen on a socket passed down from nodejs

  • Loading branch information...
rvl committed Jun 12, 2019
1 parent 4b1d08d commit 5a06942a2b71fb83eb39741298db1513d1f92c97
@@ -72,10 +72,11 @@ Usage:
cardano-wallet-launcher --version

Options:
--network <STRING> testnet, mainnet, or local [default: testnet]
--wallet-server-port <PORT> port used for serving the wallet API
--http-bridge-port <PORT> port used for communicating with the http-bridge [default: 8080]
--state-dir <DIR> write wallet state (blockchain and database) to this directory
--network <STRING> testnet, mainnet, or local [default: testnet]
--wallet-server-port <PORT> port used for serving the wallet API
--wallet-server-socket <PATH> socket file used for serving the wallet API
--http-bridge-port <PORT> port used for communicating with the http-bridge [default: 8080]
--state-dir <DIR> write wallet state (blockchain and database) to this directory
|]

main :: IO ()
@@ -91,13 +92,14 @@ main = do
let network = fromMaybe "testnet" $ args `getArg` longOption "network"
bridgePort <- args `parseArg` longOption "http-bridge-port"
walletPort <- args `parseOptionalArg` longOption "wallet-server-port"
let walletSocket = args `getArg` longOption "wallet-server-socket"

sayErr "Starting..."
installSignalHandlers
maybe (pure ()) setupStateDir stateDir
let commands =
[ nodeHttpBridgeOn stateDir bridgePort network
, walletOn stateDir walletPort bridgePort network
, walletOn stateDir walletPort walletSocket bridgePort network
]
sayErr $ fmt $ blockListF commands
ProcessHasExited name code <- launch commands
@@ -112,6 +114,11 @@ main = do
| args `isPresent` option = Just <$> parseArg args option
| otherwise = pure Nothing

-- getOptionalArg :: Arguments -> Option -> IO (Maybe String)
-- getOptionalArg args option
-- | args `isPresent` option = Just <$> getArg args option
-- | otherwise = pure Nothing

nodeHttpBridgeOn :: Maybe FilePath -> Port "Node" -> String -> Command
nodeHttpBridgeOn stateDir port net =
Command "cardano-http-bridge" args (return ()) Inherit
@@ -123,15 +130,16 @@ nodeHttpBridgeOn stateDir port net =
] ++ networkArg
networkArg = maybe [] (\d -> ["--networks-dir", d]) stateDir

walletOn :: Maybe FilePath -> Maybe (Port "Wallet") -> Port "Node" -> String -> Command
walletOn stateDir wp np net =
walletOn :: Maybe FilePath -> Maybe (Port "Wallet") -> Maybe FilePath -> Port "Node" -> String -> Command
walletOn stateDir wp ws np net =
Command "cardano-wallet" args (threadDelay oneSecond) Inherit
where
args =
[ "server"
, "--network", if net == "local" then "testnet" else net
] ++ walletPortArg ++ bridgePortArg ++ dbArg
] ++ walletPortArg ++ walletSocketArg ++ bridgePortArg ++ dbArg
walletPortArg = maybe [] (\p -> ["--port", T.unpack (toText p)]) wp
walletSocketArg = maybe [] (\s -> ["--socket", s]) ws
bridgePortArg = [ "--bridge-port", T.unpack (toText np) ]
dbArg = maybe [] (\d -> ["--database", d </> "wallet.db"]) stateDir
oneSecond = 1000000
@@ -139,7 +139,7 @@ and can be run "offline". (e.g. 'generate mnemonic')
⚠️ Options are positional (--a --b is not equivalent to --b --a) ! ⚠️

Usage:
cardano-wallet server [--network=STRING] [--port=INT] [--bridge-port=INT] [--database=FILE]
cardano-wallet server [--network=STRING] [--port=INT] [--socket=STRING] [--bridge-port=INT] [--database=FILE]
cardano-wallet mnemonic generate [--size=INT]
cardano-wallet wallet list [--port=INT]
cardano-wallet wallet create [--port=INT] <name> [--address-pool-gap=INT]
@@ -153,6 +153,7 @@ Usage:

Options:
--port <INT> port used for serving the wallet API
--socket <STRING> socket fd/pipe name used for serving the wallet API
--bridge-port <INT> port used for communicating with the http-bridge [default: 8080]
--address-pool-gap <INT> number of unused consecutive addresses to keep track of [default: 20]
--size <INT> number of mnemonic words to generate [default: 15]
@@ -342,8 +343,7 @@ execHttpBridge
:: forall n. (KeyToAddress (HttpBridge n), KnownNetwork n)
=> Arguments -> Proxy (HttpBridge n) -> IO ()
execHttpBridge args _ = do
(walletPort :: Maybe Int)
<- args `parseOptionalArg` longOption "port"
walletListen <- getWalletListen args
(bridgePort :: Int)
<- args `parseArg` longOption "bridge-port"
let dbFile = args `getArg` longOption "database"
@@ -354,10 +354,21 @@ execHttpBridge args _ = do
wallet <- newWalletLayer @_ @(HttpBridge n) db nw tl
let logStartup port = sayErr $
"[INFO] Wallet backend server listening on: " <> toText port
Server.withListeningSocket walletPort $ \(port, socket) -> do
Server.withListeningSocket walletListen $ \(port, socket) -> do
let settings = Server.mkWarpSettings logStartup port
race_ (daedalusIPC port) (Server.startOnSocket settings socket wallet)

getWalletListen :: Arguments -> IO Server.WalletListen
getWalletListen args = do
(walletPort :: Maybe Int)
<- args `parseOptionalArg` longOption "port"
(walletSocket :: Maybe Int)
<- args `parseOptionalArg` longOption "socket"
pure $ case (walletPort, walletSocket) of
(_, Just fd) -> Server.ListenOnSocketFD fd
(Just port, _) -> Server.ListenOnPort port
(Nothing, Nothing) -> Server.ListenOnRandomPort

-- | Generate a random mnemonic of the given size 'n' (n = number of words),
-- and print it to stdout.
execGenerateMnemonic :: Text -> IO ()
@@ -20,6 +20,7 @@ module Cardano.Wallet.Api.Server
, startOnSocket
, mkWarpSettings
, withListeningSocket
, WalletListen(..)
) where

import Prelude
@@ -112,7 +113,15 @@ import Network.HTTP.Media.RenderHeader
import Network.HTTP.Types.Header
( hContentType )
import Network.Socket
( Socket, close )
( Family (AF_UNIX)
, Socket
, SocketStatus (Bound)
, SocketType (Stream)
, close
, defaultProtocol
, listen
, mkSocket
)
import Network.Wai.Middleware.ServantError
( handleRawError )
import Servant
@@ -146,11 +155,11 @@ import qualified Network.Wai.Handler.Warp as Warp
start
:: forall t. (TxId t, KeyToAddress t, EncodeAddress t, DecodeAddress t)
=> (Warp.Port -> IO ())
-> Maybe Warp.Port
-> Warp.Port
-> WalletLayer (SeqState t) t
-> IO ()
start onStartup mport wl =
withListeningSocket mport $ \(port, socket) ->
start onStartup port wl =
withListeningSocket (ListenOnPort port) $ \(port, socket) ->
startOnSocket (mkWarpSettings onStartup port) socket wl

-- | Start the application server, using the given settings and a bound socket.
@@ -183,19 +192,29 @@ mkWarpSettings onStartup port = Warp.defaultSettings
& Warp.setPort port
& Warp.setBeforeMainLoop (onStartup port)

data WalletListen
= ListenOnPort Int
| ListenOnRandomPort
| ListenOnSocketFD Int
deriving (Show, Eq)

-- | Run an action with a TCP socket bound to a port. If no port is specified,
-- then an unused port will be selected at random.
withListeningSocket
:: Maybe Warp.Port
:: WalletListen
-- ^ Port to listen on, or Nothing for an unused port selected randomly.
-> ((Warp.Port, Socket) -> IO ())
-- ^ Action to run with listening socket.
-> IO ()
withListeningSocket mport = bracket acquire release
withListeningSocket wl = bracket acquire release
where
acquire = case mport of
Just port -> (port,) <$> bindPortTCP port hostPreference
Nothing -> bindRandomPortTCP hostPreference
acquire = case wl of
ListenOnPort port -> (port,) <$> bindPortTCP port hostPreference
ListenOnRandomPort -> bindRandomPortTCP hostPreference
ListenOnSocketFD fd -> do
sock <- mkSocket (fromIntegral fd) AF_UNIX Stream defaultProtocol Bound
listen sock 2
pure (0, sock)
release (_, socket) = liftIO $ close socket
-- TODO: make configurable, default to secure for now.
hostPreference = "127.0.0.1"
@@ -178,7 +178,7 @@ main = hspec $ do
db <- MVar.newDBLayer
let tl = HttpBridge.newTransactionLayer
wallet <- newWalletLayer db nl tl
Server.start (const $ pure ()) (Just serverPort) wallet
Server.start (const $ pure ()) serverPort wallet

waitForCluster :: String -> IO ()
waitForCluster addr = do
@@ -1,57 +1,81 @@
#!/usr/bin/env node

// This runs cardano-wallet-launcher in the same way that Daedalus would.
// It needs node, cardano-wallet, and cardano-wallet-launcher on the PATH to run.
// This runs cardano-wallet in the same way that Daedalus might.
// It needs node and cardano-wallet on the PATH to run.

const child_process = require("child_process");
const http = require('http');
const net = require("net");
const http = require("http");
const fs = require("fs");
const { Pipe, constants: PipeConstants } = process.binding('pipe_wrap');

function main() {
// const proc = child_process.spawn("cardano-wallet", ["server"], {
const proc = child_process.spawn("cardano-wallet-launcher", [], {
stdio: ["ignore", "inherit", "inherit", "ipc"]
// Filename for socket
// It can be used like this:
// curl -X GET --unix-socket cardano-wallet-2939.sock http://wallet/v2/wallets
const ipcName = "cardano-wallet-" + process.pid + ".sock";

// create a bound unix socket
const sock = new Pipe(PipeConstants.SOCKET);
sock.bind(ipcName);

// Attempt to listen on the socket, to make it race-free.
// Unfortunately, this will cause nodejs to crash.
sock.listen();

// Spawn cardano-wallet. Pass the socket as the 4th file descriptor
// to the forked process (fd numbers are 0-based).
const proc = child_process.spawn("cardano-wallet",
["server", "--socket", 3], {
stdio: ["ignore", "inherit", "inherit", sock.fd]
});

proc.on("close", function(code, signal) {
console.log("JS: child_process stdio streams closed");
process.exit(1);
proc.kill()
quit(1, ipcName);
});

proc.on("disconnect", function() {
console.log("JS: child_process disconnected");
process.exit(2);
proc.kill()
quit(2, ipcName);
});

proc.on("error", function(err) {
console.log("JS: error child_process: " + err);
process.exit(3);
proc.kill()
quit(3, ipcName);
});

proc.on("exit", function(code, signal) {
console.log("JS: child_process exited with status " + code + " or signal " + signal);
process.exit(4);
quit(4, ipcName);
});

proc.on("message", function(msg) {
console.log("JS: message received", msg);
if (msg.Started) {
proc.send("QueryPort");
} else if (msg.ReplyPort) {
http.get({
hostname: "localhost",
port: msg.ReplyPort,
path: "/v2/wallets",
agent: false
}, (res) => {
console.log("JS: response from wallet: " + res.statusCode);
res.resume();
res.on("end", () => {
console.log("JS: request response from wallet finished, exiting.");
process.exit(0);
});
// Make a HTTP request via the socket.
// Needs a timeout because we can't pass an already listening socket
// to the child_process.
setTimeout(() => {
http.get({
socketPath: ipcName,
path: "/v2/wallets",
agent: false
}, (res) => {
console.log("JS: response from wallet: " + res.statusCode);
res.resume();
res.on("end", () => {
console.log("JS: request response from wallet finished");
// console.log("JS: request response from wallet finished, exiting.");
// quit(0, ipcName);
});
}
});
});
}, 500);
}

function quit(code, sock) {
fs.unlinkSync(sock);
process.exit(code);
}

main();

0 comments on commit 5a06942

Please sign in to comment.
You can’t perform that action at this time.