diff --git a/src/SUMMARY.md b/src/SUMMARY.md index c08a0bf9..bb18000e 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -35,8 +35,8 @@ - [`reset-cache`](./kit/reset-cache.md) - [My First Kinode Application](./build-and-deploy-an-app.md) - [Environment Setup](./my_first_app/chapter_1.md) - - [Sending Some Messages, Using Some Tools](./my_first_app/chapter_2.md) - - [Defining Your Protocol](./my_first_app/chapter_3.md) + - [Sending and Responding to a Message](./my_first_app/chapter_2.md) + - [Messaging with Larger Data Types](./my_first_app/chapter_3.md) - [Frontend Time](./my_first_app/chapter_4.md) - [Sharing with the World](./my_first_app/chapter_5.md) - [In-Depth Guide: Chess App](./chess_app.md) diff --git a/src/build-and-deploy-an-app.md b/src/build-and-deploy-an-app.md index 0e2a8863..0da272bb 100644 --- a/src/build-and-deploy-an-app.md +++ b/src/build-and-deploy-an-app.md @@ -1,9 +1,9 @@ # My First Kinode Application In these tutorials, you'll setup your development environment and learn about the `kit` tools. -You'll learn about templates and also walk through writing an application from the group up, backend and frontend. +You'll learn about templates and also walk through writing an application from the ground up, backend and frontend. And finally, you'll learn how to deploy applications through the Kinode app store. -For the purposes of this documentation, terminal commands are provided as-is for ease of copying EXCEPT when the output of the command is also shown. +For the purposes of this documentation, terminal commands are provided as-is for ease of copying except when the output of the command is also shown. In that case, the command is prepended with a `$ ` to distinguish the command from the output. The `$ ` should not be copied into the terminal. diff --git a/src/my_first_app/chapter_1.md b/src/my_first_app/chapter_1.md index f57c0add..b650128f 100644 --- a/src/my_first_app/chapter_1.md +++ b/src/my_first_app/chapter_1.md @@ -1,6 +1,6 @@ # Environment Setup -In this chapter, you'll walk through setting up a Kinode development environment. +In this section, you'll walk through setting up a Kinode development environment. By the end, you will have created a Kinode application, or package, composed of one or more processes that run on a live Kinode. The application will be a simple chat interface: `my_chat_app`. @@ -53,33 +53,47 @@ Kinode packages are sets of one or more Kinode [processes](../process/processes. A Kinode package is represented in Unix as a directory that has a `pkg/` directory within. Each process within the package is its own directory. By default, the `kit new` command creates a simple, one-process package, a chat app. -Other templates, including a Python template and a UI-enabled template can be used by passing different flags to `kit new` (see `kit new --help`). +Other templates, including a Python template and a UI-enabled template can be used by passing [different flags to `kit new`](../kit/new.html#discussion). The default template looks like: ```bash $ tree my_chat_app my_chat_app -├── metadata.json -├── my_chat_app -│   ├── Cargo.toml -│   └── src -│   └── lib.rs -└── pkg - └── manifest.json + ├── Cargo.toml + ├── metadata.json + ├── my_chat_app + │ ├── Cargo.toml + │ └── src + │ └── lib.rs + ├── pkg + │ ├── manifest.json + │ └── scripts.json + └── send + ├── Cargo.toml + └── src + └── lib.rs ``` -The `my_chat_app/` package here contains one process, also named `my_chat_app/`. -The process directory contains source files and other metadata for compiling that process. +The `my_chat_app/` package here contains two processes: +- `my_chat_app/` — containing the main application code, and +- `send/` — containing a [script](../cookbook/writing_scripts.html). -In Rust processes, the standard Rust `Cargo.toml` file is included: it specifies dependencies. -It is exhaustively defined [here](https://doc.rust-lang.org/cargo/reference/manifest.html). -The `src/` directory is where the code for the process lives. +Rust process directories, like the ones here, contain: +- `src/` - source files where the code for the process lives, and +- `Cargo.toml` - the standard Rust file specifying dependencies, etc., for that process. + +Another standard Rust `Cargo.toml` file, a [virtual manifest](https://doc.rust-lang.org/cargo/reference/workspaces.html#virtual-workspace) is also included in `my_chat_app/` root. Also within the package directory is a `pkg/` directory. -The `pkg/` directory contains two files, `manifest.json` and `metadata.json`, that specify information the Kinode needs to run the package, which will be enumerated below. +The `pkg/` dirctory contains two files: +- `manifest.json` - specifes information the Kinode needs to run the package, and +- `scripts.json` - specifies details needed to run [scripts](../cookbook/writing_scripts.html). + The `pkg/` directory is also where `.wasm` binaries will be deposited by [`kit build`](#building-the-package). The files in the `pkg/` directory are injected into the Kinode with [`kit start-package`](#starting-the-package). +Lastly, `metadata.json` contains app metadata which is used in the Kinode [App Store](./chapter_5.html) + Though not included in this template, packages with a frontend have a `ui/` directory as well. For an example, look at the result of: ```bash @@ -87,7 +101,7 @@ kit new my_chat_app_with_ui --ui tree my_chat_app_with_ui ``` Note that not all templates have a UI-enabled version. -As of 240118, only the Rust chat template has a UI-enabled version. +More details about templates can be found [here](../kit/new.html#existshas-ui-enabled-version). ### `pkg/manifest.json` @@ -124,8 +138,8 @@ Key | Value Type `"process_wasm_path"` | String | The path to the process `"on_exit"` | String (`"None"` or `"Restart"`) or Object (covered [elsewhere](./chapter_2.md#aside-on_exit)) | What to do in case the process exits `"request_networking"` | Boolean | Whether to ask for networking capabilities from kernel -`"request_capabilities"` | Array of Strings or Objects | Strings are process IDs to request messaging capabilties from; Objects have a `"process"` field (process ID to request from) and a `"params"` field (capability to request) -`"grant_capabilities"` | Array of Strings or Objects | Strings are process IDs to grant messaging capabilties to; Objects have a `"process"` field (process ID to grant to) and a `"params"` field (capability to grant) +`"request_capabilities"` | Array of Strings or Objects | Strings are `ProcessId`s to request messaging capabilties from; Objects have a `"process"` field (`ProcessId` to request from) and a `"params"` field (capability to request) +`"grant_capabilities"` | Array of Strings or Objects | Strings are `ProcessId`s to grant messaging capabilties to; Objects have a `"process"` field (`ProcessId` to grant to) and a `"params"` field (capability to grant) `"public"` | Boolean | Whether to allow any process to message us ### `metadata.json` @@ -154,21 +168,21 @@ $ cat my_chat_app/metadata.json "animation_url": "" } ``` -Here, the `publisher` is some default value, but for a real package, this field should contain the KNS id of the publishing node. +Here, the `publisher` is some default value, but for a real package, this field should contain the KNS ID of the publishing node. The `publisher` can also be set with a `kit new --publisher` flag. -The rest of these fields are not required for development, but become important when publishing a package with the `app_store`. +The rest of these fields are not required for development, but become important when publishing a package with the [`app_store`](https://github.com/kinode-dao/kinode/tree/main/kinode/packages/app_store). -As an aside: each process has a unique process ID, used to address messages to that process, that looks like +As an aside: each process has a unique `processID`, used to address messages to that process, that looks like ``` :: ``` -You can read more about process IDs [here](../process/processes.md#overview). +You can read more about `processID`s [here](../process/processes.md#overview). ## Building the Package -To build the package, use the `kit build` tool. +To build the package, use the [`kit build`](../kit/build.md#) tool. This tool accepts an optional directory path as the first argument, or, if none is provided, attempts to build the current working directory. As such, either of the following will work: @@ -188,7 +202,7 @@ kit build Often, it is optimal to develop on a fake node. Fake nodes are simple to set up, easy to restart if broken, and mocked networking makes development testing very straightforward. -To boot a fake Kinode for development purposes, use the `kit boot-fake-node` tool. +To boot a fake Kinode for development purposes, use the [`kit boot-fake-node` tool](../kit/boot-fake-node.md). `kit boot-fake-node` downloads the OS- and architecture-appropriate Kinode core binary and runs it without connecting to the live network. Instead, it connects to a mocked local network, allowing different fake nodes on the same machine to communicate with each other. @@ -219,9 +233,12 @@ kit boot-fake-node --runtime-path ~/path/to/kinode where `~/path/to/kinode` must be replaced with a path to the Kinode core repo. +Note that your node will be named `fake.dev`, as opposed to `fake.os`. +The `.dev` suffix is used for development nodes. + ## Optional: Starting a Real Kinode -Alternatively, development sometimes calls for a real node, which has access to the actual Kinode network and its providers, such as integrated LLMs. +Alternatively, development sometimes calls for a real node, which has access to the actual Kinode network and its providers. To develop on a real Kinode, connect to the network and follow the instructions to [setup a Kinode](../install.md). @@ -250,7 +267,7 @@ or, if you are already in the correct package directory: kit start-package -p 8080 ``` -where here the port provided following `-p` must match the port bound by the node or fake node (see discussion [above](#booting-a-fake-kinode-node)). +where here the port provided following `-p` must match the port bound by the node or fake node (see discussion [above](#booting-a-fake-kinode)). The node's terminal should display something like @@ -265,7 +282,7 @@ Congratulations: you've now built and installed your first application on Kinode To test out the functionality of `my_chat_app`, spin up another fake node to chat with in a new terminal: ```bash -kit boot-fake-node -h /tmp/kinode-fake-node-2 -p 8081 -f fake2.os +kit boot-fake-node -h /tmp/kinode-fake-node-2 -p 8081 -f fake2.dev ``` The fake nodes communicate over a mocked local network. @@ -285,20 +302,20 @@ kit start-package -p 8081 To send a chat message from the first node, run the following in its terminal: ``` -m our@my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake2.os", "message": "hello world"}}' +m our@my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake2.dev", "message": "hello world"}}' ``` and replying, from the other terminal: ``` -m our@my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.os", "message": "wow, it works!"}}' +m our@my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.dev", "message": "wow, it works!"}}' ``` Messages can also be injected from the outside. From a bash terminal, use `kit inject-message`, like so: ```bash -kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake2.os", "message": "hello from the outside world"}}' -kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.os", "message": "replying from fake2.os using first method..."}}' --node fake2.os -kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.os", "message": "and second!"}}' -p 8081 +kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake2.dev", "message": "hello from the outside world"}}' +kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.dev", "message": "replying from fake2.dev using first method..."}}' --node fake2.dev +kit inject-message my_chat_app:my_chat_app:template.os '{"Send": {"target": "fake.dev", "message": "and second!"}}' -p 8081 ``` diff --git a/src/my_first_app/chapter_2.md b/src/my_first_app/chapter_2.md index 7ce41542..c68a0374 100644 --- a/src/my_first_app/chapter_2.md +++ b/src/my_first_app/chapter_2.md @@ -1,17 +1,47 @@ -# Sending Some Messages, Using Some Tools +# Sending and Responding to a Message -This chapter assumes you've completed the steps outlined in [Chapter 1](./chapter_1.md) to construct your dev environment or otherwise have a basic Kinode app open in your code editor of choice. +In this section you will learn how to use different parts of a process, how request-response handling works, and other implementation details with regards to messaging. +The process you will build is simple — it messages itself and responds to itself, printing whenever it gets messages. + +Note — the app you will build in Sections 2 through 5 is *not* `my_chat_app`; it is simply a series of examples designed to demonstrate how to use the system's features. + +## Requirements + +This section assumes you've completed the steps outlined in [Environment Setup](./chapter_1.md) to construct your development environment or otherwise have a basic Kinode app open in your code editor of choice. You should also be actively running a Kinode ([live](../login.md) or [fake](./chapter_1.md#booting-a-fake-kinode-node)) such that you can quickly compile and test your code! Tight feedback loops when building: very important. ## Starting from Scratch -If you want to hit the ground running, you can take the template code or the [chess tutorial](../chess_app/chess_engine.md) and start hacking away. +If you want to hit the ground running by yourself, you can take the template code or the [chess tutorial](../chess_app/chess_engine.md) and start hacking away. Here, you'll start from scratch and learn about every line of boilerplate. +Open `src/lib.rs`, clear its contents so it's empty, and code along! -The last chapter explained packages, the package manifest, and metadata. +The last section explained packages, the package manifest, and metadata. Every package contains one or more processes, which are the actual Wasm programs that will run on a node. -In order to compile properly to the Kinode environment, every process must generate the WIT bindings for the `process` "world". + +The [Generating WIT Bindings](#generating-wit-bindings) and [`init()` Function](#init-function) subsections explain the boilerplate code in detail, so if you just want to run some code, you can skip to [Running First Bits of Code](#running-first-bits-of-code). + +### Generating WIT Bindings + +For the purposes of this tutorial, crucial information from this [WASM documentation](https://component-model.bytecodealliance.org/design/why-component-model.html) has been abridged in this small subsection. + +A [Wasm component](https://component-model.bytecodealliance.org/design/components.html) is a wrapper around a core module that specifies its imports and exports. +E.g. a Go component can communicate directly and safely with a C or Rust component. +It need not even know which language another component was written in — it needs only the component interface, expressed in WIT. + +The external interface of a component - its imports and exports - is described by a [`world`](https://component-model.bytecodealliance.org/design/wit.html#worlds). +Exports are provided by the component, and define what consumers of the component may call; imports are things the component may call. +The component, however, internally defines how that `world` is implemented. +This interface is defined via [WIT](https://component-model.bytecodealliance.org/design/wit.html). + +WIT bindings are the glue code that is necessary for the interaction between WASM modules and their host environment. +They may be written in any WASM-compatible language — Kinode offers the most support for Rust with [`kit`](../kit-dev-toolkit.md) and [`process_lib`](../process_stdlib/overview.md). +The `world`, types, imports, and exports are all declared in a [WIT file](https://github.com/kinode-dao/kinode-wit/blob/master/kinode.wit), and using that file, [`wit_bindgen`](https://github.com/bytecodealliance/wit-bindgen) generates the code for the bindings. + +So, to bring it all together... + +In order to compile properly to the Kinode environment, based on the WIT file, every process must generate the WIT bindings for the `process` `world`, which is an interface for the Kinode kernel. ```rust wit_bindgen::generate!({ @@ -20,11 +50,13 @@ wit_bindgen::generate!({ }); ``` -After generating the bindings, every process must define a `Component` struct and implement the `Guest` trait for it defining a single function, `init()`. +### `init()` Function + +After generating the bindings, every process must define a `Component` struct which implements the `Guest` trait (i.e. a wrapper around the process which defines the export interface, as discussed [above](#generating-wit-bindings)). +The `Guest` trait should define a single function — `init()`. This is the entry point for the process, and the `init()` function is the first function called by the Kinode runtime when the process is started. The definition of the `Component` struct can be done manually, but it's easier to import the [`kinode_process_lib`](../process_stdlib/overview.md) crate (a sort of standard library for Kinode processes written in Rust) and use the `call_init!` macro. -Note that running the process below [can lead to an infinite loop](#aside-on_exit): ```rust use kinode_process_lib::{call_init, println, Address}; @@ -40,8 +72,11 @@ fn my_init_fn(our: Address) { } ``` -Every Kinode process written in Rust will need code that does the same thing as the above. -The [`Address` parameter](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/kinode/process/standard/struct.Address.html) tells our process what its globally-unique name is. +### Running First Bits of Code + +Every Kinode process written in Rust will need code that does the same thing as the code above (i.e. use the `wit_bindgen` and `call_init!` macros). + +The [`Address` parameter](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/kinode/process/standard/struct.Address.html) tells the process what its globally-unique name is. Let's fill out the init function with code that will stop it from exiting immediately. Here's an infinite loop that will wait for a message and then print it out. @@ -71,16 +106,15 @@ These imports are the necessary "system calls" for talking to other processes an Run ```bash -kit build your_pkg_name -kit start-package your_pkg_name -p 8080 +kit build your_pkg_directory +kit start-package your_pkg_directory -p 8080 ``` -to see this code in the node you set up in the last chapter. +to see this code in the node you set up in the last section. ## Sending a Message -Let's send a message to another process. -The `Request` type in [process_lib](../process_stdlib/overview.md) will provide all the necessary functionality. +To send a message to another process, `use` the [`Request`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/struct.Request.html) type from the [process_lib](../process_stdlib/overview.md), which will provide all the necessary functionality. ```rust use kinode_process_lib::{await_message, call_init, println, Address, Request}; ``` @@ -94,7 +128,7 @@ Request::new() .send(); ``` -Because this process might not have capabilities to message any other (local or remote) processes, just send the message to itself. +Because this process might not have capabilities to message any other (local or remote) processes, for the purposes of this tutorial, just send the message to itself. ```rust Request::new() @@ -135,19 +169,23 @@ fn my_init_fn(our: Address) { Using `kit build` and `kit start-package` like before, you should be able to see in your node's terminal the message being received in the loop. However, you'll see the "hello world" message as a byte vector. -Let's modify our request to expect a response, and our message-handling to send one back, as well as parse the received request into a string. +Change `Request::new().target()` to `Request::to()`, as using the `to()` method is recommended. +Modify your request to expect a response, and your message-handling to send one back, as well as parse the received request into a string. ```rust Request::to(&our) .body(b"hello world") .expects_response(5) .send() + .unwrap(); ``` The `expects_response` method takes a timeout in seconds. If the timeout is reached, the request will be returned to the process that sent it as an error. If you add that to the code above, you'll see the error after 5 seconds in your node's terminal. +## Responding to a Message + Now, let's add some code to handle the request. The `await_message()` function returns a type that looks like this: ```rust Result @@ -205,8 +243,7 @@ call_init!(my_init_fn); fn my_init_fn(our: Address) { println!("{our}: started"); - Request::new() - .target(&our) + Request::to(&our) .body(b"hello world") .expects_response(5) .send() @@ -242,22 +279,4 @@ fn my_init_fn(our: Address) { This basic structure can be found in the majority of Kinode processes. The other common structure is a thread-like process, that sends and handles a fixed series of messages and then exits. -In the next chapter, we will cover how to turn this very basic request-response pattern into something that can be extensible and composable. - -## Aside: `on_exit` - -As mentioned in the [previous chapter](./chapter_1.md#pkgmanifestjson), one of the fields in the `manifest.json` is `on_exit`. -When the process exits, it does one of: - -`on_exit` Setting | Behavior When Process Exits ------------------ | --------------------------- -`"None"` | Do nothing -`"Restart"` | Restart the process -JSON object | Send the requests described by the JSON object - -A process intended to do something once and exit should have `"None"` or a JSON object `on_exit`. -If it has `"Restart"`, it will repeat in an infinite loop, as referenced [above](#starting-from-scratch). - -A process intended to run over a period of time and serve requests and responses will often have `"Restart"` `on_exit` so that, in case of crash, it will start again. -Alternatively, a JSON object `on_exit` can be used to inform another process of its untimely demise. -In this way, Kinode processes become quite similar to Erlang processes in that crashing can be [designed into your process to increase reliability](https://ferd.ca/the-zen-of-erlang.html). +In the next section, we will cover how to turn this very basic request-response pattern into something that can be extensible and composable. diff --git a/src/my_first_app/chapter_3.md b/src/my_first_app/chapter_3.md index 1b11e861..74604a24 100644 --- a/src/my_first_app/chapter_3.md +++ b/src/my_first_app/chapter_3.md @@ -1,18 +1,26 @@ -# Defining Your Protocol +# Messaging with Larger Data Types -In the last chapter, you created a simple request-response pattern that uses strings as a `body` field type. +In this section, you will upgrade your app so that it can handle messages with more elaborate data types such as `enum`s and `struct`s. +Additionally, you will learn how to handle processes completing or crashing. + +## (De)Serialization With Serde + +In the last section, you created a simple request-response pattern that uses strings as a `body` field type. This is fine for certain limited cases, but in practice, most Kinode processes written in Rust use a `body` type that is serialized and deserialized to bytes using [Serde](https://serde.rs/). There are a multitude of libraries that implement Serde's `Serialize` and `Deserialize` traits, and the process developer is responsible for selecting a strategy that is appropriate for their use case. -Some popular options are `bincode` and `serde_json`. -In this chapter, you will use `serde_json` to serialize your Rust structs to a byte vector of JSON. +Some popular options are `bincode`, [`rmp_serde`](https://docs.rs/rmp-serde/latest/rmp_serde/), and `serde_json`. +In this section, you will use `serde_json` to serialize your Rust structs to a byte vector of JSON. + +### Defining the `body` Type Our old request looked like this: ```rust Request::to(&our) .body(b"hello world") .expects_response(5) - .send(); + .send() + .unwrap(); ``` What if you want to have two kinds of messages, which your process can handle differently? @@ -44,7 +52,7 @@ impl MyBody { } ``` -Now, when you form requests and response, instead of sticking a string in the `body` field, you can use the new `body` type. +Now, when you form requests and responses, instead of sticking a string in the `body` field, you can use the new `MyBody` type. This comes with a number of benefits: - You can now use the `body` field to send arbitrary data, not just strings. @@ -56,17 +64,21 @@ Defining `body` types is just one step towards writing interoperable code. It's also critical to document the overall structure of the program along with message `blob`s and `metadata` used, if any. Writing interoperable code is necessary for enabling permissionless composability, and Kinode OS aims to make this the default kind of program, unlike the centralized web. -First, create a request that uses the new `body` type (and stop expecting a response): +### Handling Messages + +In this example, you will learn how to handle a Request. +So, create a request that uses the new `body` type (you won't need to send a Response back, so we can remove `.expect_response()`): + ```rust -Request::new() - .target(&our) +Request::to(&our) .body(MyBody::hello("hello world")) - .send(); + .send() + .unwrap(); ``` Next, edit the way you handle a message in your process to use your new `body` type. The process should attempt to parse every message into the `MyBody` enum, handle the two cases, and handle any message that doesn't comport to the type. -This code goes into the `Ok(message)` case of the `match` statement on `await_message()`: +This piece of code goes into the `Ok(message)` case of the `match` statement on `await_message()`: ```rust let Ok(body) = MyBody::parse(message.body()) else { println!("{our}: received a message with weird `body`!"); @@ -95,8 +107,10 @@ if message.is_request() { } ``` +### Granting Capabilities + Finally, edit your `pkg/manifest.json` to grant the terminal process permission to send messages to this process. -That way, you can use the terminal to send Hello and Goodbye messages. +That way, you can use the terminal to send `Hello` and `Goodbye` messages. Go into the manifest, and under the process name, edit (or add) the `grant_capabilities` field like so: ```json ... @@ -106,6 +120,8 @@ Go into the manifest, and under the process name, edit (or add) the `grant_capab ... ``` +### Build and Run the Code! + After all this, your code should look like: ```rust use serde::{Serialize, Deserialize}; @@ -140,10 +156,10 @@ call_init!(my_init_fn); fn my_init_fn(our: Address) { println!("{our}: started"); - Request::new() - .target(&our) + Request::to(&our) .body(MyBody::hello("hello world")) - .send(); + .send() + .unwrap(); loop { match await_message() { @@ -181,23 +197,42 @@ fn my_init_fn(our: Address) { } } ``` -You should be able to build and start your package, then see that initial Hello message. +You should be able to build and start your package, then see that initial `Hello` message. At this point, you can use the terminal to test your message types! -First, try a hello. Get the address of your process by looking at the "started" printout that came from it in the terminal. -As a reminder, these values are set in the `metadata.json` and `manifest.json` package files. +First, try sending a `Hello` using the [`m` terminal script](../terminal.md#m---message-a-process). +Get the address of your process by looking at the "started" printout that came from it in the terminal. +As a reminder, these values (``, ``, ``) can be found in the `metadata.json` and `manifest.json` package files. + ```bash m our@:: '{"Hello": "hey there"}' ``` You should see the message text printed. Next, try a goodbye. This will cause the process to exit. + ```bash m our@:: '"Goodbye"' ``` -If you try to send another Hello now, nothing will happen, because the process has exited [(assuming you have set `on_exit: "None"`; with `on_exit: "Restart"` it will immediately start up again)](./chapter_2.md#aside-on_exit). +If you try to send another `Hello` now, nothing will happen, because the process has exited [(assuming you have set `on_exit: "None"`; with `on_exit: "Restart"` it will immediately start up again)](#aside-on_exit). Nice! You can use `kit start-package` to try again. -In the next chapter, you'll add some basic HTTP logic to serve a frontend from your simple process. +## Aside: `on_exit` + +As mentioned in the [previous section](./chapter_1.md#pkgmanifestjson), one of the fields in the `manifest.json` is `on_exit`. +When the process exits, it does one of: + +`on_exit` Setting | Behavior When Process Exits +----------------- | --------------------------- +`"None"` | Do nothing +`"Restart"` | Restart the process +JSON object | Send the requests described by the JSON object + +A process intended to do something once and exit should have `"None"` or a JSON object `on_exit`. +If it has `"Restart"`, it will repeat in an infinite loop. + +A process intended to run over a period of time and serve requests and responses will often have `"Restart"` `on_exit` so that, in case of crash, it will start again. +Alternatively, a JSON object `on_exit` can be used to inform another process of its untimely demise. +In this way, Kinode processes become quite similar to Erlang processes in that crashing can be [designed into your process to increase reliability](https://ferd.ca/the-zen-of-erlang.html). diff --git a/src/my_first_app/chapter_4.md b/src/my_first_app/chapter_4.md index d108b6b9..1e2fc5bb 100644 --- a/src/my_first_app/chapter_4.md +++ b/src/my_first_app/chapter_4.md @@ -1,9 +1,9 @@ # Frontend Time -After the last chapter, you should have a simple process that responds to two commands from the terminal. -In this chapter, you'll add some basic HTTP logic to serve a frontend and accept an HTTP PUT request that contains a command. +After the last section, you should have a simple process that responds to two commands from the terminal. +In this section, you'll add some basic HTTP logic to serve a frontend and accept an HTTP PUT request that contains a command. -If you're the type of person that prefers to learn by looking at a complete example, check out the [chess frontend chapter](../chess_app/frontend.md) for a fleshed-out example and a link to some frontend code. +If you're the type of person that prefers to learn by looking at a complete example, check out the [chess frontend section](../chess_app/frontend.md) for a fleshed-out example and a link to some frontend code. ## Adding HTTP request handling @@ -11,33 +11,37 @@ Using the built-in HTTP server will require handling a new type of request in ou The [process_lib](../process_stdlib/overview.md) contains types and functions for doing so. At the top of your process, import `http`, `get_blob`, and `Message` from [`kinode_process_lib`](../process_stdlib/overview.md) along with the rest of the imports. -You'll use `get_blob()` to grab the body bytes of an incoming HTTP request. +You'll use `get_blob()` to grab the `body` bytes of an incoming HTTP request. ```rust use kinode_process_lib::{ await_message, call_init, get_blob, http, println, Address, Message, Request, Response, }; ``` -Keep the custom `body` type the same, and keep using that for terminal input. +Keep the custom `body` type (i.e. `MyBody`) the same, and keep using that for terminal input. -At the beginning of the init function, in order to receive HTTP requests, you must use the `kinode_process_lib::http` library to bind a new path. Binding a path will cause the process to receive all HTTP requests that match that path. +At the beginning of the init function (here `my_init_fn()`), in order to receive HTTP requests, you must use the [`kinode_process_lib::http`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/http/index.html) library to bind a new path. +Binding a path will cause the process to receive all HTTP requests that match that path. You can also bind static content to a path using another function in the library. + ```rust // ... fn my_init_fn(our: Address) { println!("{our}: started"); - // the first argument is the path to bind. Note that requests will be namespaced - // under the process name, so this will be accessible at /my_process/ - // the second argument marks whether to serve the path only to authenticated clients, - // and the third argument marks whether to only serve the path locally. - // in order to skip authentication, set the second argument to false here. http::bind_http_path("/", false, false).unwrap(); // ... } // ... ``` +`http::bind_http_path("/", false, false)` arguments mean the following: +- The first argument is the path to bind. +Note that requests will be namespaced under the process name, so this will be accessible at e.g. `/my_process_name/`. +- The second argument marks whether to serve the path only to authenticated clients +In order to skip authentication, set the second argument to false here. +- The third argument marks whether to only serve the path locally. + Now that you're handling multiple kinds of requests, let's refactor the loop to be more concise and move the request-specific logic to dedicated functions. Put this right under the bind command: ```rust @@ -60,9 +64,9 @@ loop { ``` Note that different apps will want to discriminate between incoming messages differently. -This code doesn't check the `source.node` at all, for example. +This code doesn't check the [`source`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/enum.Message.html#method.source)[`.node`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/kinode/process/standard/struct.Address.html#method.node) at all, for example. -The `handle_hello_message` will look just like what was in chapter 3. +The `handle_hello_message` will look just like what was in [Section 5.3.](./chapter_3.md) However, since this logic is no longer inside the main loop, return a boolean to indicate whether or not to exit out of the loop. Request handling can be separated out into as many functions is needed to keep the code clean. ```rust @@ -97,6 +101,8 @@ fn handle_hello_message(message: &Message) -> bool { } ``` +### Handling an HTTP Message + Finally, let's define `handle_http_message`. ```rust fn handle_http_message(our: &Address, message: &Message) { @@ -104,7 +110,8 @@ fn handle_http_message(our: &Address, message: &Message) { } ``` -Instead of parsing our `body` type from the message, parse the type that the `http_server` process gives us. This type is defined in the `kinode_process_lib::http` module for us: +Instead of directly parsing the `body` type from the message, parse the type that the `http_server` process gives us. +This type is defined in the `kinode_process_lib::http` module for us: ```rust // ... let Ok(server_request) = http::HttpServerRequest::from_bytes(message.body()) else { @@ -114,9 +121,10 @@ let Ok(server_request) = http::HttpServerRequest::from_bytes(message.body()) els // ... ``` -Next, you must parse out the HTTP request from the general type. +Next, you must parse out the HTTP request from the `HttpServerRequest`. This is necessary because the `HttpServerRequest` enum contains both HTTP protocol requests and requests related to WebSockets. -Note that it's quite possible to streamline this series of request refinements if you're only interested in one type of request — this example is overly thorough for demonstration purposes. +If your application only needs to handle one type of request (e.g., only HTTP requests), you could simplify the code by directly handling that type without having to check for a specific request type from the `HttpServerRequest` enum each time. +This example is overly thorough for demonstration purposes. ```rust // ... @@ -137,7 +145,7 @@ if http_request.method().unwrap() != http::Method::PUT { // ... ``` -Finally, grab the `blob` from the request, send a 200 OK response to the client, and handle the `blob`, by sending a Request to ourselves with the `blob` as the `body`. +Finally, grab the `blob` from the request, send a `200 OK` response to the client, and handle the `blob` by sending a `Request` to ourselves with the `blob` as the `body`. This could be done in a different way, but this simple pattern is useful for letting HTTP requests masquerade as in-Kinode requests. ```rust // ... @@ -145,11 +153,13 @@ let Some(body) = get_blob() else { println!("received a PUT HTTP request with no body, skipping"); return; }; -http::send_response(http::StatusCode::OK, None, vec![]).unwrap(); +http::send_response(http::StatusCode::OK, None, vec![]); Request::to(our).body(body.bytes).send().unwrap(); ``` -Putting it all together, you get a process which you can build and start, then use cURL to send Hello and Goodbye requests via HTTP PUTs! +Putting it all together, you get a process which you can build and start, then use cURL to send `Hello` and `Goodbye` requests via HTTP PUTs! + +### Requesting Capabilities Also, remember to request the capability to message `http_server` in `manifest.json`: ```json @@ -160,7 +170,8 @@ Also, remember to request the capability to message `http_server` in `manifest.j ... ``` -Here's the full code: +### The Full Code + ```rust use serde::{Deserialize, Serialize}; use kinode_process_lib::{ @@ -198,8 +209,7 @@ fn my_init_fn(our: Address) { http::bind_http_path("/", false, false).unwrap(); - Request::new() - .target(&our) + Request::to(&our) .body(MyBody::hello("hello world")) .send() .unwrap(); @@ -240,7 +250,7 @@ fn handle_http_message(our: &Address, message: &Message) { println!("received a PUT HTTP request with no body, skipping"); return; }; - http::send_response(http::StatusCode::OK, None, vec![]).unwrap(); + http::send_response(http::StatusCode::OK, None, vec![]); Request::to(our).body(body.bytes).send().unwrap(); } @@ -275,12 +285,13 @@ fn handle_hello_message(message: &Message) -> bool { } ``` -A cURL command to send a Hello request looks like this. +A cURL command to send a `Hello` request looks like this. Make sure to replace the URL with your node's local port and the correct process name. -Note: if you had not set `authenticated` to false in the bind command, you would need to add an `Authorization` header to this request with the JWT cookie of your node. +Note: if you had not set `authenticated` to false in the bind command, you would need to add an `Authorization` header to this request with the [JWT](https://jwt.io/) cookie of your node. This is saved in your browser automatically on login. + ```bash -curl -X PUT -H "Content-Type: application/json" -d '{"Hello": "greetings"}' "http://localhost:8080/tutorial:tutorial:template.os" +curl -X PUT -H "Content-Type: application/json" -d '{"Hello": "greetings"}' "http://localhost:8080/my_process:my_package:template.os" ``` ## Serving a static frontend @@ -288,18 +299,23 @@ curl -X PUT -H "Content-Type: application/json" -d '{"Hello": "greetings"}' "htt If you just want to serve an API, you've seen enough now to handle PUTs and GETs to your heart's content. But the classic personal node app also serves a webpage that provides a user interface for your program. -You *could* add handling to our `/` path to dynamically serve some HTML on every GET. -But for maximum ease and efficiency, use the static bind command on `/` and move our PUT handling to `/api`. +You *could* add handling to root `/` path to dynamically serve some HTML on every GET. +But for maximum ease and efficiency, use the static bind command on `/` and move the PUT handling to `/api`. To do this, edit the bind commands in `my_init_fn` to look like this: + ```rust http::bind_http_path("/api", true, false).unwrap(); -http::serve_index_html(&our, "ui").unwrap(); +http::serve_index_html(&our, "ui", true, false, vec!["/"]).unwrap(); ``` +Note that you are setting `authenticated` to `true` in the `serve_index_html` and `bind_http_path` calls. +The result of this is that the webpage will be able to get served by the browser, but not by the raw cURL request. + Now you can add a static `index.html` file to the package. UI files are stored in the `ui/` directory and built into the application by `kit build` automatically. -Create a new file in `ui/index.html` with the following contents. +Create a `ui/` directory in the package root, and then a new file in `ui/index.html` with the following contents. **Make sure to replace the fetch URL with your process ID!** + ```html @@ -318,7 +334,7 @@ Create a new file in `ui/index.html` with the following contents.