├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src └── main.rs └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "examples_io_event_loop" 5 | version = "0.1.0" 6 | dependencies = [ 7 | "minimio 0.1.0 (git+https://github.com/cfsamson/examples-minimio?branch=node-experiment)", 8 | ] 9 | 10 | [[package]] 11 | name = "minimio" 12 | version = "0.1.0" 13 | source = "git+https://github.com/cfsamson/examples-minimio?branch=node-experiment#adade7634fd28f35a952830f705fc9423bdaccb4" 14 | 15 | [metadata] 16 | "checksum minimio 0.1.0 (git+https://github.com/cfsamson/examples-minimio?branch=node-experiment)" = "" 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples_io_event_loop" 3 | version = "0.1.0" 4 | authors = ["Carl Fredrik Samson "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | minimio = {git = "https://github.com/cfsamson/examples-minimio", branch = "node-experiment"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simplified implementation of Nodes event loop and async handling 2 | 3 | This is the example code which belongs to the gitbook [The Node Experiment - Exploring Async Basics With Rust](https://cfsamson.github.io/book-exploring-async-basics/), a gitbook explaining how concurrency is handled by operating systems, programming languages and runtimes. 4 | 5 | Node is a good example since they use two different strategies for handling async and parallel execution depending on if the task is mostly IO bound or CPU bound. Since it's well known and there is already many articles trying to explain how it works, I think it's cool to take a deeper dive and try to understand this more in detail while we investigate how async programming is working. 6 | 7 | This is part of my goal to explain Futures in Rust from the ground up. To understand even the most high level concept truly we need to understand what's happening on the low level or else there will be too much magic which prevents any profound understanding of the subject. 8 | 9 | ### Updates: 10 | 11 | #### 2019-12-18 12 | 13 | Working on the book explaining the Epoll/IOCP/Kqueue implementation backing this event loop I have made some minor revisions to the API, and a bigger change to the underlying `IOCP` implementation. 14 | 15 | - Interests is now expressed using constants: `Interests::readable()` is now `Interests::READABLE` 16 | - `Token` is refactored to a plain `usize` instead of wrapping it, simplifying the code. That means it's no longer need to call `Token::id()::value()` to get the value of the token. 17 | - Some combinations of "javascript" gave a fault when trying to exit the loop on Windows. This turned out to be caused by an oversight on my part on how `CompletionKey` worked. It's now fixed. 18 | - Pinned the dependency of `minimio` to the `node-experiment` branch -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /// Think of this function as the javascript program you have written 2 | fn javascript() { 3 | print("First call to read test.txt"); 4 | Fs::read("test.txt", |result| { 5 | let text = result.into_string().unwrap(); 6 | let len = text.len(); 7 | print(format!("First count: {} characters.", len)); 8 | 9 | print(r#"I want to create a "magic" number based on the text."#); 10 | Crypto::encrypt(text.len(), |result| { 11 | let n = result.into_int().unwrap(); 12 | print(format!(r#""Encrypted" number is: {}"#, n)); 13 | }) 14 | }); 15 | 16 | print("Registering immediate timeout 1"); 17 | set_timeout(0, |_res| { 18 | print("Immediate1 timed out"); 19 | }); 20 | print("Registering immediate timeout 2"); 21 | set_timeout(0, |_res| { 22 | print("Immediate2 timed out"); 23 | }); 24 | 25 | // let's read the file again and display the text 26 | print("Second call to read test.txt"); 27 | Fs::read("test.txt", |result| { 28 | let text = result.into_string().unwrap(); 29 | let len = text.len(); 30 | print(format!("Second count: {} characters.", len)); 31 | 32 | // aaand one more time but not in parallel. 33 | print("Third call to read test.txt"); 34 | Fs::read("test.txt", |result| { 35 | let text = result.into_string().unwrap(); 36 | print_content(&text, "file read"); 37 | }); 38 | }); 39 | 40 | print("Registering a 3000 and a 500 ms timeout"); 41 | set_timeout(3000, |_res| { 42 | print("3000ms timer timed out"); 43 | set_timeout(500, |_res| { 44 | print("500ms timer(nested) timed out"); 45 | }); 46 | }); 47 | 48 | print("Registering a 1000 ms timeout"); 49 | set_timeout(1000, |_res| { 50 | print("SETTIMEOUT"); 51 | }); 52 | 53 | // `http_get_slow` let's us define a latency we want to simulate 54 | print("Registering http get request to google.com"); 55 | Http::http_get_slow("http//www.google.com", 2000, |result| { 56 | let result = result.into_string().unwrap(); 57 | print_content(result.trim(), "web call"); 58 | }); 59 | } 60 | 61 | fn main() { 62 | let rt = Runtime::new(); 63 | rt.run(javascript); 64 | } 65 | 66 | // ===== THIS IS OUR "NODE LIBRARY" ===== 67 | use std::collections::{BTreeMap, HashMap}; 68 | use std::fmt; 69 | use std::fs; 70 | use std::io::{self, Read, Write}; 71 | use std::sync::mpsc::{channel, Receiver, Sender}; 72 | use std::sync::{Arc, Mutex}; 73 | use std::thread::{self, JoinHandle}; 74 | use std::time::{Duration, Instant}; 75 | 76 | use minimio; 77 | 78 | static mut RUNTIME: *mut Runtime = std::ptr::null_mut(); 79 | 80 | struct Task { 81 | task: Box Js + Send + 'static>, 82 | callback_id: usize, 83 | kind: ThreadPoolTaskKind, 84 | } 85 | 86 | impl Task { 87 | fn close() -> Self { 88 | Task { 89 | task: Box::new(|| Js::Undefined), 90 | callback_id: 0, 91 | kind: ThreadPoolTaskKind::Close, 92 | } 93 | } 94 | } 95 | 96 | pub enum ThreadPoolTaskKind { 97 | FileRead, 98 | Encrypt, 99 | Close, 100 | } 101 | 102 | impl fmt::Display for ThreadPoolTaskKind { 103 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 104 | use ThreadPoolTaskKind::*; 105 | match self { 106 | FileRead => write!(f, "File read"), 107 | Encrypt => write!(f, "Encrypt"), 108 | Close => write!(f, "Close"), 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug)] 114 | pub enum Js { 115 | Undefined, 116 | String(String), 117 | Int(usize), 118 | } 119 | 120 | impl Js { 121 | /// Convenience method since we know the types 122 | fn into_string(self) -> Option { 123 | match self { 124 | Js::String(s) => Some(s), 125 | _ => None, 126 | } 127 | } 128 | 129 | /// Convenience method since we know the types 130 | fn into_int(self) -> Option { 131 | match self { 132 | Js::Int(n) => Some(n), 133 | _ => None, 134 | } 135 | } 136 | } 137 | 138 | /// NodeTheread represents a thread in our threadpool. Each event has a Joinhandle 139 | /// and a transmitter part of a channel which is used to inform our main loop 140 | /// about what events has occurred. 141 | #[derive(Debug)] 142 | struct NodeThread { 143 | pub(crate) handle: JoinHandle<()>, 144 | sender: Sender, 145 | } 146 | 147 | pub struct Runtime { 148 | /// Available threads for the threadpool 149 | available_threads: Vec, 150 | /// Callbacks scheduled to run 151 | callbacks_to_run: Vec<(usize, Js)>, 152 | /// All registered callbacks 153 | callback_queue: HashMap>, 154 | /// Number of pending epoll events, only used by us to print for this example 155 | epoll_pending_events: usize, 156 | /// Our event registrator which registers interest in events with the OS 157 | epoll_registrator: minimio::Registrator, 158 | // The handle to our epoll thread 159 | epoll_thread: thread::JoinHandle<()>, 160 | /// None = infinite, Some(n) = timeout in n ms, Some(0) = immidiate 161 | epoll_timeout: Arc>>, 162 | /// Channel used by both our threadpool and our epoll thread to send events 163 | /// to the main loop 164 | event_reciever: Receiver, 165 | /// Creates an unique identity for our callbacks 166 | identity_token: usize, 167 | /// The number of events pending. When this is zero, we're done 168 | pending_events: usize, 169 | /// Handles to our threads in the threadpool 170 | thread_pool: Vec, 171 | /// Holds all our timers, and an Id for the callback to run once they expire 172 | timers: BTreeMap, 173 | /// A struct to temporarely hold timers to remove. We let Runtinme have 174 | /// ownership so we can reuse the same memory 175 | timers_to_remove: Vec, 176 | } 177 | 178 | /// Describes the three main events our epoll-eventloop handles 179 | enum PollEvent { 180 | /// An event from the `threadpool` with a tuple containing the `thread id`, 181 | /// the `callback_id` and the data which the we expect to process in our 182 | /// callback 183 | Threadpool((usize, usize, Js)), 184 | /// An event from the epoll-based eventloop holding the `event_id` for the 185 | /// event 186 | Epoll(usize), 187 | Timeout, 188 | } 189 | 190 | impl Runtime { 191 | pub fn new() -> Self { 192 | // ===== THE REGULAR THREADPOOL ===== 193 | let (event_sender, event_reciever) = channel::(); 194 | let mut threads = Vec::with_capacity(4); 195 | 196 | for i in 0..4 { 197 | let (evt_sender, evt_reciever) = channel::(); 198 | let event_sender = event_sender.clone(); 199 | 200 | let handle = thread::Builder::new() 201 | .name(format!("pool{}", i)) 202 | .spawn(move || { 203 | 204 | while let Ok(task) = evt_reciever.recv() { 205 | print(format!("recived a task of type: {}", task.kind)); 206 | 207 | if let ThreadPoolTaskKind::Close = task.kind { 208 | break; 209 | }; 210 | 211 | let res = (task.task)(); 212 | print(format!("finished running a task of type: {}.", task.kind)); 213 | 214 | let event = PollEvent::Threadpool((i, task.callback_id, res)); 215 | event_sender.send(event).expect("threadpool"); 216 | } 217 | }) 218 | .expect("Couldn't initialize thread pool."); 219 | 220 | let node_thread = NodeThread { 221 | handle, 222 | sender: evt_sender, 223 | }; 224 | 225 | threads.push(node_thread); 226 | } 227 | 228 | // ===== EPOLL THREAD ===== 229 | let mut poll = minimio::Poll::new().expect("Error creating epoll queue"); 230 | let registrator = poll.registrator(); 231 | let epoll_timeout = Arc::new(Mutex::new(None)); 232 | let epoll_timeout_clone = epoll_timeout.clone(); 233 | 234 | let epoll_thread = thread::Builder::new() 235 | .name("epoll".to_string()) 236 | .spawn(move || { 237 | let mut events = minimio::Events::with_capacity(1024); 238 | 239 | loop { 240 | let epoll_timeout_handle = epoll_timeout_clone.lock().unwrap(); 241 | let timeout = *epoll_timeout_handle; 242 | drop(epoll_timeout_handle); 243 | 244 | match poll.poll(&mut events, timeout) { 245 | Ok(v) if v > 0 => { 246 | for i in 0..v { 247 | let event = events.get_mut(i).expect("No events in event list."); 248 | print(format!("epoll event {} is ready", event.id())); 249 | 250 | let event = PollEvent::Epoll(event.id()); 251 | event_sender.send(event).expect("epoll event"); 252 | } 253 | } 254 | Ok(v) if v == 0 => { 255 | print("epoll event timeout is ready"); 256 | event_sender.send(PollEvent::Timeout).expect("epoll timeout"); 257 | } 258 | Err(ref e) if e.kind() == io::ErrorKind::Interrupted => { 259 | print("recieved event of type: Close"); 260 | break; 261 | } 262 | Err(e) => panic!("{:?}", e), 263 | _ => unreachable!(), 264 | } 265 | } 266 | }) 267 | .expect("Error creating epoll thread"); 268 | 269 | Runtime { 270 | available_threads: (0..4).collect(), 271 | callbacks_to_run: vec![], 272 | callback_queue: HashMap::new(), 273 | epoll_pending_events: 0, 274 | epoll_registrator: registrator, 275 | epoll_thread, 276 | epoll_timeout, 277 | event_reciever, 278 | identity_token: 0, 279 | pending_events: 0, 280 | thread_pool: threads, 281 | timers: BTreeMap::new(), 282 | timers_to_remove: vec![], 283 | } 284 | } 285 | 286 | /// This is the event loop. There are several things we could do here to 287 | /// make it a better implementation. One is to set a max backlog of callbacks 288 | /// to execute in a single tick, so we don't starve the threadpool or file 289 | /// handlers. Another is to dynamically decide if/and how long the thread 290 | /// could be allowed to be parked for example by looking at the backlog of 291 | /// events, and if there is any backlog disable it. Some of our Vec's will 292 | /// only grow, and not resize, so if we have a period of very high load, the 293 | /// memory will stay higher than we need until a restart. This could be 294 | /// dealt by using a different kind of data structure like a `LinkedList`. 295 | pub fn run(mut self, f: impl Fn()) { 296 | let rt_ptr: *mut Runtime = &mut self; 297 | unsafe { RUNTIME = rt_ptr }; 298 | 299 | // just for us priting out during execution 300 | let mut ticks = 0; 301 | 302 | // First we run our "main" function 303 | f(); 304 | 305 | // ===== EVENT LOOP ===== 306 | while self.pending_events > 0 { 307 | ticks += 1; 308 | // NOT PART OF LOOP, JUST FOR US TO SEE WHAT TICK IS EXCECUTING 309 | print(format!("===== TICK {} =====", ticks)); 310 | 311 | // ===== 2. TIMERS ===== 312 | self.process_expired_timers(); 313 | 314 | // ===== 2. CALLBACKS ===== 315 | // Timer callbacks and if for some reason we have postponed callbacks 316 | // to run on the next tick. Not possible in our implementation though. 317 | self.run_callbacks(); 318 | 319 | // ===== 3. IDLE/PREPARE ===== 320 | // we won't use this 321 | 322 | // ===== 4. POLL ===== 323 | // First we need to check if we have any outstanding events at all 324 | // and if not we're finished. If not we will wait forever. 325 | if self.pending_events == 0 { 326 | break; 327 | } 328 | 329 | // We want to get the time to the next timeout (if any) and we 330 | // set the timeout of our epoll wait to the same as the timeout 331 | // for the next timer. If there is none, we set it to infinite (None) 332 | let next_timeout = self.get_next_timer(); 333 | 334 | let mut epoll_timeout_lock = self.epoll_timeout.lock().unwrap(); 335 | *epoll_timeout_lock = next_timeout; 336 | // We release the lock before we wait in `recv` 337 | drop(epoll_timeout_lock); 338 | 339 | // We handle one and one event but multiple events could be returned 340 | // on the same poll. We won't cover that here though but there are 341 | // several ways of handling this. 342 | if let Ok(event) = self.event_reciever.recv() { 343 | match event { 344 | PollEvent::Timeout => (), 345 | PollEvent::Threadpool((thread_id, callback_id, data)) => { 346 | self.process_threadpool_events(thread_id, callback_id, data); 347 | } 348 | PollEvent::Epoll(event_id) => { 349 | self.process_epoll_events(event_id); 350 | } 351 | } 352 | } 353 | self.run_callbacks(); 354 | 355 | // ===== 5. CHECK ===== 356 | // an set immidiate function could be added pretty easily but we 357 | // won't do that here 358 | 359 | // ===== 6. CLOSE CALLBACKS ====== 360 | // Release resources, we won't do that here, but this is typically 361 | // where sockets etc are closed. 362 | } 363 | 364 | // We clean up our resources, makes sure all destructors runs. 365 | for thread in self.thread_pool.into_iter() { 366 | thread.sender.send(Task::close()).expect("threadpool cleanup"); 367 | thread.handle.join().unwrap(); 368 | } 369 | 370 | self.epoll_registrator.close_loop().unwrap(); 371 | self.epoll_thread.join().unwrap(); 372 | 373 | print("FINISHED"); 374 | } 375 | 376 | fn process_expired_timers(&mut self) { 377 | // Need an intermediate variable to please the borrowchecker 378 | let timers_to_remove = &mut self.timers_to_remove; 379 | 380 | self.timers 381 | .range(..=Instant::now()) 382 | .for_each(|(k, _)| timers_to_remove.push(*k)); 383 | 384 | while let Some(key) = self.timers_to_remove.pop() { 385 | let callback_id = self.timers.remove(&key).unwrap(); 386 | self.callbacks_to_run.push((callback_id, Js::Undefined)); 387 | } 388 | } 389 | 390 | fn get_next_timer(&self) -> Option { 391 | self.timers.iter().nth(0).map(|(&instant, _)| { 392 | let mut time_to_next_timeout = instant - Instant::now(); 393 | 394 | if time_to_next_timeout < Duration::new(0, 0) { 395 | time_to_next_timeout = Duration::new(0, 0); 396 | } 397 | 398 | time_to_next_timeout.as_millis() as i32 399 | }) 400 | } 401 | 402 | fn run_callbacks(&mut self) { 403 | while let Some((callback_id, data)) = self.callbacks_to_run.pop() { 404 | let cb = self.callback_queue.remove(&callback_id).unwrap(); 405 | cb(data); 406 | self.pending_events -= 1; 407 | } 408 | } 409 | 410 | fn process_epoll_events(&mut self, event_id: usize) { 411 | self.callbacks_to_run.push((event_id, Js::Undefined)); 412 | self.epoll_pending_events -= 1; 413 | } 414 | 415 | fn process_threadpool_events(&mut self, thread_id: usize, callback_id: usize, data: Js) { 416 | self.callbacks_to_run.push((callback_id, data)); 417 | self.available_threads.push(thread_id); 418 | } 419 | 420 | fn get_available_thread(&mut self) -> usize { 421 | match self.available_threads.pop() { 422 | Some(thread_id) => thread_id, 423 | // We would normally return None and the request and not panic! 424 | None => panic!("Out of threads."), 425 | } 426 | } 427 | 428 | /// If we hit max we just wrap around 429 | fn generate_identity(&mut self) -> usize { 430 | self.identity_token = self.identity_token.wrapping_add(1); 431 | self.identity_token 432 | } 433 | 434 | fn generate_cb_identity(&mut self) -> usize { 435 | let ident = self.generate_identity(); 436 | let taken = self.callback_queue.contains_key(&ident); 437 | 438 | // if there is a collision or the identity is already there we loop until we find a new one 439 | // we don't cover the case where there are `usize::MAX` number of callbacks waiting since 440 | // that if we're fast and queue a new event every nanosecond that will still take 585.5 years 441 | // to do on a 64 bit system. 442 | if !taken { 443 | ident 444 | } else { 445 | loop { 446 | let possible_ident = self.generate_identity(); 447 | if self.callback_queue.contains_key(&possible_ident) { 448 | break possible_ident; 449 | } 450 | } 451 | } 452 | } 453 | 454 | /// Adds a callback to the queue and returns the key 455 | fn add_callback(&mut self, ident: usize, cb: impl FnOnce(Js) + 'static) { 456 | let boxed_cb = Box::new(cb); 457 | self.callback_queue.insert(ident, boxed_cb); 458 | } 459 | 460 | pub fn register_event_epoll(&mut self, token: usize, cb: impl FnOnce(Js) + 'static) { 461 | self.add_callback(token, cb); 462 | 463 | print(format!("Event with id: {} registered.", token)); 464 | self.pending_events += 1; 465 | self.epoll_pending_events += 1; 466 | } 467 | 468 | pub fn register_event_threadpool( 469 | &mut self, 470 | task: impl Fn() -> Js + Send + 'static, 471 | kind: ThreadPoolTaskKind, 472 | cb: impl FnOnce(Js) + 'static, 473 | ) { 474 | let callback_id = self.generate_cb_identity(); 475 | self.add_callback(callback_id, cb); 476 | 477 | let event = Task { 478 | task: Box::new(task), 479 | callback_id, 480 | kind, 481 | }; 482 | 483 | // we are not going to implement a real scheduler here, just a LIFO queue 484 | let available = self.get_available_thread(); 485 | self.thread_pool[available].sender.send(event).expect("register work"); 486 | self.pending_events += 1; 487 | } 488 | 489 | fn set_timeout(&mut self, ms: u64, cb: impl Fn(Js) + 'static) { 490 | // Is it theoretically possible to get two equal instants? If so we'll have a bug... 491 | let now = Instant::now(); 492 | 493 | let cb_id = self.generate_cb_identity(); 494 | self.add_callback(cb_id, cb); 495 | 496 | let timeout = now + Duration::from_millis(ms); 497 | self.timers.insert(timeout, cb_id); 498 | 499 | self.pending_events += 1; 500 | print(format!("Registered timer event id: {}", cb_id)); 501 | } 502 | } 503 | 504 | pub fn set_timeout(ms: u64, cb: impl Fn(Js) + 'static) { 505 | let rt = unsafe { &mut *(RUNTIME as *mut Runtime) }; 506 | rt.set_timeout(ms, cb); 507 | } 508 | 509 | // ===== THIS IS PLUGINS CREATED IN C++ FOR THE NODE RUNTIME OR PART OF THE RUNTIME ITSELF ===== 510 | // The pointer dereferencing of our runtime is not striclty needed but is mostly for trying to 511 | // emulate a bit of the same feeling as when you use modules in javascript. We could pass the runtime in 512 | // as a reference to our startup function. 513 | 514 | struct Crypto; 515 | impl Crypto { 516 | fn encrypt(n: usize, cb: impl Fn(Js) + 'static + Clone) { 517 | let work = move || { 518 | fn fibonacchi(n: usize) -> usize { 519 | match n { 520 | 0 => 0, 521 | 1 => 1, 522 | _ => fibonacchi(n - 1) + fibonacchi(n - 2), 523 | } 524 | } 525 | 526 | let fib = fibonacchi(n); 527 | Js::Int(fib) 528 | }; 529 | 530 | let rt = unsafe { &mut *RUNTIME }; 531 | rt.register_event_threadpool(work, ThreadPoolTaskKind::Encrypt, cb); 532 | } 533 | } 534 | 535 | struct Fs; 536 | impl Fs { 537 | fn read(path: &'static str, cb: impl Fn(Js) + 'static) { 538 | let work = move || { 539 | // Let's simulate that there is a very large file we're reading allowing us to actually 540 | // observe how the code is executed 541 | thread::sleep(std::time::Duration::from_secs(1)); 542 | let mut buffer = String::new(); 543 | fs::File::open(&path) 544 | .unwrap() 545 | .read_to_string(&mut buffer) 546 | .unwrap(); 547 | Js::String(buffer) 548 | }; 549 | let rt = unsafe { &mut *RUNTIME }; 550 | rt.register_event_threadpool(work, ThreadPoolTaskKind::FileRead, cb); 551 | } 552 | } 553 | 554 | struct Http; 555 | impl Http { 556 | pub fn http_get_slow(url: &str, delay_ms: u32, cb: impl Fn(Js) + 'static + Clone) { 557 | let rt: &mut Runtime = unsafe { &mut *RUNTIME }; 558 | 559 | // Don't worry, http://slowwly.robertomurray.co.uk is a site for simulating a delayed 560 | // response from a server. Perfect for our use case. 561 | let adr = "slowwly.robertomurray.co.uk:80"; 562 | let mut stream = minimio::TcpStream::connect(adr).unwrap(); 563 | let request = format!( 564 | "GET /delay/{}/url/http://{} HTTP/1.1\r\n\ 565 | Host: slowwly.robertomurray.co.uk\r\n\ 566 | Connection: close\r\n\ 567 | \r\n", 568 | delay_ms, url 569 | ); 570 | 571 | stream 572 | .write_all(request.as_bytes()) 573 | .expect("Error writing to stream"); 574 | 575 | let token = rt.generate_cb_identity(); 576 | 577 | rt.epoll_registrator 578 | .register(&mut stream, token, minimio::Interests::READABLE) 579 | .unwrap(); 580 | 581 | let wrapped = move |_n| { 582 | let mut stream = stream; 583 | let mut buffer = String::new(); 584 | 585 | stream 586 | .read_to_string(&mut buffer) 587 | .expect("Stream read error"); 588 | 589 | cb(Js::String(buffer)); 590 | }; 591 | 592 | rt.register_event_epoll(token, wrapped); 593 | } 594 | } 595 | 596 | fn print(t: impl std::fmt::Display) { 597 | println!("Thread: {}\t {}", current(), t); 598 | } 599 | 600 | fn print_content(t: impl std::fmt::Display, descr: &str) { 601 | println!( 602 | "\n===== THREAD {} START CONTENT - {} =====", 603 | current(), 604 | descr.to_uppercase() 605 | ); 606 | 607 | let content = format!("{}", t); 608 | let lines = content.lines().take(2); 609 | let main_cont: String = lines.map(|l| format!("{}\n", l)).collect(); 610 | let opt_location = content.find("Location"); 611 | 612 | let opt_location = opt_location.map(|loc| { 613 | content[loc..] 614 | .lines() 615 | .nth(0) 616 | .map(|l| format!("{}\n",l)) 617 | .unwrap_or(String::new()) 618 | }); 619 | 620 | println!( 621 | "{}{}... [Note: Abbreviated for display] ...", 622 | main_cont, 623 | opt_location.unwrap_or(String::new()) 624 | ); 625 | 626 | println!("===== END CONTENT =====\n"); 627 | } 628 | 629 | fn current() -> String { 630 | thread::current().name().unwrap().to_string() 631 | } 632 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | Hello world! This is a text to encrypt! --------------------------------------------------------------------------------