···11-# excuse
22-33-mount jetstream as a userspace character device on linux.
44-55-## usage
66-77-```
88-λ cargo b
99-λ sudo RUST_LOG=info ./target/debug/excuse
1010-1111-# -- in another shell --
1212-λ cat /dev/jetstream | jq
1313-```
1414-1515-## motivation
1616-1717-> People who think that userspace filesystems are realistic
1818-> for anything but toys are just misguided
1919->
2020-> Linus Torvalds
+42
readme.txt
···11+excuse - mount jetstream as a character device on linux
22+33+CUSE is FUSE's little brother, and it allows mounting a
44+single character device file in /dev, with the device driver
55+being implemented in userspace.
66+77+unlike typical files, character devices like /dev/urandom
88+are not seekable. they only support a streaming interface:
99+1010+ λ cat /dev/urandom # random numbers
1111+1212+excuse simply mounts a websocket connection to an atproto
1313+jetstream as a userspace character device:
1414+1515+ λ sudo RUST_LOG=info excuse
1616+ [2025-09-21 INFO excuse] Initializing CUSE at /dev/jetstream
1717+ [2025-09-21 INFO excuse] /dev/jetstream is now 0644
1818+1919+in another terminal:
2020+2121+ λ cat /dev/jetstream | jq
2222+ {
2323+ "did": "did:plc:s6zjj6aw652mmvsrs573j6ti",
2424+ "time_us": 1758442606746247,
2525+ "kind": "commit",
2626+ "commit": { ... }
2727+ }
2828+ .
2929+ .
3030+ .
3131+3232+you will notice a couple of optimizations:
3333+3434+ - the websocket is only opened when the file has readers,
3535+ and is closed when all readers are closed
3636+ - the same connection is shared by all readers
3737+ - the data is buffered and not sent byte by byte
3838+3939+it is a bit of a gimmick however, you can do this easily
4040+with websocat:
4141+4242+ λ websocat wss://jetstream1.us-east.fire.hose.cam/subscribe
+5-7
src/main.rs
···9191 break;
9292 }
9393 Some(Err(e)) => {
9494- error!("websocket error: {}", e);
9494+ error!("websocket error: {e}");
9595 break;
9696 }
9797 None => {
···105105 }
106106 }
107107 Err(e) => {
108108- error!("failed to connect to websocket: {}", e);
108108+ error!("failed to connect to websocket: {e}");
109109 }
110110 }
111111···182182 reply: fuser::ReplyEmpty,
183183 ) {
184184 info!(
185185- "release(ino: {:#x?}, fh: {}, flags: {:#x?}, lock_owner: {:?}, flush: {})",
186186- ino, fh, flags, lock_owner, flush
185185+ "release(ino: {ino:#x?}, fh: {fh}, flags: {flags:#x?}, lock_owner: {lock_owner:?}, flush: {flush})"
187186 );
188187 self.end_stream();
189188 reply.ok();
···198197 let handle = thread::spawn(|| {
199198 fuser::cuse(device).unwrap_or_else(|e| {
200199 error!(
201201- "failed to start cuse device: {}. try run this example as privileged user",
202202- e
200200+ "failed to start cuse device: {e}. try run this example as privileged user"
203201 );
204202 std::process::exit(1);
205203 })
···207205208206 // make the device readable without sudo
209207 let output = Command::new("chmod")
210210- .args(&["644", &format!("/dev/{DEV}")])
208208+ .args(["644", &format!("/dev/{DEV}")])
211209 .output();
212210213211 match output {