diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index a5f5c101d..a0cb72a1a 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,98 +1,7 @@ # Getting Started -Welcome to nim-libp2p! This guide will walk you through a peer to peer chat example.
-The full code can be found in [directchat.nim](examples/directchat.nim) under the examples folder. +Welcome to nim-libp2p! -### Direct Chat Example -To run nim-libp2p, add it to your project's nimble file and spawn a node as follows: +To get started, please look at the [tutorials](../examples/tutorial_1_connect.md) -```nim -import tables -import chronos -import ../libp2p/[switch, - multistream, - protocols/identify, - connection, - transports/transport, - transports/tcptransport, - multiaddress, - peerinfo, - crypto/crypto, - peerid, - protocols/protocol, - muxers/muxer, - muxers/mplex/mplex, - protocols/secure/secio, - protocols/secure/secure] - -const TestCodec = "/test/proto/1.0.0" # custom protocol string - -type - TestProto = ref object of LPProtocol # declare a custom protocol - -method init(p: TestProto) {.gcsafe.} = - # handle incoming connections in closure - proc handle(conn: Connection, proto: string) {.async, gcsafe.} = - echo "Got from remote - ", cast[string](await conn.readLp(1024)) - await conn.writeLp("Hello!") - await conn.close() - - p.codec = TestCodec # init proto with the correct string id - p.handler = handle # set proto handler - -proc createSwitch(ma: MultiAddress): (Switch, PeerInfo) = - ## Helper to create a swith - - let seckey = PrivateKey.random(RSA) # use a random key for peer id - var peerInfo = PeerInfo.init(seckey) # create a peer id and assign - peerInfo.addrs.add(ma) # set this peer's multiaddresses (can be any number) - - let identify = newIdentify(peerInfo) # create the identify proto - - proc createMplex(conn: Connection): Muxer = - # helper proc to create multiplexers, - # use this to perform any custom setup up, - # such as adjusting timeout or anything else - # that the muxer requires - result = newMplex(conn) - - let mplexProvider = newMuxerProvider(createMplex, MplexCodec) # create multiplexer - let transports = @[Transport(newTransport(TcpTransport))] # add all transports (tcp only for now, but can be anything in the future) - let muxers = {MplexCodec: mplexProvider}.toTable() # add all muxers - let secureManagers = {SecioCodec: Secure(Secio.new(seckey))}.toTable() # setup the secio and any other secure provider - - # create the switch - let switch = newSwitch(peerInfo, - transports, - identify, - muxers, - secureManagers) - result = (switch, peerInfo) - -proc main() {.async, gcsafe.} = - let ma1: MultiAddress = Multiaddress.init("/ip4/0.0.0.0/tcp/0") - let ma2: MultiAddress = Multiaddress.init("/ip4/0.0.0.0/tcp/0") - - var peerInfo1, peerInfo2: PeerInfo - var switch1, switch2: Switch - (switch1, peerInfo1) = createSwitch(ma1) # create node 1 - - # setup the custom proto - let testProto = new TestProto - testProto.init() # run it's init method to perform any required initialization - switch1.mount(testProto) # mount the proto - var switch1Fut = await switch1.start() # start the node - - (switch2, peerInfo2) = createSwitch(ma2) # create node 2 - var switch2Fut = await switch2.start() # start second node - let conn = await switch2.dial(switch1.peerInfo, TestCodec) # dial the first node - - await conn.writeLp("Hello!") # writeLp send a length prefixed buffer over the wire - # readLp reads length prefixed bytes and returns a buffer without the prefix - echo "Remote responded with - ", cast[string](await conn.readLp(1024)) - - await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports - await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown - -waitFor(main()) -``` +For more concrete examples, you can look at the [hello world example](../examples/helloworld.nim) or the [direct chat](../examples/directchat.nim) diff --git a/examples/directchat.nim b/examples/directchat.nim index 197be4a41..225a9d5f1 100644 --- a/examples/directchat.nim +++ b/examples/directchat.nim @@ -1,54 +1,88 @@ when not(compileOption("threads")): {.fatal: "Please, compile this program with the --threads:on option!".} -import tables, strformat, strutils, bearssl -import chronos # an efficient library for async -import ../libp2p/[switch, # manage transports, a single entry point for dialing and listening - builders, # helper to build the switch object - multistream, # tag stream with short header to identify it - multicodec, # multicodec utilities - crypto/crypto, # cryptographic functions - errors, # error handling utilities - protocols/identify, # identify the peer info of a peer - stream/connection, # create and close stream read / write connections - transports/transport, # listen and dial to other peers using p2p protocol - transports/tcptransport, # listen and dial to other peers using client-server protocol - multiaddress, # encode different addressing schemes. For example, /ip4/7.7.7.7/tcp/6543 means it is using IPv4 protocol and TCP - peerinfo, # manage the information of a peer, such as peer ID and public / private key - peerid, # Implement how peers interact - protocols/protocol, # define the protocol base type - protocols/secure/secure, # define the protocol of secure connection - protocols/secure/secio, # define the protocol of secure input / output, allows encrypted communication that uses public keys to validate signed messages instead of a certificate authority like in TLS - muxers/muxer, # define an interface for stream multiplexing, allowing peers to offer many protocols over a single connection - muxers/mplex/mplex] # define some contants and message types for stream multiplexing +import + strformat, strutils, bearssl, + stew/byteutils, + chronos, + ../libp2p -const ChatCodec = "/nim-libp2p/chat/1.0.0" -const DefaultAddr = "/ip4/127.0.0.1/tcp/55505" +const DefaultAddr = "/ip4/127.0.0.1/tcp/0" const Help = """ - Commands: /[?|hep|connect|disconnect|exit] + Commands: /[?|help|connect|disconnect|exit] help: Prints this help connect: dials a remote peer disconnect: ends current session exit: closes the chat """ -type ChatProto = ref object of LPProtocol - switch: Switch # a single entry point for dialing and listening to peer - transp: StreamTransport # transport streams between read & write file descriptor - conn: Connection # create and close read & write stream - connected: bool # if the node is connected to another peer - started: bool # if the node has started +type + Chat = ref object + switch: Switch # a single entry point for dialing and listening to peer + stdinReader: StreamTransport # transport streams between read & write file descriptor + conn: Connection # connection to the other peer + connected: bool # if the node is connected to another peer -proc readAndPrint(p: ChatProto) {.async.} = - while true: - var strData = await p.conn.readLp(1024) - strData &= '\0'.uint8 - var str = cast[cstring](addr strdata[0]) - echo $p.switch.peerInfo.peerId & ": " & $str - await sleepAsync(100.millis) +## +# Stdout helpers, to write the prompt +## +proc writePrompt(c: Chat) = + if c.connected: + stdout.write '\r' & $c.switch.peerInfo.peerId & ": " + stdout.flushFile() -proc dialPeer(p: ChatProto, address: string) {.async.} = +proc writeStdout(c: Chat, str: string) = + echo '\r' & str + c.writePrompt() + +## +# Chat Protocol +## +const ChatCodec = "/nim-libp2p/chat/1.0.0" + +type + ChatProto = ref object of LPProtocol + +proc new(T: typedesc[ChatProto], c: Chat): T = + let chatproto = T() + + # create handler for incoming connection + proc handle(stream: Connection, proto: string) {.async.} = + if c.connected and not c.conn.closed: + c.writeStdout "a chat session is already in progress - refusing incoming peer!" + await stream.close() + else: + await c.handlePeer(stream) + await stream.close() + + # assign the new handler + chatproto.handler = handle + chatproto.codec = ChatCodec + return chatproto + +## +# Chat application +## +proc handlePeer(c: Chat, conn: Connection) {.async.} = + # Handle a peer (incoming or outgoing) + try: + c.conn = conn + c.connected = true + c.writeStdout $conn.peerId & " connected" + + # Read loop + while true: + let + strData = await conn.readLp(1024) + str = string.fromBytes(strData) + c.writeStdout $conn.peerId & ": " & $str + + except LPStreamEOFError: + c.writeStdout $conn.peerId & " disconnected" + +proc dialPeer(c: Chat, address: string) {.async.} = + # Parse and dial address let multiAddr = MultiAddress.init(address).tryGet() # split the peerId part /p2p/... @@ -63,86 +97,53 @@ proc dialPeer(p: ChatProto, address: string) {.async.} = wireAddr = ip4Addr & tcpAddr echo &"dialing peer: {multiAddr}" - p.conn = await p.switch.dial(remotePeer, @[wireAddr], ChatCodec) - p.connected = true - asyncSpawn p.readAndPrint() + asyncSpawn c.handlePeer(await c.switch.dial(remotePeer, @[wireAddr], ChatCodec)) -proc writeAndPrint(p: ChatProto) {.async.} = +proc readLoop(c: Chat) {.async.} = while true: - if not p.connected: + if not c.connected: echo "type an address or wait for a connection:" echo "type /[help|?] for help" - let line = await p.transp.readLine() - if line.startsWith("/help") or line.startsWith("/?") or not p.started: + c.writePrompt() + + let line = await c.stdinReader.readLine() + if line.startsWith("/help") or line.startsWith("/?"): echo Help continue if line.startsWith("/disconnect"): - echo "Ending current session" - if p.connected and p.conn.closed.not: - await p.conn.close() - p.connected = false + c.writeStdout "Ending current session" + if c.connected and c.conn.closed.not: + await c.conn.close() + c.connected = false elif line.startsWith("/connect"): - if p.connected: - var yesno = "N" - echo "a session is already in progress, do you want end it [y/N]?" - yesno = await p.transp.readLine() - if yesno.cmpIgnoreCase("y") == 0: - await p.conn.close() - p.connected = false - elif yesno.cmpIgnoreCase("n") == 0: - continue - else: - echo "unrecognized response" - continue - - echo "enter address of remote peer" - let address = await p.transp.readLine() + c.writeStdout "enter address of remote peer" + let address = await c.stdinReader.readLine() if address.len > 0: - await p.dialPeer(address) + await c.dialPeer(address) elif line.startsWith("/exit"): - if p.connected and p.conn.closed.not: - await p.conn.close() - p.connected = false + if c.connected and c.conn.closed.not: + await c.conn.close() + c.connected = false - await p.switch.stop() - echo "quitting..." + await c.switch.stop() + c.writeStdout "quitting..." quit(0) else: - if p.connected: - await p.conn.writeLp(line) + if c.connected: + await c.conn.writeLp(line) else: try: if line.startsWith("/") and "p2p" in line: - await p.dialPeer(line) - except: + await c.dialPeer(line) + except CatchableError as exc: echo &"unable to dial remote peer {line}" - echo getCurrentExceptionMsg() - -proc readWriteLoop(p: ChatProto) {.async.} = - await p.writeAndPrint() - -proc newChatProto(switch: Switch, transp: StreamTransport): ChatProto = - var chatproto = ChatProto(switch: switch, transp: transp, codecs: @[ChatCodec]) - - # create handler for incoming connection - proc handle(stream: Connection, proto: string) {.async.} = - if chatproto.connected and not chatproto.conn.closed: - echo "a chat session is already in progress - disconnecting!" - await stream.close() - else: - chatproto.conn = stream - chatproto.connected = true - await chatproto.readAndPrint() - - # assign the new handler - chatproto.handler = handle - return chatproto + echo exc.msg proc readInput(wfd: AsyncFD) {.thread.} = - ## This procedure performs reading from `stdin` and sends data over + ## This thread performs reading from `stdin` and sends data over ## pipe to main thread. let transp = fromPipe(wfd) @@ -150,36 +151,35 @@ proc readInput(wfd: AsyncFD) {.thread.} = let line = stdin.readLine() discard waitFor transp.write(line & "\r\n") -proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} = - let transp = fromPipe(rfd) +proc main() {.async.} = + let + rng = newRng() # Single random number source for the whole application - let seckey = PrivateKey.random(RSA, rng[]).get() - var localAddress = DefaultAddr - while true: - echo &"Type an address to bind to or Enter to use the default {DefaultAddr}" - let a = await transp.readLine() - try: - if a.len > 0: - localAddress = a - break - # uise default - break - except: - echo "invalid address" - localAddress = DefaultAddr - continue + # Pipe to read stdin from main thread + (rfd, wfd) = createAsyncPipe() + stdinReader = fromPipe(rfd) + + var thread: Thread[AsyncFD] + thread.createThread(readInput, wfd) + + var localAddress = MultiAddress.init(DefaultAddr).tryGet() var switch = SwitchBuilder - .init() - .withRng(rng) - .withPrivateKey(seckey) - .withAddress(MultiAddress.init(localAddress).tryGet()) + .new() + .withRng(rng) # Give the application RNG + .withAddress(localAddress) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager .build() - let chatProto = newChatProto(switch, transp) - switch.mount(chatProto) + let chat = Chat( + switch: switch, + stdinReader: stdinReader) + + switch.mount(ChatProto.new(chat)) + let libp2pFuts = await switch.start() - chatProto.started = true let id = $switch.peerInfo.peerId echo "PeerID: " & id @@ -187,19 +187,7 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} = for a in switch.peerInfo.addrs: echo &"{a}/p2p/{id}" - await chatProto.readWriteLoop() + await chat.readLoop() await allFuturesThrowing(libp2pFuts) -proc main() {.async.} = - let rng = newRng() # Singe random number source for the whole application - let (rfd, wfd) = createAsyncPipe() - if rfd == asyncInvalidPipe or wfd == asyncInvalidPipe: - raise newException(ValueError, "Could not initialize pipe!") - - var thread: Thread[AsyncFD] - thread.createThread(readInput, wfd) - - await processInput(rfd, rng) - -when isMainModule: # isMainModule = true when the module is compiled as the main file - waitFor(main()) +waitFor(main()) diff --git a/examples/helloworld.nim b/examples/helloworld.nim new file mode 100644 index 000000000..f6790952f --- /dev/null +++ b/examples/helloworld.nim @@ -0,0 +1,92 @@ +import bearssl +import chronos # an efficient library for async +import stew/byteutils # various utils +import ../libp2p # when installed through nimble, just use `import libp2p` + +## +# Create our custom protocol +## +const TestCodec = "/test/proto/1.0.0" # custom protocol string identifier + +type + TestProto = ref object of LPProtocol # declare a custom protocol + +proc new(T: typedesc[TestProto]): T = + + # every incoming connections will be in handled in this closure + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + echo "Got from remote - ", string.fromBytes(await conn.readLp(1024)) + await conn.writeLp("Roger p2p!") + + # We must close the connections ourselves when we're done with it + await conn.close() + + return T(codecs: @[TestCodec], handler: handle) + +## +# Helper to create a switch/node +## +proc createSwitch(ma: MultiAddress, rng: ref BrHmacDrbgContext): Switch = + var switch = SwitchBuilder + .new() + .withRng(rng) # Give the application RNG + .withAddress(ma) # Our local address(es) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager + .build() + + result = switch + +## +# The actual application +## +proc main() {.async, gcsafe.} = + let + rng = newRng() # Single random number source for the whole application + # port 0 will take a random available port + # `tryGet` will throw an exception if the Multiaddress failed + # (for instance, if the address is not well formatted) + ma1 = Multiaddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + ma2 = Multiaddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + + # setup the custom proto + let testProto = TestProto.new() + + # setup the two nodes + let + switch1 = createSwitch(ma1, rng) #Create the two switches + switch2 = createSwitch(ma2, rng) + + # mount the proto on switch1 + # the node will now listen for this proto + # and call the handler everytime a client request it + switch1.mount(testProto) + + # Start the nodes. This will start the transports + # and listen on each local addresses + let + switch1Fut = await switch1.start() + switch2Fut = await switch2.start() + + # the node addrs is populated with it's + # actual port during the start + + # use the second node to dial the first node + # using the first node peerid and address + # and specify our custom protocol codec + let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec) + + # conn is now a fully setup connection, we talk directly to the node1 custom protocol handler + await conn.writeLp("Hello p2p!") # writeLp send a length prefixed buffer over the wire + + # readLp reads length prefixed bytes and returns a buffer without the prefix + echo "Remote responded with - ", string.fromBytes(await conn.readLp(1024)) + + # We must close the connection ourselves when we're done with it + await conn.close() + + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown + +waitFor(main()) diff --git a/examples/tutorial_1_connect.md b/examples/tutorial_1_connect.md new file mode 100644 index 000000000..754683ba0 --- /dev/null +++ b/examples/tutorial_1_connect.md @@ -0,0 +1,108 @@ +Hi all, welcome to the first article of the nim-libp2p's tutorial series! + +_This tutorial is for everyone who is interested in building peer-to-peer chatting applications. No Nim programming experience is needed._ + +To give you a quick overview, **Nim** is the programming language we are using and **nim-libp2p** is the Nim implementation of [libp2p](https://libp2p.io/), a modular library that enables the development of peer-to-peer network applications. + +Hope you'll find it helpful in your journey of learning. Happy coding! ;) + +# Before you start +The only prerequisite here is [Nim](https://nim-lang.org/), the programming language with a Python-like syntax and a performance similar to C. Detailed information can be found [here](https://nim-lang.org/docs/tut1.html). + +Install Nim via their official website: [https://nim-lang.org/install.html](https://nim-lang.org/install.html) +Check Nim's installation via `nim --version` and its package manager Nimble via `nimble --version`. + +You can now install the latest version of `nim-libp2p`: +```bash +nimble install libp2p@#master +``` + +# A simple ping application +We'll start by creating a simple application, which is starting two libp2p [switch](https://docs.libp2p.io/concepts/stream-multiplexing/#switch-swarm), and pinging each other using the [Ping](https://docs.libp2p.io/concepts/protocols/#ping) protocol. + +_TIP: You can extract the code from this tutorial by running `nim c -r tools/markdown_runner.nim examples/tutorial_1_connect.md` in the libp2p folder!_ + +Let's create a `part1.nim`, and import our dependencies: +```nim +import bearssl +import chronos + +import libp2p +import libp2p/protocols/ping +``` +[bearssl](https://github.com/status-im/nim-bearssl) is used as a [cryptographic pseudorandom number generator](https://en.wikipedia.org/wiki/Cryptographically-secure_pseudorandom_number_generator) +[chronos](https://github.com/status-im/nim-chronos) the asynchronous framework used by `nim-libp2p` + +Next, we'll create an helper procedure to create our switches. A switch needs a bit of configuration, and it will be easier to do this configuration only once: +```nim +proc createSwitch(ma: MultiAddress, rng: ref BrHmacDrbgContext): Switch = + var switch = SwitchBuilder + .new() + .withRng(rng) # Give the application RNG + .withAddress(ma) # Our local address(es) + .withTcpTransport() # Use TCP as transport + .withMplex() # Use Mplex as muxer + .withNoise() # Use Noise as secure manager + .build() + + return switch +``` +This will create a switch using [Mplex](https://docs.libp2p.io/concepts/stream-multiplexing/) as a multiplexer, Noise to secure the communication, and TCP as an underlying transport. + +You can of course tweak this, to use a different or multiple transport, or tweak the configuration of Mplex and Noise, but this is some sane defaults that we'll use going forward. + + +Let's now start to create our main procedure: +```nim +proc main() {.async, gcsafe.} = + let + rng = newRng() + localAddress = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() + pingProtocol = Ping.new(rng=rng) +``` +We created some variables that we'll need for the rest of the application: the global `rng` instance, our `localAddress`, and an instance of the `Ping` protocol. +The address is in the [MultiAddress](https://github.com/multiformats/multiaddr) format. The port `0` means "take any port available". + +`tryGet` is procedure which is part of the [nim-result](https://github.com/arnetheduck/nim-result/), that will throw an exception if the supplied MultiAddress is not valid. + +We can now create our two switches: +```nim + let + switch1 = createSwitch(localAddress, rng) + switch2 = createSwitch(localAddress, rng) + + switch1.mount(pingProtocol) + + let + switch1Fut = await switch1.start() + switch2Fut = await switch2.start() +``` +We've **mounted** the `pingProtocol` on our first switch. This means that the first switch will actually listen for any ping requests coming in, and handle them accordingly. + +Now that we've started the nodes, they are listening for incoming peers. +We can find out which port was attributed, and the resulting local addresses, by using `switch1.peerInfo.addrs`. + +We'll **dial** the first switch from the second one, by specifying it's **Peer ID**, it's **MultiAddress** and the **`Ping` protocol codec**: +```nim + let conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, PingCodec) +``` +We now have a `Ping` connection setup between the second and the first switch, we can use it to actually ping the node: +```nim + # ping the other node and echo the ping duration + echo "ping: ", await pingProtocol.ping(conn) + + # We must close the connection ourselves when we're done with it + await conn.close() +``` + +And that's it! Just a little bit of cleanup: shutting down the switches, waiting for them to stop, and we'll call our `main` procedure: +```nim + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown + +waitFor(main()) +``` + +You can now run this program using `nim c -r part1.nim`, and you should see the dialing sequence, ending with a ping output. + +In the [next tutorial](tutorial_2_customproto.md), we'll look at how to create our own custom protocol. diff --git a/examples/tutorial_2_customproto.md b/examples/tutorial_2_customproto.md new file mode 100644 index 000000000..8a5b9eac7 --- /dev/null +++ b/examples/tutorial_2_customproto.md @@ -0,0 +1,82 @@ +In the [previous tutorial](tutorial_1_connect.md), we've looked at how to create a simple ping program using the `nim-libp2p`. + +We'll now look at how to create a custom protocol inside the libp2p + +# Custom protocol in libp2p +Let's create a `part2.nim`, and import our dependencies: +```nim +import bearssl +import chronos +import stew/byteutils + +import libp2p +``` +This is similar to the first tutorial, except we don't need to import the `Ping` protocol. + +Next, we'll declare our custom protocol +```nim +const TestCodec = "/test/proto/1.0.0" + +type TestProto = ref object of LPProtocol +``` + +We've set a [protocol ID](https://docs.libp2p.io/concepts/protocols/#protocol-ids), and created a custom `LPProtocol`. In a more complex protocol, we could use this structure to store interesting variables. + +A protocol generally has two part: and handling/server part, and a dialing/client part. +Theses two parts can be identical, but in our trivial protocol, the server will wait for a message from the client, and the client will send a message, so we have to handle the two cases separately. + +Let's start with the server part: +```nim +proc new(T: typedesc[TestProto]): T = + # every incoming connections will in be handled in this closure + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + echo "Got from remote - ", string.fromBytes(await conn.readLp(1024)) + # We must close the connections ourselves when we're done with it + await conn.close() + + return T(codecs: @[TestCodec], handler: handle) +``` +This is a constructor for our `TestProto`, that will specify our `codecs` and a `handler`, which will be called for each incoming peer asking for this protocol. +In our handle, we simply read a message from the connection and `echo` it. + +We can now create our client part: +```nim +proc hello(p: TestProto, conn: Connection) {.async.} = + await conn.writeLp("Hello p2p!") +``` +Again, pretty straight-forward, we just send a message on the connection. + +We can now create our main procedure: +```nim +proc main() {.async, gcsafe.} = + let + rng = newRng() + testProto = TestProto.new() + switch1 = newStandardSwitch(rng=rng) + switch2 = newStandardSwitch(rng=rng) + + switch1.mount(testProto) + + let + switch1Fut = await switch1.start() + switch2Fut = await switch2.start() + + conn = await switch2.dial(switch1.peerInfo.peerId, switch1.peerInfo.addrs, TestCodec) + + await testProto.hello(conn) + + # We must close the connection ourselves when we're done with it + await conn.close() + + await allFutures(switch1.stop(), switch2.stop()) # close connections and shutdown all transports + await allFutures(switch1Fut & switch2Fut) # wait for all transports to shutdown +``` + +This is very similar to the first tutorial's `main`, the only noteworthy difference is that we use `newStandardSwitch`, which is similar to `createSwitch` but is bundled directly in libp2p + +We can now wrap our program by calling our main proc: +```nim +waitFor(main()) +``` + +And that's it! diff --git a/libp2p.nimble b/libp2p.nimble index 5674922b6..45c7095eb 100644 --- a/libp2p.nimble +++ b/libp2p.nimble @@ -33,12 +33,18 @@ proc runTest(filename: string, verify: bool = true, sign: bool = true, exec excstr & " -d:chronicles_log_level=INFO -r" & " tests/" & filename rmFile "tests/" & filename.toExe -proc buildSample(filename: string) = +proc buildSample(filename: string, run = false) = var excstr = "nim c --opt:speed --threads:on -d:debug --verbosity:0 --hints:off" excstr.add(" --warning[CaseTransition]:off --warning[ObservableStores]:off --warning[LockLevel]:off") excstr.add(" examples/" & filename) exec excstr - rmFile "examples" & filename.toExe + if run: + exec "./examples/" & filename.toExe + rmFile "examples/" & filename.toExe + +proc buildTutorial(filename: string) = + discard gorge "cat " & filename & " | nim c -r --hints:off tools/markdown_runner.nim | " & + " nim --warning[CaseTransition]:off --warning[ObservableStores]:off --warning[LockLevel]:off c -" task testnative, "Runs libp2p native tests": runTest("testnative") @@ -75,6 +81,7 @@ task test, "Runs the test suite": exec "nimble testdaemon" exec "nimble testinterop" exec "nimble testfilter" + exec "nimble examples_build" task test_slim, "Runs the test suite": exec "nimble testnative" @@ -84,3 +91,6 @@ task test_slim, "Runs the test suite": task examples_build, "Build the samples": buildSample("directchat") + buildSample("helloworld", true) + buildTutorial("examples/tutorial_1_connect.md") + buildTutorial("examples/tutorial_2_customproto.md") diff --git a/tools/markdown_runner.nim b/tools/markdown_runner.nim new file mode 100644 index 000000000..1ed2b1e89 --- /dev/null +++ b/tools/markdown_runner.nim @@ -0,0 +1,25 @@ +import os, osproc, streams, strutils +import parseutils + +let contents = + if paramCount() > 0: + readFile(paramStr(1)) + else: + stdin.readAll() +var index = 0 + +const startDelim = "```nim\n" +const endDelim = "\n```" +while true: + let startOfBlock = contents.find(startDelim, start = index) + if startOfBlock == -1: break + + let endOfBlock = contents.find(endDelim, start = startOfBlock + startDelim.len) + if endOfBlock == -1: + quit "Unfinished block!" + + let code = contents[startOfBlock + startDelim.len .. endOfBlock] + + echo code + + index = endOfBlock + endDelim.len