├── .adr.json ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── core-lib ├── Cargo.toml └── src │ ├── app │ ├── application.rs │ └── mod.rs │ ├── domain │ ├── domain.rs │ ├── mod.rs │ └── notif.rs │ ├── infra │ ├── cleaner │ │ ├── docker.rs │ │ └── mod.rs │ ├── cpu.rs │ ├── disk.rs │ ├── host.rs │ ├── lang.rs │ ├── language │ │ ├── golang.rs │ │ ├── java.rs │ │ ├── java_utils.rs │ │ ├── lang_utils.rs │ │ ├── mod.rs │ │ ├── nodejs.rs │ │ ├── php.rs │ │ ├── python.rs │ │ └── ruby.rs │ ├── memory.rs │ ├── mod.rs │ └── process.rs │ └── lib.rs ├── docs ├── adr │ ├── 0001-clean-my-mac.md │ └── README.md └── stadal.png ├── gui ├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── assets │ ├── css │ │ ├── bootstrap.min.css │ │ └── index.css │ ├── images │ │ ├── cloudTemplate.png │ │ ├── cloudTemplate@2x.png │ │ ├── flagTemplate.png │ │ ├── flagTemplate@2x.png │ │ ├── moonTemplate.png │ │ ├── moonTemplate@2x.png │ │ ├── sunTemplate.png │ │ ├── sunTemplate@2x.png │ │ ├── umbrellaTemplate.png │ │ └── umbrellaTemplate@2x.png │ └── js │ │ ├── bootstrap.min.js │ │ ├── jquery-3.5.1.slim.min.js │ │ └── popper.min.js ├── package-lock.json ├── package.json ├── src │ ├── main.ts │ ├── preload.ts │ ├── render │ │ ├── actions.ts │ │ ├── core.ts │ │ ├── format.ts │ │ ├── renderer.ts │ │ ├── types │ │ │ └── core.ts │ │ └── view-proxy.ts │ └── utils │ │ ├── dom.ts │ │ ├── emitter.ts │ │ ├── environment.ts │ │ └── string-util.ts ├── tsconfig.json ├── tslint.json ├── views │ └── index.html └── yarn.lock ├── justfile ├── rpc ├── BUILD.gn ├── Cargo.toml ├── examples │ └── try_chan.rs ├── src │ ├── error.rs │ ├── lib.rs │ ├── parse.rs │ └── test_utils.rs └── tests │ └── integration.rs ├── src └── main.rs └── trace ├── Cargo.toml └── src ├── chrome_trace_dump.rs ├── fixed_lifo_deque.rs ├── lib.rs ├── sys_pid.rs └── sys_tid.rs /.adr.json: -------------------------------------------------------------------------------- 1 | {"language":"zh-cn","path":"docs/adr/","prefix":"","digits":4} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | stadal.log 4 | *.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: required 3 | 4 | rust: 5 | - stable 6 | node_js: 7 | - 12 8 | 9 | os: 10 | - linux 11 | - osx 12 | 13 | script: 14 | - cargo build 15 | - just tests-ci 16 | 17 | virtualenv: 18 | system_site_packages: true 19 | 20 | addons: 21 | apt: 22 | packages: 23 | - libcurl4-openssl-dev 24 | - libelf-dev 25 | - libdw-dev 26 | - cmake 27 | - gcc 28 | - binutils-dev 29 | - libiberty-dev 30 | 31 | before_install: 32 | # adding $HOME/.sdkman to cache would create an empty directory, which interferes with the initial installation 33 | - "[[ -d /home/travis/.sdkman/ ]] && [[ -d /home/travis/.sdkman/bin/ ]] || rm -rf /home/travis/.sdkman/" 34 | - curl -sL https://get.sdkman.io | bash 35 | - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config 36 | - echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config 37 | - source "$HOME/.sdkman/bin/sdkman-init.sh" 38 | 39 | notifications: 40 | email: false 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stadal" 3 | version = "0.1.0" 4 | authors = ["Phodal Huang "] 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 | heim = "0.0.11" 11 | tokio = { version = "~0.2", features = ["full"] } 12 | 13 | serde = { version = "1.0", features = ["rc"] } 14 | serde_json = "1.0" 15 | serde_derive = "1.0" 16 | futures = "0.3" 17 | dirs = "2.0" 18 | chrono = "0.4.5" 19 | 20 | fern = "0.6" 21 | log = "0.4.3" 22 | 23 | 24 | [dependencies.core-lib] 25 | path = "core-lib" 26 | 27 | [dependencies.xi-rpc] 28 | path = "rpc" 29 | 30 | [dependencies.xi-trace] 31 | path = "trace" 32 | 33 | [workspace] 34 | members = [ 35 | "rpc", 36 | "trace", 37 | "core-lib" 38 | ] 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stadal 2 | 3 | > A RPC-based client-server system status tools, with Rust + Electron architecture. 4 | 5 | ![Stadal Screenshots](docs/stadal.png) 6 | 7 | ## Architecture (design) 8 | 9 | - RPC Server, (Using Rust) for read system status. 10 | - ~~RPC Client, (Using Rust to Node.js) for communication~~. 11 | - UI Client, (Using Electron) for cross platform UI support. 12 | 13 | Node.js 14 | 15 | - [neon](https://github.com/neon-bindings/neon) Rust bindings for writing safe and fast native Node.js modules. 16 | 17 | RPC 18 | 19 | - [tarpc](https://github.com/google/tarpc) is an RPC framework for rust with a focus on ease of use. Defining a service can be done in just a few lines of code, and most of the boilerplate of writing a server is taken care of for you. 20 | - [gRPC-rs](https://github.com/tikv/grpc-rs) is a Rust wrapper of gRPC Core. gRPC is a high performance, open source universal RPC framework that puts mobile and HTTP/2 first. 21 | 22 | Refs: 23 | 24 | - [Xi Editor](https://github.com/xi-editor/xi-editor) project is an attempt to build a high quality text editor, using modern software engineering techniques. 25 | - [Tray Electrorn](https://github.com/kevinsawicki/tray-example) An example app for building a native-looking Mac OS X tray app with a popover using Electron. 26 | 27 | Stats Demo App: 28 | 29 | - [bottom](https://github.com/ClementTsang/bottom) A cross-platform graphical process/system monitor with a customizable interface and a multitude of features. Supports Linux, macOS, and Windows. Inspired by both gtop and gotop. 30 | - [Zenith](https://github.com/bvaisvil/zenith) In terminal graphical metrics for your *nix system written in Rust. 31 | 32 | Dev Setup: 33 | 34 | - Languages: [https://github.com/starship/starship](https://github.com/starship/starship) 35 | 36 | System 37 | 38 | - [bandwhich](https://github.com/imsnif/bandwhich) is a CLI utility for displaying current network utilization by process, connection and remote IP/hostname. 39 | - [https://github.com/tbillington/kondo](https://github.com/tbillington/kondo) - Save disk space by cleaning non-essential files from software projects. 40 | 41 | ## Documents 42 | 43 | Library: 44 | 45 | - [heim](https://github.com/heim-rs/heim) is an ongoing attempt to create the best tool for system information fetching (ex., CPU, memory, disks or processes stats) in the Rust crates ecosystem. 46 | 47 | [Status library comparison](https://github.com/heim-rs/heim/blob/master/COMPARISON.md) 48 | 49 | ## Notes 50 | 51 | requests 52 | 53 | ``` 54 | {"method":"client_started","params":{}} 55 | ``` 56 | 57 | ``` 58 | {"method":"client_started","params":{}} 59 | {"method":"send_memory","params":{}} 60 | ``` 61 | 62 | ``` 63 | {"method":"client_started","params":{}} 64 | {"method":"send_host","params":{}} 65 | {"method":"send_memory","params":{}} 66 | {"method":"send_languages","params":{}} 67 | {"method":"send_sizes","params":{}} 68 | ``` 69 | 70 | ## Development 71 | 72 | - core-lib, Stadal Core 73 | - gui, Electron UI 74 | - rpc, Stadal Core RPC 75 | - src, Stadal App 76 | - client, **deprecated** for Rust RPC Client 77 | - stadaljs, **deprecated** for Rust PRC Client Node.js Binding 78 | - xrl, **deprecated** for Rust RPC Client 79 | - trace, for Rust RPC Server 80 | 81 | LICENSE 82 | === 83 | 84 | RPC based on [xi-editor](https://github.com/xi-editor/xi-editor) with Apache 2.0 & Inspired by [xi-term](https://github.com/xi-frontend/xi-term) 85 | -------------------------------------------------------------------------------- /core-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "core-lib" 3 | version = "0.1.0" 4 | authors = ["Phodal Huang "] 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 | log = "0.4.3" 11 | serde = { version = "1.0", features = ["rc"] } 12 | serde_json = "1.0" 13 | serde_derive = "1.0" 14 | futures = "0.3" 15 | futures-timer = "3.0.2" 16 | 17 | xi-trace = { path = "../trace", version = "0.2.0" } 18 | xi-rpc = { path = "../rpc", version = "0.3.0" } 19 | 20 | heim = "0.0.10" 21 | nom = "5.1.0" 22 | -------------------------------------------------------------------------------- /core-lib/src/app/application.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex, MutexGuard, Weak}; 2 | 3 | use log::{info}; 4 | use serde_json::{self, Value}; 5 | 6 | use xi_rpc::{Handler, RemoteError, RpcCtx, RpcPeer}; 7 | 8 | use crate::domain::notif::CoreNotification; 9 | use crate::domain::notif::CoreNotification::{ClientStarted, TracingConfig}; 10 | use futures::executor; 11 | use crate::infra::memory::get_memory; 12 | use crate::infra::{get_host, get_languages, get_clean_size, get_cpu, get_disks, get_sort_processes, get_processes}; 13 | 14 | pub struct Client(RpcPeer); 15 | 16 | impl Client { 17 | pub fn new(peer: RpcPeer) -> Self { 18 | Client(peer) 19 | } 20 | 21 | pub fn send_host(&self) { 22 | let host = executor::block_on(get_host()); 23 | self.0.send_rpc_notification( 24 | "send_host", 25 | &json!({ 26 | "name": &host.name, 27 | "release": &host.release, 28 | "version": &host.version, 29 | "hostname": &host.hostname, 30 | "arch": &host.arch, 31 | "uptime": &host.uptime 32 | }), 33 | ); 34 | } 35 | 36 | pub fn send_languages(&self) { 37 | let langs = get_languages(); 38 | self.0.send_rpc_notification( 39 | "send_languages", 40 | &json!(&langs), 41 | ); 42 | } 43 | 44 | pub fn send_sizes(&self) { 45 | let sizes = get_clean_size(); 46 | self.0.send_rpc_notification( 47 | "send_sizes", 48 | &json!(&sizes), 49 | ); 50 | } 51 | 52 | pub fn send_memory(&self) { 53 | let memory = executor::block_on(get_memory()); 54 | self.0.send_rpc_notification( 55 | "send_memory", 56 | &json!(&memory), 57 | ); 58 | } 59 | 60 | pub fn send_cpu(&self) { 61 | let cpu = executor::block_on(get_cpu()); 62 | self.0.send_rpc_notification( 63 | "send_cpu", 64 | &json!(&cpu), 65 | ); 66 | } 67 | 68 | pub fn send_disks(&self) { 69 | let disks = executor::block_on(get_disks()); 70 | self.0.send_rpc_notification( 71 | "send_disks", 72 | &json!(&disks), 73 | ); 74 | } 75 | 76 | pub fn send_processes(&self) { 77 | let processes = get_sort_processes()[0..10].to_vec(); 78 | self.0.send_rpc_notification( 79 | "send_processes", 80 | &json!(&processes), 81 | ); 82 | } 83 | } 84 | 85 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 86 | #[serde(rename_all = "snake_case")] 87 | #[serde(tag = "method", content = "params")] 88 | pub enum CoreRequest { 89 | GetConfig {}, 90 | } 91 | 92 | #[allow(dead_code)] 93 | pub struct CoreState { 94 | peer: Client, 95 | } 96 | 97 | impl CoreState { 98 | pub(crate) fn new(peer: &RpcPeer) -> Self { 99 | CoreState { 100 | peer: Client::new(peer.clone()), 101 | } 102 | } 103 | 104 | pub(crate) fn client_notification(&mut self, cmd: CoreNotification) { 105 | use self::CoreNotification::*; 106 | match cmd { 107 | SendHost {} => { 108 | self.peer.send_host(); 109 | } 110 | SendMemory {} => { 111 | self.peer.send_memory(); 112 | } 113 | SendLanguages {} => { 114 | self.peer.send_languages(); 115 | } 116 | SendSizes {} => { 117 | self.peer.send_sizes(); 118 | } 119 | SendDisks {} => { 120 | self.peer.send_disks(); 121 | } 122 | SendCpu {} => { 123 | self.peer.send_cpu(); 124 | } 125 | SendProcesses {} => { 126 | self.peer.send_processes(); 127 | } 128 | ClientStarted { .. } => (), 129 | _ => { 130 | // self.not_command(view_id, language_id); 131 | } 132 | } 133 | } 134 | 135 | pub(crate) fn client_request(&mut self, cmd: CoreRequest) -> Result { 136 | use self::CoreRequest::*; 137 | match cmd { 138 | GetConfig {} => Ok(json!(1)), 139 | } 140 | } 141 | 142 | pub(crate) fn finish_setup(&mut self, self_ref: WeakStadalCore) { 143 | self.peer.0.send_rpc_notification("config_status", &json!({ "success": true })) 144 | } 145 | 146 | pub(crate) fn handle_idle(&mut self, token: usize) { 147 | match token { 148 | _ => { 149 | info!("token: {}", token); 150 | } 151 | } 152 | } 153 | } 154 | 155 | pub enum Stadal { 156 | // TODO: profile startup, and determine what things (such as theme loading) 157 | // we should be doing before client_init. 158 | Waiting, 159 | Running(Arc>), 160 | } 161 | 162 | /// A weak reference to the main state. This is passed to plugin threads. 163 | #[derive(Clone)] 164 | pub struct WeakStadalCore(Weak>); 165 | 166 | #[allow(dead_code)] 167 | impl Stadal { 168 | pub fn new() -> Self { 169 | Stadal::Waiting 170 | } 171 | 172 | /// Returns `true` if the `client_started` has not been received. 173 | fn is_waiting(&self) -> bool { 174 | match *self { 175 | Stadal::Waiting => true, 176 | _ => false, 177 | } 178 | } 179 | 180 | /// Returns a guard to the core state. A convenience around `Mutex::lock`. 181 | /// 182 | /// # Panics 183 | /// 184 | /// Panics if core has not yet received the `client_started` message. 185 | pub fn inner(&self) -> MutexGuard { 186 | match self { 187 | Stadal::Running(ref inner) => inner.lock().unwrap(), 188 | Stadal::Waiting => panic!( 189 | "core does not start until client_started \ 190 | RPC is received" 191 | ), 192 | } 193 | } 194 | 195 | /// Returns a new reference to the core state, if core is running. 196 | fn weak_self(&self) -> Option { 197 | match self { 198 | Stadal::Running(ref inner) => Some(WeakStadalCore(Arc::downgrade(inner))), 199 | Stadal::Waiting => None, 200 | } 201 | } 202 | } 203 | 204 | impl Handler for Stadal { 205 | type Notification = CoreNotification; 206 | type Request = CoreRequest; 207 | 208 | fn handle_notification(&mut self, ctx: &RpcCtx, rpc: Self::Notification) { 209 | // We allow tracing to be enabled before event `client_started` 210 | if let TracingConfig { enabled } = rpc { 211 | match enabled { 212 | true => xi_trace::enable_tracing(), 213 | false => xi_trace::disable_tracing(), 214 | } 215 | info!("tracing in core = {:?}", enabled); 216 | if self.is_waiting() { 217 | return; 218 | } 219 | } 220 | 221 | if let ClientStarted { 222 | ref config_dir, 223 | ref client_extras_dir, 224 | } = rpc 225 | { 226 | assert!(self.is_waiting(), "client_started can only be sent once"); 227 | let state = CoreState::new(ctx.get_peer()); 228 | let state = Arc::new(Mutex::new(state)); 229 | *self = Stadal::Running(state); 230 | let weak_self = self.weak_self().unwrap(); 231 | self.inner().finish_setup(weak_self); 232 | } 233 | 234 | self.inner().client_notification(rpc); 235 | } 236 | 237 | fn handle_request(&mut self, ctx: &RpcCtx, rpc: Self::Request) -> Result { 238 | self.inner().client_request(rpc) 239 | } 240 | 241 | fn idle(&mut self, _ctx: &RpcCtx, token: usize) { 242 | self.inner().handle_idle(token); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /core-lib/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | mod application; 2 | 3 | pub use crate::app::application::Stadal; 4 | -------------------------------------------------------------------------------- /core-lib/src/domain/domain.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /core-lib/src/domain/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod domain; 2 | pub mod notif; 3 | -------------------------------------------------------------------------------- /core-lib/src/domain/notif.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use core::fmt; 3 | 4 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 5 | #[serde(rename_all = "snake_case")] 6 | #[serde(tag = "method", content = "params")] 7 | pub enum CoreNotification { 8 | TracingConfig { 9 | enabled: bool, 10 | }, 11 | 12 | SendHost {}, 13 | SendMemory {}, 14 | SendLanguages {}, 15 | SendSizes {}, 16 | SendDisks {}, 17 | SendCpu {}, 18 | SendProcesses {}, 19 | ClientStarted { 20 | #[serde(default)] 21 | config_dir: Option, 22 | /// Path to additional plugins, included by the client. 23 | #[serde(default)] 24 | client_extras_dir: Option, 25 | }, 26 | } 27 | 28 | impl fmt::Display for CoreNotification { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 30 | write!(f, "{:?}", self) 31 | } 32 | } -------------------------------------------------------------------------------- /core-lib/src/infra/cleaner/docker.rs: -------------------------------------------------------------------------------- 1 | use std::{fs}; 2 | use crate::infra::CleanSize; 3 | 4 | pub fn get_docker_env() -> CleanSize { 5 | let home = std::env::var("HOME").unwrap(); 6 | let path = format!("{}/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw", home); 7 | match fs::metadata(path.clone()) { 8 | Ok(size) => { 9 | CleanSize::new( 10 | String::from("docker"), 11 | size.len().to_string(), 12 | path, 13 | ) 14 | } 15 | Err(_) => { 16 | CleanSize::new( 17 | String::from(""), 18 | String::from(""), 19 | String::from("") 20 | ) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core-lib/src/infra/cleaner/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::cleaner::docker::{get_docker_env}; 2 | 3 | mod docker; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone)] 6 | pub struct CleanSize { 7 | name: String, 8 | size: String, 9 | path: String, 10 | } 11 | 12 | impl CleanSize { 13 | fn new(name: String, size: String, path: String) -> CleanSize { 14 | CleanSize { 15 | name, 16 | size, 17 | path 18 | } 19 | } 20 | } 21 | 22 | pub fn get_clean_size() -> Vec { 23 | let mut sizes = Vec::with_capacity(1); 24 | sizes.push(get_docker_env()); 25 | 26 | sizes 27 | } -------------------------------------------------------------------------------- /core-lib/src/infra/cpu.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use heim::units::{frequency}; 3 | 4 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 5 | struct StadalCpu { 6 | cores: String, 7 | current_ghz: String, 8 | min_ghz: String, 9 | max_ghz: String, 10 | } 11 | 12 | impl StadalCpu { 13 | pub fn new() -> StadalCpu { 14 | StadalCpu { 15 | cores: "".to_string(), 16 | current_ghz: "".to_string(), 17 | min_ghz: "".to_string(), 18 | max_ghz: "".to_string(), 19 | } 20 | } 21 | } 22 | 23 | 24 | // based on github.com/nushell/nushell/crates/nu_plugin_sys/src/nu/mod.rs 25 | // MIT License 26 | // 27 | // Copyright (c) 2019 - 2020 Yehuda Katz, Jonathan Turner 28 | pub async fn get_cpu() -> Option { 29 | match futures::future::try_join(heim::cpu::logical_count(), heim::cpu::frequency()).await { 30 | Ok((num_cpu, cpu_speed)) => { 31 | let mut cpu = StadalCpu::new(); 32 | cpu.cores = num_cpu.to_string(); 33 | 34 | let current_speed = 35 | (cpu_speed.current().get::() as f64 / 1_000_000_000.0 * 100.0) 36 | .round() 37 | / 100.0; 38 | cpu.current_ghz = current_speed.to_string(); 39 | 40 | if let Some(min_speed) = cpu_speed.min() { 41 | let min_speed = 42 | (min_speed.get::() as f64 / 1_000_000_000.0 * 100.0).round() 43 | / 100.0; 44 | 45 | cpu.min_ghz = min_speed.to_string(); 46 | 47 | } 48 | 49 | if let Some(max_speed) = cpu_speed.max() { 50 | let max_speed = 51 | (max_speed.get::() as f64 / 1_000_000_000.0 * 100.0).round() 52 | / 100.0; 53 | 54 | cpu.max_ghz = max_speed.to_string(); 55 | } 56 | 57 | Some(json!(cpu)) 58 | } 59 | Err(_) => None, 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use crate::infra::cpu::get_cpu; 66 | use serde_json::Value; 67 | use futures::executor::block_on; 68 | 69 | #[test] 70 | fn should_get_cpu_info() { 71 | let value = block_on(get_cpu_info()); 72 | println!("{}, ", json!(value)); 73 | } 74 | 75 | async fn get_cpu_info() -> Option { 76 | get_cpu().await 77 | } 78 | } -------------------------------------------------------------------------------- /core-lib/src/infra/disk.rs: -------------------------------------------------------------------------------- 1 | use heim::disk; 2 | use heim::units::{information}; 3 | use std::ffi::OsStr; 4 | use futures::{StreamExt}; 5 | 6 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 7 | pub struct StadalDisk { 8 | device: String, 9 | filesystem: String, 10 | mount: String, 11 | total: String, 12 | used: String, 13 | free: String, 14 | } 15 | 16 | impl StadalDisk { 17 | pub fn new() -> StadalDisk { 18 | StadalDisk { 19 | device: "".to_string(), 20 | filesystem: "".to_string(), 21 | mount: "".to_string(), 22 | total: "".to_string(), 23 | used: "".to_string(), 24 | free: "".to_string(), 25 | } 26 | } 27 | } 28 | 29 | pub async fn get_disks() -> Option> { 30 | let mut output = vec![]; 31 | 32 | let mut partitions = heim::disk::partitions_physical(); 33 | while let Some(part) = partitions.next().await { 34 | let part = part.unwrap(); 35 | let mut sdisk = StadalDisk::new(); 36 | sdisk.device = part.device().unwrap_or_else(|| OsStr::new("N/A")).to_string_lossy().to_string(); 37 | sdisk.filesystem = part.file_system().as_str().to_string(); 38 | sdisk.mount = part.mount_point().to_string_lossy().to_string(); 39 | if let Ok(usage) = disk::usage(part.mount_point().to_path_buf()).await { 40 | sdisk.total = usage.total().get::().to_string(); 41 | sdisk.used = usage.used().get::().to_string(); 42 | sdisk.free = usage.free().get::().to_string(); 43 | } 44 | 45 | output.push(sdisk); 46 | } 47 | 48 | if !output.is_empty() { 49 | Some(output) 50 | } else { 51 | None 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use crate::infra::disk::get_disks; 58 | use futures::executor::block_on; 59 | 60 | #[test] 61 | fn get_disks_sizes() { 62 | let disks = block_on(get_disks()).unwrap(); 63 | println!("{},", json!(&disks)); 64 | } 65 | } -------------------------------------------------------------------------------- /core-lib/src/infra/host.rs: -------------------------------------------------------------------------------- 1 | use heim::host; 2 | use heim::units::{time}; 3 | 4 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 5 | pub struct StadalHost { 6 | pub name: String, 7 | pub release: String, 8 | pub version: String, 9 | pub hostname: String, 10 | pub arch: String, 11 | pub uptime: String, 12 | } 13 | 14 | pub async fn get_host() -> StadalHost { 15 | let mut result = StadalHost { 16 | name: "".to_string(), 17 | release: "".to_string(), 18 | version: "".to_string(), 19 | hostname: "".to_string(), 20 | arch: "".to_string(), 21 | uptime: "".to_string() 22 | }; 23 | 24 | let (platform_result, uptime_result) = 25 | futures::future::join(host::platform(), host::uptime()).await; 26 | 27 | if let Ok(platform) = platform_result { 28 | result.name = platform.system().to_string(); 29 | result.release = platform.release().to_string(); 30 | result.version = platform.version().to_string(); 31 | result.hostname = platform.hostname().to_string(); 32 | result.arch = platform.architecture().as_str().to_string(); 33 | } 34 | 35 | if let Ok(uptime) = uptime_result { 36 | let uptime = uptime.get::().round() as i64; 37 | result.uptime = uptime.to_string(); 38 | } 39 | 40 | result 41 | } -------------------------------------------------------------------------------- /core-lib/src/infra/lang.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct Lang { 5 | name: String, 6 | version: String, 7 | } 8 | 9 | impl Lang { 10 | fn new(name: String, version: String) -> Lang { 11 | Lang { 12 | name, 13 | version 14 | } 15 | } 16 | } 17 | 18 | pub fn get_languages() -> Vec { 19 | let mut languages = Vec::with_capacity(6); 20 | 21 | let java_version = language::get_java_version().unwrap(); 22 | let php_version = language::get_php_version().unwrap(); 23 | let ruby_version = language::get_ruby_version().unwrap(); 24 | let nodejs_version = language::get_nodejs_version().unwrap(); 25 | let python_version = language::get_python_version().unwrap(); 26 | let golang_version = language::get_golang_version().unwrap(); 27 | 28 | languages.push(Lang::new(String::from("java"), java_version)); 29 | languages.push(Lang::new(String::from("php"), php_version)); 30 | languages.push(Lang::new(String::from("ruby"), ruby_version)); 31 | languages.push(Lang::new(String::from("nodejs"), nodejs_version)); 32 | languages.push(Lang::new(String::from("python"), python_version)); 33 | languages.push(Lang::new(String::from("golang"), golang_version)); 34 | 35 | languages 36 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/golang.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::lang_utils; 2 | 3 | pub fn get_golang_version() -> Option { 4 | let output = lang_utils::exec_cmd("go", &["version"]).unwrap(); 5 | let formatted_version = format_go_version(&output.stdout.as_str()); 6 | formatted_version 7 | } 8 | 9 | fn format_go_version(go_stdout: &str) -> Option { 10 | let version = go_stdout 11 | .splitn(2, "go version go") 12 | .nth(1)? 13 | .split_whitespace() 14 | .next()?; 15 | 16 | Some(format!("v{}", version)) 17 | } 18 | -------------------------------------------------------------------------------- /core-lib/src/infra/language/java.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::{java_utils, lang_utils}; 2 | 3 | pub fn get_java_version() -> Option { 4 | match run_get_java_version() { 5 | Some(java_version) => { 6 | let formatted_version = format_java_version(java_version)?; 7 | Some(formatted_version) 8 | } 9 | None => None, 10 | } 11 | } 12 | 13 | fn run_get_java_version() -> Option { 14 | let java_command = match std::env::var("JAVA_HOME") { 15 | Ok(java_home) => format!("{}/bin/java", java_home), 16 | Err(_) => String::from("java"), 17 | }; 18 | 19 | let output = lang_utils::exec_cmd(&java_command.as_str(), &["-Xinternalversion"])?; 20 | Some(format!("{}{}", output.stdout, output.stderr)) 21 | } 22 | 23 | /// Extract the java version from `java_out`. 24 | fn format_java_version(java_out: String) -> Option { 25 | java_utils::parse_jre_version(&java_out).map(|result| format!("v{}", result)) 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | #[test] 33 | fn test_format_java_version_openjdk() { 34 | let java_8 = String::from("OpenJDK 64-Bit Server VM (25.222-b10) for linux-amd64 JRE (1.8.0_222-b10), built on Jul 11 2019 10:18:43 by \"openjdk\" with gcc 4.4.7 20120313 (Red Hat 4.4.7-23)"); 35 | let java_11 = String::from("OpenJDK 64-Bit Server VM (11.0.4+11-post-Ubuntu-1ubuntu219.04) for linux-amd64 JRE (11.0.4+11-post-Ubuntu-1ubuntu219.04), built on Jul 18 2019 18:21:46 by \"build\" with gcc 8.3.0"); 36 | assert_eq!(format_java_version(java_11), Some(String::from("v11.0.4"))); 37 | assert_eq!(format_java_version(java_8), Some(String::from("v1.8.0"))); 38 | } 39 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/java_utils.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | branch::alt, 3 | bytes::complete::{tag, take_until, take_while1}, 4 | combinator::rest, 5 | sequence::{preceded, tuple}, 6 | IResult, 7 | }; 8 | 9 | fn is_version(c: char) -> bool { 10 | c >= '0' && c <= '9' || c == '.' 11 | } 12 | 13 | fn version(input: &str) -> IResult<&str, &str> { 14 | take_while1(&is_version)(input) 15 | } 16 | 17 | fn zulu(input: &str) -> IResult<&str, &str> { 18 | let zulu_prefix_value = preceded(take_until("("), tag("(")); 19 | preceded(zulu_prefix_value, version)(input) 20 | } 21 | 22 | fn jre_prefix(input: &str) -> IResult<&str, &str> { 23 | preceded(take_until("JRE ("), tag("JRE ("))(input) 24 | } 25 | 26 | fn j9_prefix(input: &str) -> IResult<&str, &str> { 27 | preceded(take_until("VM ("), tag("VM ("))(input) 28 | } 29 | 30 | fn suffix(input: &str) -> IResult<&str, &str> { 31 | rest(input) 32 | } 33 | 34 | fn parse(input: &str) -> IResult<&str, &str> { 35 | let prefix = alt((jre_prefix, j9_prefix)); 36 | let version_or_zulu = alt((version, zulu)); 37 | let (input, (_, version, _)) = tuple((prefix, version_or_zulu, suffix))(input)?; 38 | 39 | Ok((input, version)) 40 | } 41 | 42 | /// Parse the java version from `java -Xinternalversion` format. 43 | /// 44 | /// The expected format is similar to: 45 | /// "JRE (1.8.0_222-b10)" 46 | /// "JRE (Zulu 8.40.0.25-CA-linux64) (1.8.0_222-b10)" 47 | /// "VM (1.8.0_222-b10)". 48 | /// 49 | /// Some Java vendors might not follow this format. 50 | pub fn parse_jre_version(input: &str) -> Option<&str> { 51 | parse(input).map(|result| result.1).ok() 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | 58 | #[test] 59 | fn test_parse_eclipse_openj9() { 60 | let java_8 = "Eclipse OpenJ9 OpenJDK 64-bit Server VM (1.8.0_222-b10) from linux-amd64 JRE with Extensions for OpenJDK for Eclipse OpenJ9 8.0.222.0, built on Jul 17 2019 21:29:18 by jenkins with g++ (GCC) 7.3.1 20180303 (Red Hat 7.3.1-5)"; 61 | let java_11 = "Eclipse OpenJ9 OpenJDK 64-bit Server VM (11.0.4+11) from linux-amd64 JRE with Extensions for OpenJDK for Eclipse OpenJ9 11.0.4.0, built on Jul 17 2019 21:51:37 by jenkins with g++ (GCC) 7.3.1 20180303 (Red Hat 7.3.1-5)"; 62 | assert_eq!(parse(java_8), Ok(("", "1.8.0"))); 63 | assert_eq!(parse(java_11), Ok(("", "11.0.4"))); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core-lib/src/infra/language/lang_utils.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | #[derive(Debug)] 4 | pub struct CommandOutput { 5 | pub stdout: String, 6 | pub stderr: String, 7 | } 8 | 9 | impl PartialEq for CommandOutput { 10 | fn eq(&self, other: &Self) -> bool { 11 | self.stdout == other.stdout && self.stderr == other.stderr 12 | } 13 | } 14 | 15 | pub fn exec_cmd(cmd: &str, args: &[&str]) -> Option { 16 | internal_exec_cmd(&cmd, &args) 17 | } 18 | 19 | fn internal_exec_cmd(cmd: &str, args: &[&str]) -> Option { 20 | log::trace!("Executing command '{:?}' with args '{:?}'", cmd, args); 21 | match Command::new(cmd).args(args).output() { 22 | Ok(output) => { 23 | let stdout_string = String::from_utf8(output.stdout).unwrap(); 24 | let stderr_string = String::from_utf8(output.stderr).unwrap(); 25 | 26 | if !output.status.success() { 27 | log::trace!("Non-zero exit code '{:?}'", output.status.code()); 28 | log::trace!("stdout: {}", stdout_string); 29 | log::trace!("stderr: {}", stderr_string); 30 | return None; 31 | } 32 | 33 | Some(CommandOutput { 34 | stdout: stdout_string, 35 | stderr: stderr_string, 36 | }) 37 | } 38 | Err(_) => None, 39 | } 40 | } 41 | 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | 47 | #[test] 48 | fn exec_with_output_stdout() { 49 | let result = internal_exec_cmd("/bin/echo", &["-n", "hello"]); 50 | let expected = Some(CommandOutput { 51 | stdout: String::from("hello"), 52 | stderr: String::from(""), 53 | }); 54 | 55 | assert_eq!(result, expected) 56 | } 57 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/mod.rs: -------------------------------------------------------------------------------- 1 | mod php; 2 | pub use self::php::get_php_version; 3 | 4 | mod ruby; 5 | pub use self::ruby::get_ruby_version; 6 | 7 | mod java; 8 | pub use self::java::get_java_version; 9 | 10 | mod nodejs; 11 | pub use self::nodejs::get_nodejs_version; 12 | 13 | mod python; 14 | pub use self::python::get_python_version; 15 | 16 | mod golang; 17 | pub use self::golang::get_golang_version; 18 | 19 | mod lang_utils; 20 | pub use self::lang_utils::{exec_cmd, CommandOutput}; 21 | 22 | mod java_utils; 23 | pub use self::java_utils::parse_jre_version; 24 | 25 | -------------------------------------------------------------------------------- /core-lib/src/infra/language/nodejs.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::lang_utils; 2 | 3 | pub fn get_nodejs_version() -> Option { 4 | let node_version = lang_utils::exec_cmd("node", &["--version"]).unwrap().stdout; 5 | let formatted_version = node_version.trim(); 6 | 7 | Some(String::from(formatted_version)) 8 | } 9 | 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use super::*; 14 | 15 | #[test] 16 | fn exec_with_output_stdout() { 17 | let string = get_nodejs_version(); 18 | } 19 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/php.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::lang_utils; 2 | 3 | pub fn get_php_version() -> Option { 4 | match lang_utils::exec_cmd( 5 | "php", 6 | &[ 7 | "-r", 8 | "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION;", 9 | ], 10 | ) { 11 | Some(php_cmd_output) => { 12 | let php_version = php_cmd_output.stdout; 13 | let formatted_version = format_php_version(&php_version)?; 14 | 15 | Some(formatted_version) 16 | } 17 | None => None, 18 | } 19 | } 20 | 21 | fn format_php_version(php_version: &str) -> Option { 22 | let mut formatted_version = String::with_capacity(php_version.len() + 1); 23 | formatted_version.push('v'); 24 | formatted_version.push_str(php_version); 25 | Some(formatted_version) 26 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/python.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::lang_utils; 2 | 3 | pub fn get_python_version() -> Option { 4 | let python_version = run_get_python_version()?; 5 | let formatted_version = format_python_version(&python_version); 6 | 7 | Some(formatted_version) 8 | } 9 | 10 | fn run_get_python_version() -> Option { 11 | let exec_python = lang_utils::exec_cmd("python", &["--version"]); 12 | match exec_python { 13 | Some(output) => { 14 | if output.stdout.is_empty() { 15 | Some(output.stderr) 16 | } else { 17 | Some(output.stdout) 18 | } 19 | } 20 | None => None, 21 | } 22 | } 23 | 24 | fn format_python_version(python_stdout: &str) -> String { 25 | format!( 26 | "v{}", 27 | python_stdout 28 | .trim_start_matches("Python ") 29 | .trim_end_matches(":: Anaconda, Inc.") 30 | .trim() 31 | ) 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | #[test] 39 | fn exec_with_output_stdout() { 40 | let _str = get_python_version(); 41 | } 42 | } -------------------------------------------------------------------------------- /core-lib/src/infra/language/ruby.rs: -------------------------------------------------------------------------------- 1 | use crate::infra::language::lang_utils; 2 | 3 | pub fn get_ruby_version() -> Option { 4 | let ruby_version = lang_utils::exec_cmd("ruby", &["-v"])?.stdout; 5 | let formatted_version = format_ruby_version(&ruby_version)?; 6 | Some(formatted_version) 7 | } 8 | 9 | fn format_ruby_version(ruby_version: &str) -> Option { 10 | let version = ruby_version 11 | // split into ["ruby", "2.6.0p0", "linux/amd64"] 12 | .split_whitespace() 13 | // return "2.6.0p0" 14 | .nth(1)? 15 | .get(0..5)?; 16 | 17 | let mut formatted_version = String::with_capacity(version.len() + 1); 18 | formatted_version.push('v'); 19 | formatted_version.push_str(version); 20 | Some(formatted_version) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /core-lib/src/infra/memory.rs: -------------------------------------------------------------------------------- 1 | use heim::{memory, units::information}; 2 | 3 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 4 | pub struct StadalMemory { 5 | pub total: String, 6 | pub free: String, 7 | pub available: String, 8 | pub swap_total: String, 9 | pub swap_free: String, 10 | pub swap_used: String, 11 | } 12 | 13 | impl StadalMemory { 14 | pub fn new() -> StadalMemory { 15 | StadalMemory { 16 | total: "".to_string(), 17 | free: "".to_string(), 18 | available: "".to_string(), 19 | swap_total: "".to_string(), 20 | swap_free: "".to_string(), 21 | swap_used: "".to_string() 22 | } 23 | } 24 | } 25 | 26 | pub async fn get_memory() -> StadalMemory { 27 | let mut stadal_memory = StadalMemory::new(); 28 | let (memory_result, swap_result) = 29 | futures::future::join(memory::memory(), memory::swap()).await; 30 | 31 | if let Ok(memory) = memory_result { 32 | stadal_memory.total = memory.total().get::().to_string(); 33 | stadal_memory.free = memory.free().get::().to_string(); 34 | stadal_memory.available = memory.available().get::().to_string(); 35 | } 36 | 37 | if let Ok(swap) = swap_result { 38 | stadal_memory.swap_total = swap.total().get::().to_string(); 39 | stadal_memory.swap_free = swap.free().get::().to_string(); 40 | stadal_memory.swap_used = swap.used().get::().to_string(); 41 | } 42 | 43 | stadal_memory 44 | } 45 | 46 | -------------------------------------------------------------------------------- /core-lib/src/infra/mod.rs: -------------------------------------------------------------------------------- 1 | mod process; 2 | pub use process::{get_processes, get_sort_processes}; 3 | 4 | mod disk; 5 | pub use disk::get_disks; 6 | 7 | mod cpu; 8 | pub use cpu::get_cpu; 9 | 10 | mod cleaner; 11 | pub use cleaner::{get_clean_size, CleanSize}; 12 | 13 | mod lang; 14 | pub use lang::{get_languages, Lang}; 15 | 16 | pub mod language; 17 | 18 | mod host; 19 | pub use host::{StadalHost, get_host}; 20 | 21 | pub mod memory; 22 | pub use memory::{StadalMemory, get_memory}; 23 | 24 | -------------------------------------------------------------------------------- /core-lib/src/infra/process.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::usize; 3 | 4 | use futures::{StreamExt, TryStreamExt}; 5 | use heim::process::{self as process, Process, ProcessResult}; 6 | use heim::units::{information, ratio, Ratio}; 7 | use futures::executor::block_on; 8 | use std::collections::HashMap; 9 | use std::cmp::Ordering::Equal; 10 | use std::cmp::Ordering; 11 | 12 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 13 | pub struct StadalProcess { 14 | pid: i32, 15 | name: String, 16 | status: String, 17 | cpu_usage: f32, 18 | mem: u64, 19 | virtual_mem: u64, 20 | parent: String, 21 | exe: String, 22 | command: String, 23 | } 24 | 25 | impl StadalProcess { 26 | pub fn new() -> StadalProcess { 27 | StadalProcess { 28 | pid: 0, 29 | name: "".to_string(), 30 | status: "".to_string(), 31 | cpu_usage: 0.0, 32 | mem: 0, 33 | virtual_mem: 0, 34 | parent: "".to_string(), 35 | exe: "".to_string(), 36 | command: "".to_string(), 37 | } 38 | } 39 | } 40 | 41 | async fn usage(process: Process) -> ProcessResult<(process::Process, Ratio, process::Memory)> { 42 | let usage_1 = process.cpu_usage().await?; 43 | futures_timer::Delay::new(Duration::from_millis(100)).await; 44 | let usage_2 = process.cpu_usage().await?; 45 | 46 | let memory = process.memory().await?; 47 | 48 | Ok((process, usage_2 - usage_1, memory)) 49 | } 50 | 51 | pub async fn get_processes() -> Option> { 52 | let mut output = vec![]; 53 | let mut results = process::processes() 54 | .map_ok(|process| { 55 | usage(process) 56 | }) 57 | .try_buffer_unordered(usize::MAX); 58 | futures::pin_mut!(results); 59 | 60 | while let Some(res) = results.next().await { 61 | if let Ok((process, usage, memory)) = res { 62 | let mut stadal_process = StadalProcess::new(); 63 | 64 | stadal_process.pid = process.pid(); 65 | if let Ok(name) = process.name().await { 66 | stadal_process.name = name; 67 | } 68 | if let Ok(status) = process.status().await { 69 | stadal_process.status = format!("{:?}", status); 70 | } 71 | stadal_process.cpu_usage = usage.get::(); 72 | stadal_process.mem = memory.rss().get::(); 73 | stadal_process.virtual_mem = memory.vms().get::(); 74 | 75 | if let Ok(parent_pid) = process.parent_pid().await { 76 | stadal_process.parent = parent_pid.to_string(); 77 | } 78 | 79 | if let Ok(exe) = process.exe().await { 80 | stadal_process.exe = exe.to_string_lossy().to_string(); 81 | } 82 | 83 | #[cfg(not(windows))] 84 | { 85 | if let Ok(command) = process.command().await { 86 | stadal_process.command = command.to_os_string().to_string_lossy().to_string(); 87 | } 88 | } 89 | 90 | output.push(stadal_process) 91 | 92 | } 93 | } 94 | 95 | if !output.is_empty() { 96 | Some(output) 97 | } else { 98 | None 99 | } 100 | } 101 | 102 | #[derive(PartialEq, Eq)] 103 | pub enum ProcessTableSortOrder { 104 | Ascending = 0, 105 | Descending = 1, 106 | } 107 | 108 | pub fn field_comparator() -> fn(&StadalProcess, &StadalProcess) -> Ordering { 109 | |pa, pb| pa.cpu_usage.partial_cmp(&pb.cpu_usage).unwrap_or(Equal) 110 | } 111 | 112 | pub fn get_sort_processes() -> Vec { 113 | let mut proc_vec = block_on(get_processes()).unwrap(); 114 | let mut pm = HashMap::with_capacity(400); 115 | 116 | let mut process_ids = vec![]; 117 | for x in proc_vec.clone() { 118 | pm.insert(x.pid, x.clone()); 119 | process_ids.push(x.pid); 120 | } 121 | 122 | let sorter = field_comparator(); 123 | let sortorder = &ProcessTableSortOrder::Descending; 124 | 125 | proc_vec.sort_by(|a, b| { 126 | let ord = sorter(a, b); 127 | match sortorder { 128 | ProcessTableSortOrder::Ascending => ord, 129 | ProcessTableSortOrder::Descending => ord.reverse(), 130 | } 131 | }); 132 | 133 | // let mut results = vec![]; 134 | // for (k, v) in pm.iter() { 135 | // results.push(v.clone()); 136 | // } 137 | // 138 | // results 139 | proc_vec 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use futures::executor::block_on; 145 | 146 | use crate::infra::process::{get_processes, get_sort_processes}; 147 | 148 | #[test] 149 | fn get_processes_test() { 150 | let processes = block_on(get_processes()).unwrap(); 151 | println!("{},", json!(&processes)); 152 | } 153 | 154 | #[test] 155 | fn get_sort_processes_test() { 156 | let processes = get_sort_processes(); 157 | println!("{},", json!(&processes)); 158 | } 159 | } -------------------------------------------------------------------------------- /core-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | #[macro_use] 4 | extern crate serde_json; 5 | 6 | extern crate serde; 7 | 8 | pub mod app; 9 | pub mod domain; 10 | pub mod infra; 11 | 12 | extern crate xi_trace; 13 | -------------------------------------------------------------------------------- /docs/adr/0001-clean-my-mac.md: -------------------------------------------------------------------------------- 1 | # 1. clean my mac 2 | 3 | 日期: 2020-07-06 4 | 5 | ## 状态 6 | 7 | 2020-07-06 提议 8 | 9 | ## 背景 10 | 11 | Refs: https://github.com/Kevin-De-Koninck/Clean-Me/blob/master/Clean%20Me/Paths.swift 12 | 13 | dir: 14 | 15 | ``` 16 | 17 | let globalTempFilesPath = "/tmp/" 18 | let userCachePath = "~/Library/Caches/" 19 | let userLogsPath = "~/Library/logs/" 20 | let userPreferencesPath = "~/Library/Preferences/" 21 | let systemLogs1Path = "/Library/logs/" 22 | let systemLogs2Path = "/var/log/" 23 | let mailAttachementsPath = "~/Library/Containers/com.apple.mail/Data/Library/Mail\\ Downloads/" 24 | let trashPath = "~/.Trash/" 25 | let xcodeDerivedDataPath = "~/Library/Developer/Xcode/DerivedData/" 26 | let xcodeArchivesPath = "~/Library/Developer/Xcode/Archives/" 27 | let xcodeDeviceLogsPath = "~/Library/Developer/Xcode/iOS\\ Device\\ Logs/" 28 | let terminalCacheFilesPath = "/private/var/log/asl/*.asl" 29 | let terminalCachePath = "/private/var/log/asl/" // used for open func 30 | let bashHistoryFile = "~/.bash_history" 31 | let bashHistoryPath = "~/.bash_sessions/" 32 | let downloadsPath = "~/Downloads/" 33 | let userAppLogsPath = "~/Library/Containers/*/Data/Library/Logs/" 34 | let userAppCachePath = "~/Library/Containers/*/Data/Library/Caches/" 35 | let spotlightPath = "/.Spotlight-V100/" 36 | let docRevPath = "/.DocumentRevisions-V100/" 37 | let imessageAttachmentsPath = "~/Library/Messages/Attachments/" 38 | ``` 39 | 40 | with Command: 41 | 42 | ``` 43 | sudo du -sh /var 44 | ``` 45 | 46 | Get All Homebrew 47 | 48 | https://stackoverflow.com/questions/40065188/get-size-of-each-installed-formula-in-homebrew 49 | 50 | ``` 51 | brew list | xargs brew info | egrep --color '\d*\.\d*(KB|MB|GB)' 52 | ``` 53 | 54 | Dev Tools: 55 | 56 | ### Gradle 57 | 58 | ``` 59 | rm -rf ~/.gradle/caches/build-cache-* 60 | ``` 61 | 62 | 63 | ## 决策 64 | 65 | 在这里补充上决策信息... 66 | 67 | ## 后果 68 | 69 | 在这里记录结果... 70 | -------------------------------------------------------------------------------- /docs/adr/README.md: -------------------------------------------------------------------------------- 1 | # 架构决策记录 2 | 3 | * [1. clean-my-mac](0001-clean-my-mac.md) 4 | -------------------------------------------------------------------------------- /docs/stadal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/docs/stadal.png -------------------------------------------------------------------------------- /gui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | # 使用单引号 11 | quote_type = single 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /gui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /gui/LICENSE.md: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | ================== 3 | 4 | Statement of Purpose 5 | --------------------- 6 | 7 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 8 | 9 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 10 | 11 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 12 | 13 | 1. Copyright and Related Rights. 14 | -------------------------------- 15 | A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 16 | 17 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 18 | ii. moral rights retained by the original author(s) and/or performer(s); 19 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 20 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 21 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 22 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 23 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 24 | 25 | 2. Waiver. 26 | ----------- 27 | To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 28 | 29 | 3. Public License Fallback. 30 | ---------------------------- 31 | Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 32 | 33 | 4. Limitations and Disclaimers. 34 | -------------------------------- 35 | 36 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 37 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 38 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 39 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 40 | -------------------------------------------------------------------------------- /gui/README.md: -------------------------------------------------------------------------------- 1 | # electron-quick-start-typescript 2 | 3 | **Clone and run for a quick way to see Electron in action.** 4 | 5 | This is a typescript port of the [Electron Quick Start repo](https://github.com/electron/electron-quick-start) -- a minimal Electron application based on the [Quick Start Guide](http://electron.atom.io/docs/tutorial/quick-start) within the Electron documentation. 6 | 7 | **Use this app along with the [Electron API Demos](http://electron.atom.io/#get-started) app for API code examples to help you get started.** 8 | 9 | A basic Electron application needs just these files: 10 | 11 | - `package.json` - Points to the app's main file and lists its details and dependencies. 12 | - `main.ts` - Starts the app and creates a browser window to render HTML. This is the app's **main process**. 13 | - `index.html` - A web page to render. This is the app's **renderer process**. 14 | 15 | You can learn more about each of these components within the [Quick Start Guide](http://electron.atom.io/docs/tutorial/quick-start). 16 | 17 | ## To Use 18 | 19 | To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line: 20 | 21 | ```bash 22 | # Clone this repository 23 | git clone https://github.com/electron/electron-quick-start-typescript 24 | # Go into the repository 25 | cd electron-quick-start-typescript 26 | # Install dependencies 27 | npm install 28 | # Run the app 29 | npm start 30 | ``` 31 | 32 | Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt. 33 | 34 | ## Re-compile automatically 35 | 36 | To recompile automatically and to allow using [electron-reload](https://github.com/yan-foto/electron-reload), run this in a separate terminal: 37 | 38 | ```bash 39 | npm run watch 40 | ``` 41 | 42 | ## Resources for Learning Electron 43 | 44 | - [electron.atom.io/docs](http://electron.atom.io/docs) - all of Electron's documentation 45 | - [electron.atom.io/community/#boilerplates](http://electron.atom.io/community/#boilerplates) - sample starter apps created by the community 46 | - [electron/electron-quick-start](https://github.com/electron/electron-quick-start) - a very basic starter Electron app 47 | - [electron/simple-samples](https://github.com/electron/simple-samples) - small applications with ideas for taking them further 48 | - [electron/electron-api-demos](https://github.com/electron/electron-api-demos) - an Electron app that teaches you how to use Electron 49 | - [hokein/electron-sample-apps](https://github.com/hokein/electron-sample-apps) - small demo apps for the various Electron APIs 50 | 51 | ## License 52 | 53 | [CC0 1.0 (Public Domain)](LICENSE.md) 54 | -------------------------------------------------------------------------------- /gui/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --background-color: #f5f5f4; 4 | --text-color: #333; 5 | --window-border-radius: 6px; 6 | --font-size: 14px; 7 | } 8 | 9 | body { 10 | padding: 12px; 11 | } 12 | 13 | .memory-content { 14 | border: solid 1px #000; 15 | display: flex; 16 | justify-content: space-between; 17 | } 18 | 19 | #processes .item { 20 | display: flex; 21 | justify-content: space-between; 22 | } 23 | 24 | #exit-app { 25 | float: right; 26 | } 27 | -------------------------------------------------------------------------------- /gui/assets/images/cloudTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/cloudTemplate.png -------------------------------------------------------------------------------- /gui/assets/images/cloudTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/cloudTemplate@2x.png -------------------------------------------------------------------------------- /gui/assets/images/flagTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/flagTemplate.png -------------------------------------------------------------------------------- /gui/assets/images/flagTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/flagTemplate@2x.png -------------------------------------------------------------------------------- /gui/assets/images/moonTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/moonTemplate.png -------------------------------------------------------------------------------- /gui/assets/images/moonTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/moonTemplate@2x.png -------------------------------------------------------------------------------- /gui/assets/images/sunTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/sunTemplate.png -------------------------------------------------------------------------------- /gui/assets/images/sunTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/sunTemplate@2x.png -------------------------------------------------------------------------------- /gui/assets/images/umbrellaTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/umbrellaTemplate.png -------------------------------------------------------------------------------- /gui/assets/images/umbrellaTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/stadal/237c07b1b2710b4e73898f9a62c30c53b6a0aee6/gui/assets/images/umbrellaTemplate@2x.png -------------------------------------------------------------------------------- /gui/assets/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,$(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ce.FLIP:p=[n,i];break;case ce.CLOCKWISE:p=G(n);break;case ce.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&u||!w&&'end'===r&&g),v=y||E;(m||b||v)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),v&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport',flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!fe),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=B('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=le({},E,e.attributes),e.styles=le({},m,e.styles),e.arrowStyles=le({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return V(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&V(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),V(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ge}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-quick-start-typescript", 3 | "version": "1.0.0", 4 | "description": "A minimal Electron application written with Typescript", 5 | "scripts": { 6 | "build": "tsc && cp -a views dist", 7 | "watch": "tsc -w", 8 | "lint": "tslint -c tslint.json -p tsconfig.json", 9 | "start": "npm run build && electron ./dist/main.js" 10 | }, 11 | "repository": "https://github.com/electron/electron-quick-start-typescript", 12 | "keywords": [ 13 | "Electron", 14 | "quick", 15 | "start", 16 | "tutorial", 17 | "demo", 18 | "typescript" 19 | ], 20 | "author": "GitHub", 21 | "license": "CC0-1.0", 22 | "devDependencies": { 23 | "@types/node": "12.12.6", 24 | "electron": "^9.0.4", 25 | "electron-build-env": "^0.2.0", 26 | "neon-cli": "^0.4.0", 27 | "tslint": "^6.1.2", 28 | "typescript": "^3.9.5" 29 | }, 30 | "dependencies": { 31 | "execa": "^4.0.2", 32 | "reflect-metadata": "^0.1.13", 33 | "tsyringe": "^4.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /gui/src/main.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow, ipcMain, Menu, nativeImage, NativeImage, Tray} from "electron"; 2 | import * as path from "path"; 3 | 4 | app.allowRendererProcessReuse = false; 5 | 6 | const assetsDirectory = path.join(__dirname, '../assets') 7 | 8 | let tray: Tray = undefined 9 | let win: BrowserWindow = undefined 10 | 11 | function createWindow() { 12 | win = new BrowserWindow({ 13 | width: 300, 14 | height: 250, 15 | frame: false, 16 | show: false, 17 | fullscreenable: false, 18 | // resizable: false, 19 | transparent: true, 20 | backgroundColor: '#fff', 21 | webPreferences: { 22 | backgroundThrottling: false, 23 | preload: path.join(__dirname, "preload.js"), 24 | nodeIntegration: true 25 | }, 26 | }); 27 | 28 | win.loadFile(path.join(__dirname, "../views/index.html")); 29 | win.on('blur', () => { 30 | if (!win.webContents.isDevToolsOpened()) { 31 | win.hide() 32 | win.webContents.send('window.blur') 33 | } 34 | }) 35 | } 36 | 37 | app.dock.hide(); 38 | app.on("ready", () => { 39 | createTray(); 40 | createWindow(); 41 | 42 | app.on("activate", function () { 43 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 44 | }); 45 | }); 46 | 47 | app.on("window-all-closed", () => { 48 | if (process.platform !== "darwin") { 49 | app.quit(); 50 | } 51 | }); 52 | 53 | const getWindowPosition = () => { 54 | const windowBounds = win.getBounds() 55 | const trayBounds = tray.getBounds() 56 | 57 | const x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2)) 58 | const y = Math.round(trayBounds.y + trayBounds.height + 4) 59 | 60 | return {x: x, y: y} 61 | } 62 | 63 | const createTray = () => { 64 | let image_path = path.join(assetsDirectory, 'images/sunTemplate.png'); 65 | tray = new Tray(nativeImage.createFromPath(image_path)) 66 | tray.on('right-click', toggleWindow) 67 | tray.on('double-click', toggleWindow) 68 | tray.on('click', function (event) { 69 | toggleWindow() 70 | 71 | if (win.isVisible() && process.defaultApp && event.metaKey) { 72 | win.webContents.openDevTools(); 73 | } 74 | }) 75 | } 76 | 77 | const toggleWindow = () => { 78 | if (win.isVisible()) { 79 | win.hide() 80 | win.webContents.send('window.blur') 81 | } else { 82 | showWindow() 83 | win.webContents.send('window.focus') 84 | } 85 | } 86 | 87 | const showWindow = () => { 88 | const position = getWindowPosition() 89 | win.setPosition(position.x, position.y, false) 90 | win.show() 91 | win.focus() 92 | } 93 | 94 | ipcMain.on('show-window', () => { 95 | showWindow() 96 | }) 97 | 98 | ipcMain.on('stadal.exit', () => { 99 | app.exit() 100 | }) 101 | -------------------------------------------------------------------------------- /gui/src/preload.ts: -------------------------------------------------------------------------------- 1 | // All of the Node.js APIs are available in the preload process. 2 | // It has the same sandbox as a Chrome extension. 3 | window.addEventListener("DOMContentLoaded", () => { 4 | const replaceText = (selector: string, text: string) => { 5 | const element = document.getElementById(selector); 6 | if (element) { 7 | element.innerText = text; 8 | } 9 | }; 10 | 11 | for (const type of ["chrome", "node", "electron"]) { 12 | replaceText(`${type}-version`, (process.versions as any)[type]); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /gui/src/render/actions.ts: -------------------------------------------------------------------------------- 1 | import {injectable} from "tsyringe"; 2 | import {niceBytes, secondsToHms} from "./format"; 3 | import {capitalizeFirstLetter} from "../utils/string-util"; 4 | 5 | interface StadalMemory { 6 | total: string, 7 | available: string, 8 | free: string, 9 | swap_total: string, 10 | swap_free: string, 11 | swap_used: string, 12 | } 13 | 14 | interface StadalHost { 15 | name: string, 16 | release: string, 17 | version: string, 18 | hostname: string, 19 | arch: string, 20 | uptime: string, 21 | } 22 | 23 | interface Language { 24 | name: string, 25 | version: string, 26 | } 27 | 28 | interface CleanSize { 29 | name: string, 30 | size: string, 31 | path: string, 32 | } 33 | 34 | interface CPU { 35 | cores: string, 36 | current_ghz: string, 37 | min_ghz: string, 38 | max_ghz: string, 39 | } 40 | 41 | interface Disk { 42 | device: string, 43 | filesystem: string, 44 | mount: string, 45 | total: string, 46 | used: string, 47 | free: string, 48 | } 49 | 50 | interface Process { 51 | pid: number, 52 | name: string, 53 | status: string, 54 | cpu_usage: number, 55 | mem: number, 56 | virtual_mem: number, 57 | parent: string, 58 | exe: string, 59 | command: string, 60 | } 61 | 62 | 63 | @injectable() 64 | export default class Actions { 65 | display_memory(data: StadalMemory) { 66 | document.getElementById("mem-total").innerText = niceBytes(data.total); 67 | document.getElementById("mem-available").innerText = niceBytes(data.available); 68 | document.getElementById("mem-free").innerText = niceBytes(data.free); 69 | document.getElementById("swap-total").innerText = niceBytes(data.swap_total); 70 | document.getElementById("swap-free").innerText = niceBytes(data.swap_free); 71 | document.getElementById("swap-used").innerText = niceBytes(data.swap_used); 72 | 73 | let swap_rate: number = (parseInt(data.swap_used, 10) / 1024) / (parseInt(data.swap_total, 10) / 1024) * 100; 74 | document.getElementById("swap-container").innerHTML = ` 75 |
76 |
77 |
78 | `; 79 | } 80 | 81 | display_host(data: StadalHost) { 82 | document.getElementById("host-version").innerText = data.version; 83 | document.getElementById("host-uptime").innerText = secondsToHms(data.uptime); 84 | } 85 | 86 | display_languages(data: Language[]) { 87 | let result = ""; 88 | for (let datum of data) { 89 | result += `${capitalizeFirstLetter(datum.name)} : ${datum.version}
` 90 | } 91 | document.getElementById("languages").innerHTML = result; 92 | } 93 | 94 | display_sizes(data: CleanSize[]) { 95 | let result = ""; 96 | for (let datum of data) { 97 | result += `${capitalizeFirstLetter(datum.name)} : ${niceBytes(datum.size)} , ${datum.path}
` 98 | } 99 | document.getElementById("sizes").innerHTML = result; 100 | } 101 | 102 | display_cpu(data: CPU) { 103 | let innerHTML = `CPU -> cores:${data.cores}, current: ${data.current_ghz} `; 104 | document.getElementById("cpu").innerText = innerHTML; 105 | } 106 | 107 | display_disks(data: Disk[]) { 108 | let results = ''; 109 | for (let datum of data) { 110 | let innerHTML = `
111 |
Device ${datum.device}
112 |
Mount ${datum.mount}
113 |
Total ${niceBytes(datum.total)}
114 |
Free ${niceBytes(datum.free)}
115 |
Used ${niceBytes(datum.used)}
116 |
117 | `; 118 | 119 | results += innerHTML; 120 | } 121 | document.getElementById("disk").innerHTML = results; 122 | } 123 | 124 | display_processes(data: Process[]) { 125 | let results = ''; 126 | for (let datum of data) { 127 | let innerHTML = `
128 |
${datum.pid}
${datum.name}
${datum.cpu_usage.toFixed(2)}%
${niceBytes(datum.mem)}
129 |
130 | `; 131 | results += innerHTML; 132 | } 133 | document.getElementById("processes").innerHTML = results; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /gui/src/render/core.ts: -------------------------------------------------------------------------------- 1 | import * as execa from 'execa'; 2 | import {ChildProcess} from 'child_process'; 3 | import EventEmitter from '../utils/emitter'; 4 | import {XI_CORE_BIN, XI_CORE_DIR} from '../utils/environment'; 5 | import ViewProxy from './view-proxy'; 6 | import {CoreMethod, CoreResponse} from './types/core'; 7 | import {container} from "tsyringe"; 8 | import Actions from "./actions"; 9 | 10 | export type CoreOptions = { 11 | env?: { [key: string]: string | undefined }, 12 | configDir?: string, 13 | }; 14 | 15 | /** 16 | * This is a class that manages xi-core. It creates ViewProxies which are simple 17 | * emitters that link xi-core's internal views with out actual ViewControllers. 18 | * It is also responsible for encoding/decoding messages to and from xi-core, and 19 | * managing the spawned process. 20 | */ 21 | export default class Core extends EventEmitter { 22 | 23 | // The spawned child process. 24 | private child: ChildProcess; 25 | 26 | // References to our ViewProxy classes. Keyed by the view's id. 27 | private proxies: { [key: string]: ViewProxy }; 28 | private action: Actions; 29 | 30 | /** 31 | * Create the class. 32 | * @param {Object} env The environment map to use when spawning xi-core. 33 | */ 34 | constructor(opts: CoreOptions) { 35 | super(); 36 | 37 | this.proxies = {}; 38 | 39 | // Spawn xi-core. 40 | this.child = execa(XI_CORE_BIN, [], {env: opts.env || {}}); 41 | this.child.on('close', this.coreClosed.bind(this)); 42 | 43 | // Receive messages from xi-core as text. 44 | this.stdout().setEncoding('utf8'); 45 | this.stderr().setEncoding('utf8'); 46 | 47 | // Listen to its streams. 48 | this.stdout().on('data', this.eventFromCore.bind(this)); 49 | this.stderr().on('data', this.errorFromCore.bind(this)); 50 | 51 | this.action = container.resolve(Actions); 52 | this.send(CoreMethod.CLIENT_STARTED, { 53 | client_extras_dir: XI_CORE_DIR, 54 | config_dir: opts.configDir || XI_CORE_DIR 55 | }); 56 | } 57 | 58 | /** 59 | * Public API 60 | */ 61 | 62 | /** 63 | * Serialise and send a message to xi-core. 64 | * @param {CoreMethod} method The method to send. 65 | * @param {Object} params The method's parameters. 66 | * @param {Object} rest An optional object to extend the top request. 67 | * @return {Boolean} Whether or not the message successfully sent. 68 | */ 69 | public send(method: CoreMethod, params: any = {}, rest: any = {}): boolean { 70 | const data = {method, params, ...rest}; 71 | try { 72 | this.stdin().write(`${JSON.stringify(data)}\n`); 73 | return true; 74 | } catch (e) { 75 | console.error(e); 76 | return false; 77 | } 78 | } 79 | 80 | public send_multiple(data: { method: CoreMethod, params?: any, rest?: any }[]): boolean { 81 | let output = ""; 82 | for (let datum of data) { 83 | const st = { 84 | method: datum.method, 85 | params: datum.params ? datum.params : {} 86 | } 87 | output += `${JSON.stringify(st)}\n`; 88 | } 89 | try { 90 | this.stdin().write(output); 91 | return true; 92 | } catch (e) { 93 | console.error(e); 94 | return false; 95 | } 96 | } 97 | 98 | public close() { 99 | this.child.kill(); 100 | } 101 | 102 | /** 103 | * Private API 104 | */ 105 | 106 | // Getters for easier access to streams. 107 | private stdin() { 108 | return this.child.stdin; 109 | } 110 | 111 | private stdout() { 112 | return this.child.stdout; 113 | } 114 | 115 | private stderr() { 116 | return this.child.stderr; 117 | } 118 | 119 | /** 120 | * Called when we get events from xi-core's `stdout` stream. 121 | * @param {String} data Raw data emitted from xi-core's stdout. 122 | */ 123 | private eventFromCore(raw: string) { 124 | parseMessages(raw).forEach((msg) => { 125 | // Otherwise respond to other messages. 126 | switch (msg.method) { 127 | case CoreResponse.CONFIG_STATUS: { 128 | return; 129 | } 130 | case CoreResponse.SEND_MEMORY: { 131 | this.action.display_memory(msg.params) 132 | return; 133 | } 134 | case CoreResponse.SEND_HOST: { 135 | this.action.display_host(msg.params) 136 | return; 137 | } 138 | case CoreResponse.SEND_LANGUAGES: { 139 | this.action.display_languages(msg.params) 140 | return; 141 | } 142 | case CoreResponse.SEND_SIZES: { 143 | this.action.display_sizes(msg.params) 144 | return; 145 | } 146 | case CoreResponse.SEND_CPU: { 147 | this.action.display_cpu(msg.params) 148 | return; 149 | } 150 | case CoreResponse.SEND_DISKS: { 151 | this.action.display_disks(msg.params) 152 | return; 153 | } 154 | case CoreResponse.SEND_PROCESSES: { 155 | this.action.display_processes(msg.params) 156 | return; 157 | } 158 | default: { 159 | console.warn('Unhandled message from core: ', msg); 160 | } 161 | } 162 | }); 163 | } 164 | 165 | /** 166 | * Called when we get events from xi-core's `stderr` stream. 167 | * @param {String} data Raw data emitted from xi-core's stderr. 168 | */ 169 | private errorFromCore(data: Buffer) { 170 | console.log(`${data}`); 171 | } 172 | 173 | /** 174 | * Called when the xi-core process has closed. 175 | * @param {Number} code The exit code of the process. 176 | * @param {String} signal The close signal (why the process closed). 177 | */ 178 | private coreClosed(code: number, signal: string) { 179 | // TODO: if error attempt to reboot core process? 180 | // TODO: or alternatively just close the app with a dialog error? 181 | console.log('core proc closed: ', code, signal); 182 | } 183 | 184 | /** 185 | * This function is bound to this class and given to each ViewProxy so that 186 | * they may send messages back to the core process. 187 | * @param {CoreMethod} method The method to send. 188 | * @param {Object} params The method's parameters. 189 | */ 190 | private proxySend = (method: CoreMethod, params: any = {}): void => { 191 | this.send(method, params); 192 | } 193 | } 194 | 195 | // Helpers --------------------------------------------------------------------- 196 | 197 | /** 198 | * Parses a message (from stdout/err) sent from xi-core. Xi sends multiple 199 | * messages as serialised JSON objects separated by newlines. 200 | * @param {String} string Raw data emitted from xi-core's stdout. 201 | * @return {Array} An array containing JSON objects of xi's messages. 202 | */ 203 | function parseMessages(raw: string): Array { 204 | const parsed = []; 205 | const lines = raw.split('\n'); 206 | 207 | for (let i = 0; i < lines.length; ++i) { 208 | if (typeof lines[i] !== 'string' || lines[i] === '') { 209 | continue; 210 | } 211 | try { 212 | parsed.push(JSON.parse(lines[i])); 213 | } catch (err) { 214 | console.warn('Error parsing message from core!'); 215 | console.error(err); 216 | } 217 | } 218 | 219 | return parsed; 220 | } 221 | -------------------------------------------------------------------------------- /gui/src/render/format.ts: -------------------------------------------------------------------------------- 1 | const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 2 | 3 | export function niceBytes(x: string | number) { 4 | let l = 0; 5 | let n; 6 | if (typeof x === "string") { 7 | n = parseInt(x, 10) || 0; 8 | } else { 9 | n = x; 10 | } 11 | 12 | while (n >= 1024 && ++l) { 13 | n = n / 1024; 14 | } 15 | 16 | return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]); 17 | } 18 | 19 | export function secondsToHms(d: string) { 20 | let value = Number(parseInt(d, 10)); 21 | const h = Math.floor(value / 3600); 22 | const m = Math.floor(value % 3600 / 60); 23 | const s = Math.floor(value % 3600 % 60); 24 | 25 | const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : ""; 26 | const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : ""; 27 | const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : ""; 28 | return hDisplay + mDisplay + sDisplay; 29 | } 30 | -------------------------------------------------------------------------------- /gui/src/render/renderer.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import * as path from "path"; 3 | 4 | const {ipcRenderer} = require('electron') 5 | 6 | let Core = require('./core').default; 7 | (window).Core = Core; 8 | 9 | const opts = { 10 | filePath: path.resolve(__dirname, '..'), 11 | coreOptions: { 12 | env: Object.assign({RUST_BACKTRACE: "full"}, process.env) 13 | }, 14 | viewOptions: {} 15 | }; 16 | 17 | (window).stadal = new Core(opts.coreOptions); 18 | 19 | function sendMessage() { 20 | (window).stadal.send_multiple([ 21 | {method: "send_memory"}, 22 | {method: "send_processes"}, 23 | ]) 24 | } 25 | 26 | function sendFirstMessages() { 27 | (window).stadal.send_multiple([ 28 | {method: "send_host"}, 29 | {method: "send_memory"}, 30 | {method: "send_languages"}, 31 | {method: "send_sizes"}, 32 | {method: "send_cpu"}, 33 | {method: "send_disks"}, 34 | {method: "send_processes"}, 35 | ]) 36 | } 37 | 38 | function startGetData() { 39 | let memoryInterval = setInterval(() => { 40 | if ((window).stadal) { 41 | sendFirstMessages(); 42 | } 43 | }, 1000); 44 | 45 | ipcRenderer.on('window.focus', (event, arg) => { 46 | if (!memoryInterval) { 47 | memoryInterval = setInterval(() => { 48 | if ((window).stadal) { 49 | sendMessage(); 50 | } 51 | }, 1000); 52 | } 53 | }) 54 | 55 | ipcRenderer.on('window.blur', (event, arg) => { 56 | clearInterval(memoryInterval); 57 | memoryInterval = null; 58 | }) 59 | } 60 | 61 | setTimeout(() => { 62 | startGetData(); 63 | }, 5000); 64 | 65 | const demoButton = document.getElementById('exit-app'); 66 | demoButton.addEventListener('click', () => { 67 | ipcRenderer.send('stadal.exit'); 68 | }) 69 | -------------------------------------------------------------------------------- /gui/src/render/types/core.ts: -------------------------------------------------------------------------------- 1 | export enum CoreMethod { 2 | CLIENT_STARTED = 'client_started', 3 | SEND_MEMORY = 'send_memory', 4 | SEND_HOST = 'send_host', 5 | SEND_LANGUAGES = 'send_languages', 6 | SEND_SIZES = 'send_sizes', 7 | SEND_CPU = 'send_cpu', 8 | SEND_DISKS = 'send_disks', 9 | SEND_PROCESSES = 'send_processes', 10 | } 11 | 12 | export enum CoreResponse { 13 | CONFIG_STATUS = 'config_status', 14 | SEND_MEMORY = 'send_memory', 15 | SEND_HOST = 'send_host', 16 | SEND_LANGUAGES = 'send_languages', 17 | SEND_SIZES = 'send_sizes', 18 | SEND_CPU = 'send_cpu', 19 | SEND_DISKS = 'send_disks', 20 | SEND_PROCESSES = 'send_processes', 21 | } 22 | -------------------------------------------------------------------------------- /gui/src/render/view-proxy.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '../utils/emitter'; 2 | import {CoreMethod} from './types/core'; 3 | 4 | /** 5 | * A proxy that listens/emits to events in regards to one xi view. 6 | * This is a simple emitter that channels core events directly to its 7 | * ViewController. 8 | */ 9 | export default class ViewProxy extends EventEmitter { 10 | 11 | // Our unique view instance id. 12 | id: number; 13 | 14 | // The view's id generated by xi-core. 15 | viewId: string; 16 | 17 | // A function given that sends a message to the Core. 18 | sendToCore: (method: CoreMethod, params: any) => void; 19 | 20 | /** 21 | * Create the ViewProxy. 22 | * @param {Function} sendToCore Sends a message to the Core. 23 | * @param {Number} id This classes unique id. 24 | * @param {CoreMethod} viewId The id of xi-core's corresponding view. 25 | */ 26 | constructor(sendToCore: (method: CoreMethod, params: any) => void, id: number, viewId: string) { 27 | super(); 28 | 29 | this.id = id; 30 | this.viewId = viewId; 31 | this.sendToCore = sendToCore; 32 | } 33 | 34 | /** 35 | * Send a message back to xi-core's corresponding view. 36 | * @param {CoreMethod} method The method to send. 37 | * @param {Object} params Method parameters. 38 | */ 39 | send(method: CoreMethod, params: any = {}) { 40 | params.view_id = this.viewId; 41 | this.sendToCore(method, params); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gui/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export function elt( 2 | tag: string, 3 | content?: string | HTMLElement[] | null, 4 | className?: string | null, 5 | cssText?: string 6 | ): HTMLElement { 7 | const e = document.createElement(tag); 8 | if (className) { 9 | e.className = className; 10 | } 11 | if (cssText) { 12 | e.style.cssText = cssText; 13 | } 14 | if (typeof content === 'string') { 15 | e.appendChild(document.createTextNode(content)); 16 | } else if (Array.isArray(content)) { 17 | for (let i = 0; i < content.length; ++i) { 18 | e.appendChild(content[i]); 19 | } 20 | } 21 | return e; 22 | } 23 | 24 | export type ListenerOptions = { 25 | capture: boolean, 26 | passive: boolean 27 | } | boolean; 28 | 29 | export function on( 30 | el: HTMLElement, 31 | event: string, 32 | listener: (...args: any[]) => void, 33 | opts: ListenerOptions = false 34 | ): void { 35 | el.addEventListener(event, listener, opts); 36 | } 37 | 38 | export function off( 39 | el: HTMLElement, 40 | event: string, 41 | listener: (...args: any[]) => void, 42 | opts: ListenerOptions = false 43 | ): void { 44 | el.removeEventListener(event, listener, opts); 45 | } 46 | 47 | export function removeChildren(el: HTMLElement): HTMLElement { 48 | while (el.firstChild) { 49 | el.removeChild(el.firstChild); 50 | } 51 | return el; 52 | } 53 | 54 | export function removeChildrenAndAdd(parent: HTMLElement, el: HTMLElement | HTMLElement[]): HTMLElement { 55 | if (Array.isArray(el)) { 56 | removeChildren(parent); 57 | el.forEach((e) => parent.appendChild(e)); 58 | return parent; 59 | } else { 60 | return removeChildren(parent).appendChild(el); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /gui/src/utils/emitter.ts: -------------------------------------------------------------------------------- 1 | // Simple event emitter class. 2 | export default class EventEmitter { 3 | 4 | // Map of events and listeners. 5 | _events: { [key: string]: Array<(...args: any[]) => void> }; 6 | 7 | // Max amount of listeners that will be added without warnings. Useful to 8 | // protect against accidental memory leaks. 9 | _maxListeners: number = 10; 10 | 11 | /** 12 | * Instantiate the class. 13 | */ 14 | constructor() { 15 | this._events = {}; 16 | } 17 | 18 | /** 19 | * Get the max listeners settings. 20 | * @return {Number} Value of setting. 21 | */ 22 | getMaxListeners(): number { 23 | return this._maxListeners; 24 | } 25 | 26 | /** 27 | * Set the max listeners. Any more listeners added to a single event will 28 | * result in console warnings. 29 | * @param {Number} n The desired value. 30 | */ 31 | setMaxListeners(n: number): number { 32 | return this._maxListeners = n; 33 | } 34 | 35 | /** 36 | * Add a listener to the given event. 37 | * @param {String} event The event name. 38 | * @param {(...args: any[]) => void} listener The listener. 39 | */ 40 | on(event: string, listener: (...args: any[]) => void) { 41 | if (this._events[event] == null) { 42 | this._events[event] = []; 43 | } 44 | 45 | const n = this._events[event].push(listener); 46 | if (n > this._maxListeners) { 47 | console.warn(`Possible EventEmitter memory leak detected. ${n} "${ 48 | event}" listener(s) added. Use emitter.setMaxListeners() to increase limit.`); 49 | } 50 | } 51 | 52 | /** 53 | * Listen to an event only once. 54 | * @param {String} event The event name. 55 | * @param {(...args: any[]) => void} listener: The listener. 56 | */ 57 | once(event: string, listener: (...args: any[]) => void) { 58 | const once = (...args: any[]) => { 59 | listener(...args); 60 | this.off(event, once); 61 | }; 62 | 63 | this.on(event, once); 64 | } 65 | 66 | /** 67 | * Remove the given listener from the event. 68 | * TODO: review how this works: it currently scans backwards so identical 69 | * listeners are added/removed in a stack-like fashion. If this is called, 70 | * should all instances of the same listener be removed? Or just the last 71 | * one that's been added? 72 | * @param {String} event The event name. 73 | * @param {(...args: any[]) => void} listener The listener. 74 | */ 75 | off(event: string, listener: (...args: any[]) => void) { 76 | const listeners = this._events[event]; 77 | if (listeners && listeners.length) { 78 | for (let i = listeners.length - 1; i >= 0; --i) { 79 | if (listeners[i] === listener) { 80 | listeners.splice(i, 1); 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Emit an event - also emits the "all" event. 89 | * @param {String} event The event name. 90 | * @return {Boolean} Returns true if there were any listeners subscribed 91 | * to the event. 92 | */ 93 | emit(event: string, ...args: Array) { 94 | const listeners = this._events[event]; 95 | const listenersExist = !!(listeners && listeners.length); 96 | if (listenersExist) { 97 | for (let i = 0; i < listeners.length; ++i) { 98 | listeners[i](...args); 99 | } 100 | } 101 | 102 | if (event != 'all') { 103 | this.emit('all', ...args); 104 | } 105 | 106 | return listenersExist; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gui/src/utils/environment.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const MACOS = process.platform === 'darwin'; 4 | export const WIN = process.platform === 'win32'; 5 | 6 | // project dirs 7 | const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); 8 | // const SOURCE_DIR = path.join(PROJECT_ROOT, '..'); 9 | 10 | // xi-core 11 | export const XI_CORE_DIR = path.join(PROJECT_ROOT, '../target/release/'); 12 | export const XI_CORE_BIN = path.join(XI_CORE_DIR, WIN ? 'stadal.exe' : 'stadal'); 13 | -------------------------------------------------------------------------------- /gui/src/utils/string-util.ts: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string: String) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /gui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "baseUrl": ".", 9 | "target": "es6", 10 | "paths": { 11 | "*": [ 12 | "node_modules/*" 13 | ] 14 | }, 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /gui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "max-line-length": { 5 | "options": [ 6 | 120 7 | ] 8 | }, 9 | "new-parens": true, 10 | "no-arg": true, 11 | "no-bitwise": true, 12 | "no-conditional-assignment": true, 13 | "no-consecutive-blank-lines": false 14 | }, 15 | "jsRules": { 16 | "max-line-length": { 17 | "options": [ 18 | 120 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /gui/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello World! 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 |
29 |

Memory

30 |
31 |
Total
32 |
Available
33 |
Free
34 |
35 |

Swap

36 |
37 |
Total
38 |
Free
39 |
Used
40 |
41 |
42 | 43 |
44 |

Disk

45 |
46 | 47 |
48 |
49 |
50 |

CPU

51 |
52 |
53 |
54 |

Processes

55 |
56 |
57 |
PID
Name
CPU
Mem
58 |
59 |
60 |
61 |
62 |
63 |

Host

64 |

UP Time:

65 |
66 |
67 |

Programming Env

68 |
69 |
70 |
71 |

Clean Size

72 |
73 |
74 | 75 |

76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | tests: 2 | cargo test 3 | 4 | tests-ci: 5 | cargo test 6 | 7 | build: 8 | cargo build 9 | 10 | @bench: 11 | cargo bench 12 | 13 | @lint: 14 | rustup component add clippy 15 | rustup component add rustfmt 16 | cargo clippy -- -D warnings 17 | cargo clippy --tests 18 | cargo fmt -- --check 19 | 20 | @fix: 21 | cargo fmt --all 22 | 23 | clean: 24 | cargo clean 25 | find . -type f -name "*.orig" -exec rm {} \; 26 | find . -type f -name "*.bk" -exec rm {} \; 27 | find . -type f -name ".*~" -exec rm {} \; 28 | -------------------------------------------------------------------------------- /rpc/BUILD.gn: -------------------------------------------------------------------------------- 1 | import("//build/rust/rust_library.gni") 2 | 3 | rust_library("xi_rpc") { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /rpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xi-rpc" 3 | version = "0.3.0" 4 | license = "Apache-2.0" 5 | authors = ["Raph Levien "] 6 | repository = "https://github.com/google/xi-editor" 7 | description = "Utilities for building peers (both client and server side) for xi's JSON RPC variant." 8 | edition = '2018' 9 | 10 | [dependencies] 11 | log = "0.4.3" 12 | serde = "1.0" 13 | serde_json = "1.0" 14 | serde_derive = "1.0" 15 | crossbeam-utils = "0.7" 16 | 17 | xi-trace = { path = "../trace", version = "0.2.0" } 18 | -------------------------------------------------------------------------------- /rpc/examples/try_chan.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! A simple test program for evaluating the speed of cross-thread communications. 16 | extern crate xi_rpc; 17 | 18 | use std::sync::mpsc; 19 | use std::thread; 20 | 21 | /* 22 | use xi_rpc::chan::Chan; 23 | 24 | pub fn test_chan() { 25 | let n_iter = 1000000; 26 | let chan1 = Chan::new(); 27 | let chan1s = chan1.clone(); 28 | let chan2 = Chan::new(); 29 | let chan2s = chan2.clone(); 30 | let thread1 = thread::spawn(move|| { 31 | for _ in 0..n_iter { 32 | chan2s.try_send(chan1.recv()); 33 | } 34 | }); 35 | let thread2 = thread::spawn(move|| { 36 | for _ in 0..n_iter { 37 | chan1s.try_send(42); 38 | let _ = chan2.recv(); 39 | } 40 | }); 41 | let _ = thread1.join(); 42 | let _ = thread2.join(); 43 | } 44 | */ 45 | 46 | pub fn test_mpsc() { 47 | let n_iter = 1000000; 48 | let (chan1s, chan1) = mpsc::channel(); 49 | let (chan2s, chan2) = mpsc::channel(); 50 | let thread1 = thread::spawn(move || { 51 | for _ in 0..n_iter { 52 | chan2s.send(chan1.recv()).unwrap(); 53 | } 54 | }); 55 | let thread2 = thread::spawn(move || { 56 | for _ in 0..n_iter { 57 | chan1s.send(42).unwrap(); 58 | let _ = chan2.recv(); 59 | } 60 | }); 61 | let _ = thread1.join(); 62 | let _ = thread2.join(); 63 | } 64 | 65 | pub fn main() { 66 | test_mpsc() 67 | } 68 | -------------------------------------------------------------------------------- /rpc/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt; 16 | use std::io; 17 | 18 | use serde::de::{Deserialize, Deserializer}; 19 | use serde::ser::{Serialize, Serializer}; 20 | use serde_json::{Error as JsonError, Value}; 21 | 22 | /// The possible error outcomes when attempting to send a message. 23 | #[derive(Debug)] 24 | pub enum Error { 25 | /// An IO error occurred on the underlying communication channel. 26 | Io(io::Error), 27 | /// The peer returned an error. 28 | RemoteError(RemoteError), 29 | /// The peer closed the connection. 30 | PeerDisconnect, 31 | /// The peer sent a response containing the id, but was malformed. 32 | InvalidResponse, 33 | } 34 | 35 | /// The possible error outcomes when attempting to read a message. 36 | #[derive(Debug)] 37 | pub enum ReadError { 38 | /// An error occurred in the underlying stream 39 | Io(io::Error), 40 | /// The message was not valid JSON. 41 | Json(JsonError), 42 | /// The message was not a JSON object. 43 | NotObject, 44 | /// The the method and params were not recognized by the handler. 45 | UnknownRequest(JsonError), 46 | /// The peer closed the connection. 47 | Disconnect, 48 | } 49 | 50 | /// Errors that can be received from the other side of the RPC channel. 51 | /// 52 | /// This type is intended to go over the wire. And by convention 53 | /// should `Serialize` as a JSON object with "code", "message", 54 | /// and optionally "data" fields. 55 | /// 56 | /// The xi RPC protocol defines one error: `RemoteError::InvalidRequest`, 57 | /// represented by error code `-32600`; however codes in the range 58 | /// `-32700 ... -32000` (inclusive) are reserved for compatability with 59 | /// the JSON-RPC spec. 60 | /// 61 | /// # Examples 62 | /// 63 | /// An invalid request: 64 | /// 65 | /// ``` 66 | /// # extern crate xi_rpc; 67 | /// # extern crate serde_json; 68 | /// use xi_rpc::RemoteError; 69 | /// use serde_json::Value; 70 | /// 71 | /// let json = r#"{ 72 | /// "code": -32600, 73 | /// "message": "Invalid request", 74 | /// "data": "Additional details" 75 | /// }"#; 76 | /// 77 | /// let err = serde_json::from_str::(&json).unwrap(); 78 | /// assert_eq!(err, 79 | /// RemoteError::InvalidRequest( 80 | /// Some(Value::String("Additional details".into())))); 81 | /// ``` 82 | /// 83 | /// A custom error: 84 | /// 85 | /// ``` 86 | /// # extern crate xi_rpc; 87 | /// # extern crate serde_json; 88 | /// use xi_rpc::RemoteError; 89 | /// use serde_json::Value; 90 | /// 91 | /// let json = r#"{ 92 | /// "code": 404, 93 | /// "message": "Not Found" 94 | /// }"#; 95 | /// 96 | /// let err = serde_json::from_str::(&json).unwrap(); 97 | /// assert_eq!(err, RemoteError::custom(404, "Not Found", None)); 98 | /// ``` 99 | #[derive(Debug, Clone, PartialEq)] 100 | pub enum RemoteError { 101 | /// The JSON was valid, but was not a correctly formed request. 102 | /// 103 | /// This Error is used internally, and should not be returned by 104 | /// clients. 105 | InvalidRequest(Option), 106 | /// A custom error, defined by the client. 107 | Custom { 108 | code: i64, 109 | message: String, 110 | data: Option, 111 | }, 112 | /// An error that cannot be represented by an error object. 113 | /// 114 | /// This error is intended to accommodate clients that return arbitrary 115 | /// error values. It should not be used for new errors. 116 | Unknown(Value), 117 | } 118 | 119 | impl RemoteError { 120 | /// Creates a new custom error. 121 | pub fn custom(code: i64, message: S, data: V) -> Self 122 | where 123 | S: AsRef, 124 | V: Into>, 125 | { 126 | let message = message.as_ref().into(); 127 | let data = data.into(); 128 | RemoteError::Custom { 129 | code, 130 | message, 131 | data, 132 | } 133 | } 134 | } 135 | 136 | impl ReadError { 137 | /// Returns `true` iff this is the `ReadError::Disconnect` variant. 138 | pub fn is_disconnect(&self) -> bool { 139 | match *self { 140 | ReadError::Disconnect => true, 141 | _ => false, 142 | } 143 | } 144 | } 145 | 146 | impl fmt::Display for ReadError { 147 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 148 | match *self { 149 | ReadError::Io(ref err) => write!(f, "I/O Error: {:?}", err), 150 | ReadError::Json(ref err) => write!(f, "JSON Error: {:?}", err), 151 | ReadError::NotObject => write!(f, "JSON message was not an object."), 152 | ReadError::UnknownRequest(ref err) => write!(f, "Unknown request: {:?}", err), 153 | ReadError::Disconnect => write!(f, "Peer closed the connection."), 154 | } 155 | } 156 | } 157 | 158 | impl From for ReadError { 159 | fn from(err: JsonError) -> ReadError { 160 | ReadError::Json(err) 161 | } 162 | } 163 | 164 | impl From for ReadError { 165 | fn from(err: io::Error) -> ReadError { 166 | ReadError::Io(err) 167 | } 168 | } 169 | 170 | impl From for RemoteError { 171 | fn from(err: JsonError) -> RemoteError { 172 | RemoteError::InvalidRequest(Some(json!(err.to_string()))) 173 | } 174 | } 175 | 176 | impl From for Error { 177 | fn from(err: RemoteError) -> Error { 178 | Error::RemoteError(err) 179 | } 180 | } 181 | 182 | #[derive(Deserialize, Serialize)] 183 | struct ErrorHelper { 184 | code: i64, 185 | message: String, 186 | #[serde(skip_serializing_if = "Option::is_none")] 187 | data: Option, 188 | } 189 | 190 | impl<'de> Deserialize<'de> for RemoteError { 191 | fn deserialize(deserializer: D) -> Result 192 | where 193 | D: Deserializer<'de>, 194 | { 195 | let v = Value::deserialize(deserializer)?; 196 | let resp = match ErrorHelper::deserialize(&v) { 197 | Ok(resp) => resp, 198 | Err(_) => return Ok(RemoteError::Unknown(v)), 199 | }; 200 | 201 | Ok(match resp.code { 202 | -32600 => RemoteError::InvalidRequest(resp.data), 203 | _ => RemoteError::Custom { 204 | code: resp.code, 205 | message: resp.message, 206 | data: resp.data, 207 | }, 208 | }) 209 | } 210 | } 211 | 212 | impl Serialize for RemoteError { 213 | fn serialize(&self, serializer: S) -> Result 214 | where 215 | S: Serializer, 216 | { 217 | let (code, message, data) = match *self { 218 | RemoteError::InvalidRequest(ref d) => (-32600, "Invalid request", d), 219 | RemoteError::Custom { 220 | code, 221 | ref message, 222 | ref data, 223 | } => (code, message.as_ref(), data), 224 | RemoteError::Unknown(_) => panic!( 225 | "The 'Unknown' error variant is \ 226 | not intended for client use." 227 | ), 228 | }; 229 | let message = message.to_owned(); 230 | let data = data.to_owned(); 231 | let err = ErrorHelper { 232 | code, 233 | message, 234 | data, 235 | }; 236 | err.serialize(serializer) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /rpc/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Generic RPC handling (used for both front end and plugin communication). 16 | //! 17 | //! The RPC protocol is based on [JSON-RPC](http://www.jsonrpc.org/specification), 18 | //! but with some modifications. Unlike JSON-RPC 2.0, requests and notifications 19 | //! are allowed in both directions, rather than imposing client and server roles. 20 | //! Further, the batch form is not supported. 21 | //! 22 | //! Because these changes make the protocol not fully compliant with the spec, 23 | //! the `"jsonrpc"` member is omitted from request and response objects. 24 | #![allow(clippy::boxed_local, clippy::or_fun_call)] 25 | 26 | #[macro_use] 27 | extern crate serde_json; 28 | #[macro_use] 29 | extern crate serde_derive; 30 | extern crate crossbeam_utils; 31 | extern crate serde; 32 | extern crate xi_trace; 33 | 34 | #[macro_use] 35 | extern crate log; 36 | 37 | mod error; 38 | mod parse; 39 | 40 | pub mod test_utils; 41 | 42 | use std::cmp; 43 | use std::collections::{BTreeMap, BinaryHeap, VecDeque}; 44 | use std::io::{self, BufRead, Write}; 45 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 46 | use std::sync::mpsc; 47 | use std::sync::{Arc, Condvar, Mutex}; 48 | use std::thread; 49 | use std::time::{Duration, Instant}; 50 | 51 | use serde::de::DeserializeOwned; 52 | use serde_json::Value; 53 | 54 | use xi_trace::{trace, trace_block, trace_block_payload, trace_payload}; 55 | 56 | pub use crate::error::{Error, ReadError, RemoteError}; 57 | use crate::parse::{Call, MessageReader, Response, RpcObject}; 58 | 59 | /// The maximum duration we will block on a reader before checking for an task. 60 | const MAX_IDLE_WAIT: Duration = Duration::from_millis(5); 61 | 62 | /// An interface to access the other side of the RPC channel. The main purpose 63 | /// is to send RPC requests and notifications to the peer. 64 | /// 65 | /// A single shared `RawPeer` exists for each `RpcLoop`; a reference can 66 | /// be taken with `RpcLoop::get_peer()`. 67 | /// 68 | /// In general, `RawPeer` shouldn't be used directly, but behind a pointer as 69 | /// the `Peer` trait object. 70 | pub struct RawPeer(Arc>); 71 | 72 | /// The `Peer` trait represents the interface for the other side of the RPC 73 | /// channel. It is intended to be used behind a pointer, a trait object. 74 | pub trait Peer: Send + 'static { 75 | /// Used to implement `clone` in an object-safe way. 76 | /// For an explanation on this approach, see 77 | /// [this thread](https://users.rust-lang.org/t/solved-is-it-possible-to-clone-a-boxed-trait-object/1714/6). 78 | fn box_clone(&self) -> Box; 79 | /// Sends a notification (asynchronous RPC) to the peer. 80 | fn send_rpc_notification(&self, method: &str, params: &Value); 81 | /// Sends a request asynchronously, and the supplied callback will 82 | /// be called when the response arrives. 83 | /// 84 | /// `Callback` is an alias for `FnOnce(Result)`; it must 85 | /// be boxed because trait objects cannot use generic paramaters. 86 | fn send_rpc_request_async(&self, method: &str, params: &Value, f: Box); 87 | /// Sends a request (synchronous RPC) to the peer, and waits for the result. 88 | fn send_rpc_request(&self, method: &str, params: &Value) -> Result; 89 | /// Determines whether an incoming request (or notification) is 90 | /// pending. This is intended to reduce latency for bulk operations 91 | /// done in the background. 92 | fn request_is_pending(&self) -> bool; 93 | /// Adds a token to the idle queue. When the runloop is idle and the 94 | /// queue is not empty, the handler's `idle` fn will be called 95 | /// with the earliest added token. 96 | fn schedule_idle(&self, token: usize); 97 | /// Like `schedule_idle`, with the guarantee that the handler's `idle` 98 | /// fn will not be called _before_ the provided `Instant`. 99 | /// 100 | /// # Note 101 | /// 102 | /// This is not intended as a high-fidelity timer. Regular RPC messages 103 | /// will always take priority over an idle task. 104 | fn schedule_timer(&self, after: Instant, token: usize); 105 | } 106 | 107 | /// The `Peer` trait object. 108 | pub type RpcPeer = Box; 109 | 110 | pub struct RpcCtx { 111 | peer: RpcPeer, 112 | } 113 | 114 | #[derive(Debug, Clone, Serialize, Deserialize)] 115 | /// An RPC command. 116 | /// 117 | /// This type is used as a placeholder in various places, and can be 118 | /// used by clients as a catchall type for implementing `MethodHandler`. 119 | pub struct RpcCall { 120 | pub method: String, 121 | pub params: Value, 122 | } 123 | 124 | /// A trait for types which can handle RPCs. 125 | /// 126 | /// Types which implement `MethodHandler` are also responsible for implementing 127 | /// `Parser`; `Parser` is provided when Self::Notification and Self::Request 128 | /// can be used with serde::DeserializeOwned. 129 | pub trait Handler { 130 | type Notification: DeserializeOwned; 131 | type Request: DeserializeOwned; 132 | fn handle_notification(&mut self, ctx: &RpcCtx, rpc: Self::Notification); 133 | fn handle_request(&mut self, ctx: &RpcCtx, rpc: Self::Request) -> Result; 134 | #[allow(unused_variables)] 135 | fn idle(&mut self, ctx: &RpcCtx, token: usize) {} 136 | } 137 | 138 | pub trait Callback: Send { 139 | fn call(self: Box, result: Result); 140 | } 141 | 142 | impl)> Callback for F { 143 | fn call(self: Box, result: Result) { 144 | (*self)(result) 145 | } 146 | } 147 | 148 | /// A helper type which shuts down the runloop if a panic occurs while 149 | /// handling an RPC. 150 | struct PanicGuard<'a, W: Write + 'static>(&'a RawPeer); 151 | 152 | impl<'a, W: Write + 'static> Drop for PanicGuard<'a, W> { 153 | fn drop(&mut self) { 154 | if thread::panicking() { 155 | error!("panic guard hit, closing runloop"); 156 | self.0.disconnect(); 157 | } 158 | } 159 | } 160 | 161 | trait IdleProc: Send { 162 | fn call(self: Box, token: usize); 163 | } 164 | 165 | impl IdleProc for F { 166 | fn call(self: Box, token: usize) { 167 | (*self)(token) 168 | } 169 | } 170 | 171 | enum ResponseHandler { 172 | Chan(mpsc::Sender>), 173 | Callback(Box), 174 | } 175 | 176 | impl ResponseHandler { 177 | fn invoke(self, result: Result) { 178 | match self { 179 | ResponseHandler::Chan(tx) => { 180 | let _ = tx.send(result); 181 | } 182 | ResponseHandler::Callback(f) => f.call(result), 183 | } 184 | } 185 | } 186 | 187 | #[derive(Debug, PartialEq, Eq)] 188 | struct Timer { 189 | fire_after: Instant, 190 | token: usize, 191 | } 192 | 193 | struct RpcState { 194 | rx_queue: Mutex>>, 195 | rx_cvar: Condvar, 196 | writer: Mutex, 197 | id: AtomicUsize, 198 | pending: Mutex>, 199 | idle_queue: Mutex>, 200 | timers: Mutex>, 201 | needs_exit: AtomicBool, 202 | is_blocked: AtomicBool, 203 | } 204 | 205 | /// A structure holding the state of a main loop for handling RPC's. 206 | pub struct RpcLoop { 207 | reader: MessageReader, 208 | peer: RawPeer, 209 | } 210 | 211 | impl RpcLoop { 212 | /// Creates a new `RpcLoop` with the given output stream (which is used for 213 | /// sending requests and notifications, as well as responses). 214 | pub fn new(writer: W) -> Self { 215 | let rpc_peer = RawPeer(Arc::new(RpcState { 216 | rx_queue: Mutex::new(VecDeque::new()), 217 | rx_cvar: Condvar::new(), 218 | writer: Mutex::new(writer), 219 | id: AtomicUsize::new(0), 220 | pending: Mutex::new(BTreeMap::new()), 221 | idle_queue: Mutex::new(VecDeque::new()), 222 | timers: Mutex::new(BinaryHeap::new()), 223 | needs_exit: AtomicBool::new(false), 224 | is_blocked: AtomicBool::new(false), 225 | })); 226 | RpcLoop { 227 | reader: MessageReader::default(), 228 | peer: rpc_peer, 229 | } 230 | } 231 | 232 | /// Gets a reference to the peer. 233 | pub fn get_raw_peer(&self) -> RawPeer { 234 | self.peer.clone() 235 | } 236 | 237 | /// Starts the event loop, reading lines from the reader until EOF, 238 | /// or an error occurs. 239 | /// 240 | /// Returns `Ok()` in the EOF case, otherwise returns the 241 | /// underlying `ReadError`. 242 | /// 243 | /// # Note: 244 | /// The reader is supplied via a closure, as basically a workaround 245 | /// so that the reader doesn't have to be `Send`. Internally, the 246 | /// main loop starts a separate thread for I/O, and at startup that 247 | /// thread calls the given closure. 248 | /// 249 | /// Calls to the handler happen on the caller's thread. 250 | /// 251 | /// Calls to the handler are guaranteed to preserve the order as 252 | /// they appear on on the channel. At the moment, there is no way 253 | /// for there to be more than one incoming request to be outstanding. 254 | pub fn mainloop(&mut self, rf: RF, handler: &mut H) -> Result<(), ReadError> 255 | where 256 | R: BufRead, 257 | RF: Send + FnOnce() -> R, 258 | H: Handler, 259 | { 260 | let exit = crossbeam_utils::thread::scope(|scope| { 261 | let peer = self.get_raw_peer(); 262 | peer.reset_needs_exit(); 263 | 264 | let ctx = RpcCtx { 265 | peer: Box::new(peer.clone()), 266 | }; 267 | scope.spawn(move |_| { 268 | let mut stream = rf(); 269 | loop { 270 | // The main thread cannot return while this thread is active; 271 | // when the main thread wants to exit it sets this flag. 272 | if self.peer.needs_exit() { 273 | trace("read loop exit", &["rpc"]); 274 | break; 275 | } 276 | 277 | let json = match self.reader.next(&mut stream) { 278 | Ok(json) => json, 279 | Err(err) => { 280 | if self.peer.0.is_blocked.load(Ordering::Acquire) { 281 | error!("failed to parse response json: {}", err); 282 | self.peer.disconnect(); 283 | } 284 | self.peer.put_rx(Err(err)); 285 | break; 286 | } 287 | }; 288 | if json.is_response() { 289 | let id = json.get_id().unwrap(); 290 | let _resp = 291 | trace_block_payload("read loop response", &["rpc"], format!("{}", id)); 292 | match json.into_response() { 293 | Ok(resp) => { 294 | let resp = resp.map_err(Error::from); 295 | self.peer.handle_response(id, resp); 296 | } 297 | Err(msg) => { 298 | error!("failed to parse response: {}", msg); 299 | self.peer.handle_response(id, Err(Error::InvalidResponse)); 300 | } 301 | } 302 | } else { 303 | self.peer.put_rx(Ok(json)); 304 | } 305 | } 306 | }); 307 | 308 | loop { 309 | let _guard = PanicGuard(&peer); 310 | let read_result = next_read(&peer, handler, &ctx); 311 | let _trace = trace_block("main got msg", &["rpc"]); 312 | 313 | let json = match read_result { 314 | Ok(json) => json, 315 | Err(err) => { 316 | trace_payload("main loop err", &["rpc"], err.to_string()); 317 | // finish idle work before disconnecting; 318 | // this is mostly useful for integration tests. 319 | if let Some(idle_token) = peer.try_get_idle() { 320 | handler.idle(&ctx, idle_token); 321 | } 322 | peer.disconnect(); 323 | return err; 324 | } 325 | }; 326 | 327 | let method = json.get_method().map(String::from); 328 | match method { 329 | None => { 330 | info!("json: {}", json!(json)) 331 | }, 332 | Some(_) => { 333 | match json.into_rpc::() { 334 | Ok(Call::Request(id, cmd)) => { 335 | let _t = trace_block_payload("handle request", &["rpc"], method.unwrap()); 336 | let result = handler.handle_request(&ctx, cmd); 337 | peer.respond(result, id); 338 | } 339 | Ok(Call::Notification(cmd)) => { 340 | let _t = trace_block_payload("handle notif", &["rpc"], method.unwrap()); 341 | handler.handle_notification(&ctx, cmd); 342 | } 343 | Ok(Call::InvalidRequest(id, err)) => peer.respond(Err(err), id), 344 | Err(err) => { 345 | trace_payload("read loop exit", &["rpc"], err.to_string()); 346 | peer.disconnect(); 347 | return ReadError::UnknownRequest(err); 348 | } 349 | } 350 | }, 351 | } 352 | } 353 | }) 354 | .unwrap(); 355 | 356 | if exit.is_disconnect() { 357 | Ok(()) 358 | } else { 359 | Err(exit) 360 | } 361 | } 362 | } 363 | 364 | /// Returns the next read result, checking for idle work when no 365 | /// result is available. 366 | fn next_read(peer: &RawPeer, handler: &mut H, ctx: &RpcCtx) -> Result 367 | where 368 | W: Write + Send, 369 | H: Handler, 370 | { 371 | loop { 372 | if let Some(result) = peer.try_get_rx() { 373 | return result; 374 | } 375 | // handle timers before general idle work 376 | let time_to_next_timer = match peer.check_timers() { 377 | Some(Ok(token)) => { 378 | do_idle(handler, ctx, token); 379 | continue; 380 | } 381 | Some(Err(duration)) => Some(duration), 382 | None => None, 383 | }; 384 | 385 | if let Some(idle_token) = peer.try_get_idle() { 386 | do_idle(handler, ctx, idle_token); 387 | continue; 388 | } 389 | 390 | // we don't want to block indefinitely if there's no current idle work, 391 | // because idle work could be scheduled from another thread. 392 | let idle_timeout = time_to_next_timer 393 | .unwrap_or(MAX_IDLE_WAIT) 394 | .min(MAX_IDLE_WAIT); 395 | 396 | if let Some(result) = peer.get_rx_timeout(idle_timeout) { 397 | return result; 398 | } 399 | } 400 | } 401 | 402 | fn do_idle(handler: &mut H, ctx: &RpcCtx, token: usize) { 403 | let _trace = trace_block_payload("do_idle", &["rpc"], format!("token: {}", token)); 404 | handler.idle(ctx, token); 405 | } 406 | 407 | impl RpcCtx { 408 | pub fn get_peer(&self) -> &RpcPeer { 409 | &self.peer 410 | } 411 | 412 | /// Schedule the idle handler to be run when there are no requests pending. 413 | pub fn schedule_idle(&self, token: usize) { 414 | self.peer.schedule_idle(token) 415 | } 416 | } 417 | 418 | impl Peer for RawPeer { 419 | fn box_clone(&self) -> Box { 420 | Box::new((*self).clone()) 421 | } 422 | 423 | fn send_rpc_notification(&self, method: &str, params: &Value) { 424 | let _trace = trace_block_payload("send notif", &["rpc"], method.to_owned()); 425 | if let Err(e) = self.send(&json!({ 426 | "method": method, 427 | "params": params, 428 | })) { 429 | error!( 430 | "send error on send_rpc_notification method {}: {}", 431 | method, e 432 | ); 433 | } 434 | } 435 | 436 | fn send_rpc_request_async(&self, method: &str, params: &Value, f: Box) { 437 | let _trace = trace_block_payload("send req async", &["rpc"], method.to_owned()); 438 | self.send_rpc_request_common(method, params, ResponseHandler::Callback(f)); 439 | } 440 | 441 | fn send_rpc_request(&self, method: &str, params: &Value) -> Result { 442 | let _trace = trace_block_payload("send req sync", &["rpc"], method.to_owned()); 443 | self.0.is_blocked.store(true, Ordering::Release); 444 | let (tx, rx) = mpsc::channel(); 445 | self.send_rpc_request_common(method, params, ResponseHandler::Chan(tx)); 446 | rx.recv().unwrap_or(Err(Error::PeerDisconnect)) 447 | } 448 | 449 | fn request_is_pending(&self) -> bool { 450 | let queue = self.0.rx_queue.lock().unwrap(); 451 | !queue.is_empty() 452 | } 453 | 454 | fn schedule_idle(&self, token: usize) { 455 | self.0.idle_queue.lock().unwrap().push_back(token); 456 | } 457 | 458 | fn schedule_timer(&self, after: Instant, token: usize) { 459 | self.0.timers.lock().unwrap().push(Timer { 460 | fire_after: after, 461 | token, 462 | }); 463 | } 464 | } 465 | 466 | impl RawPeer { 467 | fn send(&self, v: &Value) -> Result<(), io::Error> { 468 | let _trace = trace_block("send", &["rpc"]); 469 | let mut s = serde_json::to_string(v).unwrap(); 470 | s.push('\n'); 471 | self.0.writer.lock().unwrap().write_all(s.as_bytes()) 472 | // Technically, maybe we should flush here, but doesn't seem to be required. 473 | } 474 | 475 | fn respond(&self, result: Response, id: u64) { 476 | let mut response = json!({ "id": id }); 477 | match result { 478 | Ok(result) => response["result"] = result, 479 | Err(error) => response["error"] = json!(error), 480 | }; 481 | if let Err(e) = self.send(&response) { 482 | error!("error {} sending response to RPC {:?}", e, id); 483 | } 484 | } 485 | 486 | fn send_rpc_request_common(&self, method: &str, params: &Value, rh: ResponseHandler) { 487 | let id = self.0.id.fetch_add(1, Ordering::Relaxed); 488 | { 489 | let mut pending = self.0.pending.lock().unwrap(); 490 | pending.insert(id, rh); 491 | } 492 | if let Err(e) = self.send(&json!({ 493 | "id": id, 494 | "method": method, 495 | "params": params, 496 | })) { 497 | let mut pending = self.0.pending.lock().unwrap(); 498 | if let Some(rh) = pending.remove(&id) { 499 | rh.invoke(Err(Error::Io(e))); 500 | } 501 | } 502 | } 503 | 504 | fn handle_response(&self, id: u64, resp: Result) { 505 | let id = id as usize; 506 | let handler = { 507 | let mut pending = self.0.pending.lock().unwrap(); 508 | pending.remove(&id) 509 | }; 510 | match handler { 511 | Some(responsehandler) => responsehandler.invoke(resp), 512 | None => warn!("id {} not found in pending", id), 513 | } 514 | } 515 | 516 | /// Get a message from the receive queue if available. 517 | fn try_get_rx(&self) -> Option> { 518 | let mut queue = self.0.rx_queue.lock().unwrap(); 519 | queue.pop_front() 520 | } 521 | 522 | /// Get a message from the receive queue, waiting for at most `Duration` 523 | /// and returning `None` if no message is available. 524 | fn get_rx_timeout(&self, dur: Duration) -> Option> { 525 | let mut queue = self.0.rx_queue.lock().unwrap(); 526 | let result = self.0.rx_cvar.wait_timeout(queue, dur).unwrap(); 527 | queue = result.0; 528 | queue.pop_front() 529 | } 530 | 531 | /// Adds a message to the receive queue. The message should only 532 | /// be `None` if the read thread is exiting. 533 | fn put_rx(&self, json: Result) { 534 | let mut queue = self.0.rx_queue.lock().unwrap(); 535 | queue.push_back(json); 536 | self.0.rx_cvar.notify_one(); 537 | } 538 | 539 | fn try_get_idle(&self) -> Option { 540 | self.0.idle_queue.lock().unwrap().pop_front() 541 | } 542 | 543 | /// Checks status of the most imminent timer. If that timer has expired, 544 | /// returns `Some(Ok(_))`, with the corresponding token. 545 | /// If a timer exists but has not expired, returns `Some(Err(_))`, 546 | /// with the error value being the `Duration` until the timer is ready. 547 | /// Returns `None` if no timers are registered. 548 | fn check_timers(&self) -> Option> { 549 | let mut timers = self.0.timers.lock().unwrap(); 550 | match timers.peek() { 551 | None => return None, 552 | Some(t) => { 553 | let now = Instant::now(); 554 | if t.fire_after > now { 555 | return Some(Err(t.fire_after - now)); 556 | } 557 | } 558 | } 559 | Some(Ok(timers.pop().unwrap().token)) 560 | } 561 | 562 | /// send disconnect error to pending requests. 563 | fn disconnect(&self) { 564 | let mut pending = self.0.pending.lock().unwrap(); 565 | let ids = pending.keys().cloned().collect::>(); 566 | for id in &ids { 567 | let callback = pending.remove(id).unwrap(); 568 | callback.invoke(Err(Error::PeerDisconnect)); 569 | } 570 | self.0.needs_exit.store(true, Ordering::Relaxed); 571 | } 572 | 573 | /// Returns `true` if an error has occured in the main thread. 574 | fn needs_exit(&self) -> bool { 575 | self.0.needs_exit.load(Ordering::Relaxed) 576 | } 577 | 578 | fn reset_needs_exit(&self) { 579 | self.0.needs_exit.store(false, Ordering::SeqCst); 580 | } 581 | } 582 | 583 | impl Clone for Box { 584 | fn clone(&self) -> Box { 585 | self.box_clone() 586 | } 587 | } 588 | 589 | impl Clone for RawPeer { 590 | fn clone(&self) -> Self { 591 | RawPeer(self.0.clone()) 592 | } 593 | } 594 | 595 | //NOTE: for our timers to work with Rust's BinaryHeap we want to reverse 596 | //the default comparison; smaller `Instant`'s are considered 'greater'. 597 | impl Ord for Timer { 598 | fn cmp(&self, other: &Timer) -> cmp::Ordering { 599 | other.fire_after.cmp(&self.fire_after) 600 | } 601 | } 602 | 603 | impl PartialOrd for Timer { 604 | fn partial_cmp(&self, other: &Timer) -> Option { 605 | Some(self.cmp(other)) 606 | } 607 | } 608 | 609 | #[cfg(test)] 610 | mod tests { 611 | use super::*; 612 | 613 | #[test] 614 | fn test_parse_notif() { 615 | let reader = MessageReader::default(); 616 | let json = reader 617 | .parse(r#"{"method": "hi", "params": {"words": "plz"}}"#) 618 | .unwrap(); 619 | assert!(!json.is_response()); 620 | let rpc = json.into_rpc::().unwrap(); 621 | match rpc { 622 | Call::Notification(_) => (), 623 | _ => panic!("parse failed"), 624 | } 625 | } 626 | 627 | #[test] 628 | fn test_parse_req() { 629 | let reader = MessageReader::default(); 630 | let json = reader 631 | .parse(r#"{"id": 5, "method": "hi", "params": {"words": "plz"}}"#) 632 | .unwrap(); 633 | assert!(!json.is_response()); 634 | let rpc = json.into_rpc::().unwrap(); 635 | match rpc { 636 | Call::Request(..) => (), 637 | _ => panic!("parse failed"), 638 | } 639 | } 640 | 641 | #[test] 642 | fn test_parse_bad_json() { 643 | // missing "" around params 644 | let reader = MessageReader::default(); 645 | let json = reader 646 | .parse(r#"{"id": 5, "method": "hi", params: {"words": "plz"}}"#) 647 | .err() 648 | .unwrap(); 649 | 650 | match json { 651 | ReadError::Json(..) => (), 652 | _ => panic!("parse failed"), 653 | } 654 | // not an object 655 | let json = reader.parse(r#"[5, "hi", {"arg": "val"}]"#).err().unwrap(); 656 | 657 | match json { 658 | ReadError::NotObject => (), 659 | _ => panic!("parse failed"), 660 | } 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /rpc/src/parse.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Parsing of raw JSON messages into RPC objects. 16 | 17 | use std::io::BufRead; 18 | 19 | use serde::de::DeserializeOwned; 20 | use serde_json::{Error as JsonError, Value}; 21 | 22 | use crate::error::{ReadError, RemoteError}; 23 | 24 | /// A unique identifier attached to request RPCs. 25 | type RequestId = u64; 26 | 27 | /// An RPC response, received from the peer. 28 | pub type Response = Result; 29 | 30 | /// Reads and parses RPC messages from a stream, maintaining an 31 | /// internal buffer. 32 | #[derive(Debug, Default)] 33 | pub struct MessageReader(String); 34 | 35 | /// An internal type used during initial JSON parsing. 36 | /// 37 | /// Wraps an arbitrary JSON object, which may be any valid or invalid 38 | /// RPC message. This allows initial parsing and response handling to 39 | /// occur on the read thread. If the message looks like a request, it 40 | /// is passed to the main thread for handling. 41 | #[derive(Debug, Clone, Serialize)] 42 | pub struct RpcObject(pub Value); 43 | 44 | #[derive(Debug, Clone, PartialEq)] 45 | /// An RPC call, which may be either a notification or a request. 46 | pub enum Call { 47 | /// An id and an RPC Request 48 | Request(RequestId, R), 49 | /// An RPC Notification 50 | Notification(N), 51 | /// A malformed request: the request contained an id, but could 52 | /// not be parsed. The client will receive an error. 53 | InvalidRequest(RequestId, RemoteError), 54 | } 55 | 56 | impl MessageReader { 57 | /// Attempts to read the next line from the stream and parse it as 58 | /// an RPC object. 59 | /// 60 | /// # Errors 61 | /// 62 | /// This function will return an error if there is an underlying 63 | /// I/O error, if the stream is closed, or if the message is not 64 | /// a valid JSON object. 65 | pub fn next(&mut self, reader: &mut R) -> Result { 66 | self.0.clear(); 67 | let _ = reader.read_line(&mut self.0)?; 68 | if self.0.is_empty() { 69 | Err(ReadError::Disconnect) 70 | } else { 71 | self.parse(&self.0) 72 | } 73 | } 74 | 75 | /// Attempts to parse a &str as an RPC Object. 76 | /// 77 | /// This should not be called directly unless you are writing tests. 78 | #[doc(hidden)] 79 | pub fn parse(&self, s: &str) -> Result { 80 | let _trace = xi_trace::trace_block("parse", &["rpc"]); 81 | let val = serde_json::from_str::(&s)?; 82 | if !val.is_object() { 83 | Err(ReadError::NotObject) 84 | } else { 85 | Ok(val.into()) 86 | } 87 | } 88 | } 89 | 90 | impl RpcObject { 91 | /// Returns the 'id' of the underlying object, if present. 92 | pub fn get_id(&self) -> Option { 93 | self.0.get("id").and_then(Value::as_u64) 94 | } 95 | 96 | /// Returns the 'method' field of the underlying object, if present. 97 | pub fn get_method(&self) -> Option<&str> { 98 | self.0.get("method").and_then(Value::as_str) 99 | } 100 | 101 | /// Returns `true` if this object looks like an RPC response; 102 | /// that is, if it has an 'id' field and does _not_ have a 'method' 103 | /// field. 104 | pub fn is_response(&self) -> bool { 105 | self.0.get("id").is_some() && self.0.get("method").is_none() 106 | } 107 | 108 | /// Attempts to convert the underlying `Value` into an RPC response 109 | /// object, and returns the result. 110 | /// 111 | /// The caller is expected to verify that the object is a response 112 | /// before calling this method. 113 | /// 114 | /// # Errors 115 | /// 116 | /// If the `Value` is not a well formed response object, this will 117 | /// return a `String` containing an error message. The caller should 118 | /// print this message and exit. 119 | pub fn into_response(mut self) -> Result { 120 | let _ = self 121 | .get_id() 122 | .ok_or("Response requires 'id' field.".to_string())?; 123 | 124 | if self.0.get("result").is_some() == self.0.get("error").is_some() { 125 | return Err("RPC response must contain exactly one of\ 126 | 'error' or 'result' fields." 127 | .into()); 128 | } 129 | let result = self.0.as_object_mut().and_then(|obj| obj.remove("result")); 130 | 131 | match result { 132 | Some(r) => Ok(Ok(r)), 133 | None => { 134 | let error = self 135 | .0 136 | .as_object_mut() 137 | .and_then(|obj| obj.remove("error")) 138 | .unwrap(); 139 | match serde_json::from_value::(error) { 140 | Ok(e) => Ok(Err(e)), 141 | Err(e) => Err(format!("Error handling response: {:?}", e)), 142 | } 143 | } 144 | } 145 | } 146 | 147 | /// Attempts to convert the underlying `Value` into either an RPC 148 | /// notification or request. 149 | /// 150 | /// # Errors 151 | /// 152 | /// Returns a `serde_json::Error` if the `Value` cannot be converted 153 | /// to one of the expected types. 154 | pub fn into_rpc(self) -> Result, JsonError> 155 | where 156 | N: DeserializeOwned, 157 | R: DeserializeOwned, 158 | { 159 | let id = self.get_id(); 160 | match id { 161 | Some(id) => match serde_json::from_value::(self.0) { 162 | Ok(resp) => Ok(Call::Request(id, resp)), 163 | Err(err) => Ok(Call::InvalidRequest(id, err.into())), 164 | }, 165 | None => { 166 | let result = serde_json::from_value::(self.0)?; 167 | Ok(Call::Notification(result)) 168 | } 169 | } 170 | } 171 | } 172 | 173 | impl From for RpcObject { 174 | fn from(v: Value) -> RpcObject { 175 | RpcObject(v) 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | 182 | use super::*; 183 | use serde_json; 184 | 185 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 186 | #[serde(rename_all = "snake_case")] 187 | #[serde(tag = "method", content = "params")] 188 | enum TestR { 189 | NewView { file_path: Option }, 190 | OldView { file_path: usize }, 191 | } 192 | 193 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 194 | #[serde(rename_all = "snake_case")] 195 | #[serde(tag = "method", content = "params")] 196 | enum TestN { 197 | CloseView { view_id: String }, 198 | Save { view_id: String, file_path: String }, 199 | } 200 | 201 | #[test] 202 | fn request_success() { 203 | let json = r#"{"id":0,"method":"new_view","params":{}}"#; 204 | let p: RpcObject = serde_json::from_str::(json).unwrap().into(); 205 | assert!(!p.is_response()); 206 | let req = p.into_rpc::().unwrap(); 207 | assert_eq!(req, Call::Request(0, TestR::NewView { file_path: None })); 208 | } 209 | 210 | #[test] 211 | fn request_failure() { 212 | // method does not exist 213 | let json = r#"{"id":0,"method":"new_truth","params":{}}"#; 214 | let p: RpcObject = serde_json::from_str::(json).unwrap().into(); 215 | let req = p.into_rpc::().unwrap(); 216 | let is_ok = match req { 217 | Call::InvalidRequest(0, _) => true, 218 | _ => false, 219 | }; 220 | if !is_ok { 221 | panic!("{:?}", req); 222 | } 223 | } 224 | 225 | #[test] 226 | fn notif_with_id() { 227 | // method is a notification, should not have ID 228 | let json = r#"{"id":0,"method":"close_view","params":{"view_id": "view-id-1"}}"#; 229 | let p: RpcObject = serde_json::from_str::(json).unwrap().into(); 230 | let req = p.into_rpc::().unwrap(); 231 | let is_ok = match req { 232 | Call::InvalidRequest(0, _) => true, 233 | _ => false, 234 | }; 235 | if !is_ok { 236 | panic!("{:?}", req); 237 | } 238 | } 239 | 240 | #[test] 241 | fn test_resp_err() { 242 | let json = r#"{"id":5,"error":{"code":420, "message":"chill out"}}"#; 243 | let p: RpcObject = serde_json::from_str::(json).unwrap().into(); 244 | assert!(p.is_response()); 245 | let resp = p.into_response().unwrap(); 246 | assert_eq!(resp, Err(RemoteError::custom(420, "chill out", None))); 247 | } 248 | 249 | #[test] 250 | fn test_resp_result() { 251 | let json = r#"{"id":5,"result":"success!"}"#; 252 | let p: RpcObject = serde_json::from_str::(json).unwrap().into(); 253 | assert!(p.is_response()); 254 | let resp = p.into_response().unwrap(); 255 | assert_eq!(resp, Ok(json!("success!"))); 256 | } 257 | 258 | #[test] 259 | fn test_err() { 260 | let json = r#"{"code": -32600, "message": "Invalid Request"}"#; 261 | let e = serde_json::from_str::(json).unwrap(); 262 | assert_eq!(e, RemoteError::InvalidRequest(None)); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /rpc/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Types and helpers used for testing. 16 | 17 | use std::io::{self, Cursor, Write}; 18 | use std::sync::mpsc::{channel, Receiver, Sender}; 19 | use std::time::{Duration, Instant}; 20 | 21 | use serde_json::{self, Value}; 22 | 23 | use super::{Callback, Error, MessageReader, Peer, ReadError, Response, RpcObject}; 24 | 25 | /// Wraps an instance of `mpsc::Sender`, implementing `Write`. 26 | /// 27 | /// This lets the tx side of an mpsc::channel serve as the destination 28 | /// stream for an RPC loop. 29 | pub struct DummyWriter(Sender); 30 | 31 | /// Wraps an instance of `mpsc::Receiver`, providing convenience methods 32 | /// for parsing received messages. 33 | pub struct DummyReader(MessageReader, Receiver); 34 | 35 | /// An Peer that doesn't do anything. 36 | #[derive(Debug, Clone)] 37 | pub struct DummyPeer; 38 | 39 | /// Returns a `(DummyWriter, DummyReader)` pair. 40 | pub fn test_channel() -> (DummyWriter, DummyReader) { 41 | let (tx, rx) = channel(); 42 | (DummyWriter(tx), DummyReader(MessageReader::default(), rx)) 43 | } 44 | 45 | /// Given a string type, returns a `Cursor>`, which implements 46 | /// `BufRead`. 47 | pub fn make_reader>(s: S) -> Cursor> { 48 | Cursor::new(s.as_ref().as_bytes().to_vec()) 49 | } 50 | 51 | impl DummyReader { 52 | /// Attempts to read a message, returning `None` if the wait exceeds 53 | /// `timeout`. 54 | /// 55 | /// This method makes no assumptions about the contents of the 56 | /// message, and does no error handling. 57 | pub fn next_timeout(&mut self, timeout: Duration) -> Option> { 58 | self.1.recv_timeout(timeout).ok().map(|s| self.0.parse(&s)) 59 | } 60 | 61 | /// Reads and parses a response object. 62 | /// 63 | /// # Panics 64 | /// 65 | /// Panics if a non-response message is received, or if no message 66 | /// is received after a reasonable time. 67 | pub fn expect_response(&mut self) -> Response { 68 | let raw = self 69 | .next_timeout(Duration::from_secs(1)) 70 | .expect("response should be received"); 71 | let val = raw.as_ref().ok().map(|v| serde_json::to_string(&v.0)); 72 | let resp = raw 73 | .map_err(|e| e.to_string()) 74 | .and_then(|r| r.into_response()); 75 | 76 | match resp { 77 | Err(msg) => panic!("Bad response: {:?}. {}", val, msg), 78 | Ok(resp) => resp, 79 | } 80 | } 81 | 82 | pub fn expect_object(&mut self) -> RpcObject { 83 | self.next_timeout(Duration::from_secs(1)) 84 | .expect("expected object") 85 | .unwrap() 86 | } 87 | 88 | pub fn expect_rpc(&mut self, method: &str) -> RpcObject { 89 | let obj = self 90 | .next_timeout(Duration::from_secs(1)) 91 | .unwrap_or_else(|| panic!("expected rpc \"{}\"", method)) 92 | .unwrap(); 93 | assert_eq!(obj.get_method(), Some(method)); 94 | obj 95 | } 96 | 97 | pub fn expect_nothing(&mut self) { 98 | if let Some(thing) = self.next_timeout(Duration::from_millis(500)) { 99 | panic!("unexpected something {:?}", thing); 100 | } 101 | } 102 | } 103 | 104 | impl Write for DummyWriter { 105 | fn write(&mut self, buf: &[u8]) -> io::Result { 106 | let s = String::from_utf8(buf.to_vec()).unwrap(); 107 | self.0 108 | .send(s) 109 | .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err))) 110 | .map(|_| buf.len()) 111 | } 112 | 113 | fn flush(&mut self) -> io::Result<()> { 114 | Ok(()) 115 | } 116 | } 117 | 118 | impl Peer for DummyPeer { 119 | fn box_clone(&self) -> Box { 120 | Box::new(self.clone()) 121 | } 122 | fn send_rpc_notification(&self, _method: &str, _params: &Value) {} 123 | fn send_rpc_request_async(&self, _method: &str, _params: &Value, f: Box) { 124 | f.call(Ok("dummy peer".into())) 125 | } 126 | fn send_rpc_request(&self, _method: &str, _params: &Value) -> Result { 127 | Ok("dummy peer".into()) 128 | } 129 | fn request_is_pending(&self) -> bool { 130 | false 131 | } 132 | fn schedule_idle(&self, _token: usize) {} 133 | fn schedule_timer(&self, _time: Instant, _token: usize) {} 134 | } 135 | -------------------------------------------------------------------------------- /rpc/tests/integration.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | #[macro_use] 15 | extern crate serde_json; 16 | extern crate xi_rpc; 17 | 18 | use std::io; 19 | use std::time::Duration; 20 | 21 | use serde_json::Value; 22 | use xi_rpc::test_utils::{make_reader, test_channel}; 23 | use xi_rpc::{Handler, ReadError, RemoteError, RpcCall, RpcCtx, RpcLoop}; 24 | 25 | /// Handler that responds to requests with whatever params they sent. 26 | pub struct EchoHandler; 27 | 28 | #[allow(unused)] 29 | impl Handler for EchoHandler { 30 | type Notification = RpcCall; 31 | type Request = RpcCall; 32 | fn handle_notification(&mut self, ctx: &RpcCtx, rpc: Self::Notification) {} 33 | fn handle_request(&mut self, ctx: &RpcCtx, rpc: Self::Request) -> Result { 34 | Ok(rpc.params) 35 | } 36 | } 37 | 38 | #[test] 39 | fn test_recv_notif() { 40 | // we should not reply to a well formed notification 41 | let mut handler = EchoHandler; 42 | let (tx, mut rx) = test_channel(); 43 | let mut rpc_looper = RpcLoop::new(tx); 44 | let r = make_reader(r#"{"method": "hullo", "params": {"words": "plz"}}"#); 45 | assert!(rpc_looper.mainloop(|| r, &mut handler).is_ok()); 46 | let resp = rx.next_timeout(Duration::from_millis(100)); 47 | assert!(resp.is_none()); 48 | } 49 | 50 | #[test] 51 | fn test_recv_resp() { 52 | // we should reply to a well formed request 53 | let mut handler = EchoHandler; 54 | let (tx, mut rx) = test_channel(); 55 | let mut rpc_looper = RpcLoop::new(tx); 56 | let r = make_reader(r#"{"id": 1, "method": "hullo", "params": {"words": "plz"}}"#); 57 | assert!(rpc_looper.mainloop(|| r, &mut handler).is_ok()); 58 | let resp = rx.expect_response().unwrap(); 59 | assert_eq!(resp["words"], json!("plz")); 60 | // do it again 61 | let r = make_reader(r#"{"id": 0, "method": "hullo", "params": {"words": "yay"}}"#); 62 | assert!(rpc_looper.mainloop(|| r, &mut handler).is_ok()); 63 | let resp = rx.expect_response().unwrap(); 64 | assert_eq!(resp["words"], json!("yay")); 65 | } 66 | 67 | #[test] 68 | fn test_recv_error() { 69 | // a malformed request containing an ID should receive an error 70 | let mut handler = EchoHandler; 71 | let (tx, mut rx) = test_channel(); 72 | let mut rpc_looper = RpcLoop::new(tx); 73 | let r = 74 | make_reader(r#"{"id": 0, "method": "hullo","args": {"args": "should", "be": "params"}}"#); 75 | assert!(rpc_looper.mainloop(|| r, &mut handler).is_ok()); 76 | let resp = rx.expect_response(); 77 | assert!(resp.is_err(), "{:?}", resp); 78 | } 79 | 80 | #[test] 81 | fn test_bad_json_err() { 82 | // malformed json should cause the runloop to return an error. 83 | let mut handler = EchoHandler; 84 | let mut rpc_looper = RpcLoop::new(io::sink()); 85 | let r = make_reader(r#"this is not valid json"#); 86 | let exit = rpc_looper.mainloop(|| r, &mut handler); 87 | match exit { 88 | Err(ReadError::Json(_)) => (), 89 | Err(err) => panic!("Incorrect error: {:?}", err), 90 | Ok(()) => panic!("Expected an error"), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate chrono; 2 | extern crate dirs; 3 | extern crate fern; 4 | 5 | use std::fs; 6 | use std::io; 7 | use std::path::{Path, PathBuf}; 8 | use std::process; 9 | 10 | use log::{error, info, warn}; 11 | 12 | use core_lib::app::Stadal; 13 | use xi_rpc::RpcLoop; 14 | 15 | fn create_log_directory(path_with_file: &Path) -> io::Result<()> { 16 | let log_dir = path_with_file.parent().ok_or_else(|| io::Error::new( 17 | io::ErrorKind::InvalidInput, 18 | format!( 19 | "Unable to get the parent of the following Path: {}, Your path should contain a file name", 20 | path_with_file.display(), 21 | ), 22 | ))?; 23 | fs::create_dir_all(log_dir)?; 24 | Ok(()) 25 | } 26 | 27 | fn setup_logging(logging_path: Option<&Path>) -> Result<(), fern::InitError> { 28 | let level_filter = match std::env::var("XI_LOG") { 29 | Ok(level) => match level.to_lowercase().as_ref() { 30 | "trace" => log::LevelFilter::Trace, 31 | "debug" => log::LevelFilter::Debug, 32 | _ => log::LevelFilter::Info, 33 | }, 34 | // Default to info 35 | Err(_) => log::LevelFilter::Info, 36 | }; 37 | 38 | let mut fern_dispatch = fern::Dispatch::new() 39 | .format(|out, message, record| { 40 | out.finish(format_args!( 41 | "{}[{}][{}] {}", 42 | chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), 43 | record.target(), 44 | record.level(), 45 | message, 46 | )) 47 | }) 48 | .level(level_filter) 49 | .chain(io::stderr()); 50 | 51 | if let Some(logging_file_path) = logging_path { 52 | create_log_directory(logging_file_path)?; 53 | 54 | fern_dispatch = fern_dispatch.chain(fern::log_file(logging_file_path)?); 55 | }; 56 | 57 | // Start fern 58 | fern_dispatch.apply()?; 59 | info!("Logging with fern is set up"); 60 | 61 | // Log details of the logging_file_path result using fern/log 62 | // Either logging the path fern is outputting to or the error from obtaining the path 63 | match logging_path { 64 | Some(logging_file_path) => info!("Writing logs to: {}", logging_file_path.display()), 65 | None => warn!("No path was supplied for the log file. Not saving logs to disk, falling back to just stderr"), 66 | } 67 | Ok(()) 68 | } 69 | 70 | fn get_logging_directory_path>(directory: P) -> Result { 71 | match dirs::data_local_dir() { 72 | Some(mut log_dir) => { 73 | log_dir.push(directory); 74 | Ok(log_dir) 75 | } 76 | None => Err( 77 | io::Error::new( 78 | io::ErrorKind::NotFound, 79 | "No standard logging directory known for this platform", 80 | )) 81 | } 82 | } 83 | 84 | #[tokio::main] 85 | async fn main() { 86 | let mut state = Stadal::new(); 87 | let stdin = io::stdin(); 88 | let stdout = io::stdout(); 89 | let mut rpc_looper = RpcLoop::new(stdout); 90 | 91 | let mut directory_path = get_logging_directory_path(PathBuf::from("stadal")).unwrap(); 92 | directory_path.push(PathBuf::from("stadal.log")); 93 | 94 | if let Err(e) = setup_logging(Some(directory_path.as_path())) { 95 | eprintln!("[ERROR] setup_logging returned error, logging not enabled: {:?}", e); 96 | } 97 | 98 | match rpc_looper.mainloop(|| stdin.lock(), &mut state) { 99 | Ok(_) => (), 100 | Err(err) => { 101 | error!("exited with error:\n{:?}", err); 102 | process::exit(1); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /trace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xi-trace" 3 | version = "0.2.0" 4 | license = "Apache-2.0" 5 | authors = ["Vitali Lovich "] 6 | categories = ["development-tools::profiling"] 7 | repository = "https://github.com/google/xi-editor" 8 | description = "Library-based distributed tracing API to meet the needs of xi-core, frontends and plugins" 9 | edition = '2018' 10 | 11 | [features] 12 | benchmarks = [] 13 | default = ["chrome_trace_event"] 14 | json_payload = ["serde_json"] 15 | getpid = [] 16 | chrome_trace_event = ["serde_json"] 17 | ipc = ["bincode"] 18 | 19 | [dependencies] 20 | time = "0.1" 21 | lazy_static = "1.0" 22 | serde_json = { version = "1.0", optional = true } 23 | serde_derive = "1.0" 24 | serde = "1.0" 25 | libc = "0.2" 26 | log = "0.4.3" 27 | bincode = { version = "1.0", optional = true } 28 | -------------------------------------------------------------------------------- /trace/src/chrome_trace_dump.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![allow( 16 | clippy::if_same_then_else, 17 | clippy::needless_bool, 18 | clippy::needless_pass_by_value, 19 | clippy::ptr_arg 20 | )] 21 | 22 | #[cfg(all(test, feature = "benchmarks"))] 23 | extern crate test; 24 | 25 | use std::io::{Error as IOError, ErrorKind as IOErrorKind, Read, Write}; 26 | 27 | use super::Sample; 28 | 29 | #[derive(Debug)] 30 | pub enum Error { 31 | Io(IOError), 32 | Json(serde_json::Error), 33 | DecodingFormat(String), 34 | } 35 | 36 | impl From for Error { 37 | fn from(e: IOError) -> Error { 38 | Error::Io(e) 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(e: serde_json::Error) -> Error { 44 | Error::Json(e) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(e: String) -> Error { 50 | Error::DecodingFormat(e) 51 | } 52 | } 53 | 54 | impl Error { 55 | pub fn already_exists() -> Error { 56 | Error::Io(IOError::from(IOErrorKind::AlreadyExists)) 57 | } 58 | } 59 | 60 | #[derive(Clone, Debug, Deserialize)] 61 | #[serde(untagged)] 62 | enum ChromeTraceArrayEntries { 63 | Array(Vec), 64 | } 65 | 66 | /// This serializes the samples into the [Chrome trace event format](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwiJlZmDguXYAhUD4GMKHVmEDqIQFggpMAA&url=https%3A%2F%2Fdocs.google.com%2Fdocument%2Fd%2F1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU%2Fpreview&usg=AOvVaw0tBFlVbDVBikdzLqgrWK3g). 67 | /// 68 | /// # Arguments 69 | /// `samples` - Something that can be converted into an iterator of sample 70 | /// references. 71 | /// `format` - Which trace format to save the data in. There are four total 72 | /// formats described in the document. 73 | /// `output` - Where to write the serialized result. 74 | /// 75 | /// # Returns 76 | /// A `Result<(), Error>` that indicates if serialization was successful or the 77 | /// details of any error that occured. 78 | /// 79 | /// # Examples 80 | /// ```norun 81 | /// let samples = xi_trace::samples_cloned_sorted(); 82 | /// let mut serialized = Vec::::new(); 83 | /// serialize(samples.iter(), serialized); 84 | /// ``` 85 | pub fn serialize(samples: &Vec, output: W) -> Result<(), Error> 86 | where 87 | W: Write, 88 | { 89 | serde_json::to_writer(output, samples).map_err(Error::Json) 90 | } 91 | 92 | pub fn to_value(samples: &Vec) -> Result { 93 | serde_json::to_value(samples).map_err(Error::Json) 94 | } 95 | 96 | pub fn decode(samples: serde_json::Value) -> Result, Error> { 97 | serde_json::from_value(samples).map_err(Error::Json) 98 | } 99 | 100 | pub fn deserialize(input: R) -> Result, Error> 101 | where 102 | R: Read, 103 | { 104 | serde_json::from_reader(input).map_err(Error::Json) 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use super::*; 110 | #[cfg(feature = "json_payload")] 111 | use crate::TracePayloadT; 112 | #[cfg(feature = "benchmarks")] 113 | use test::Bencher; 114 | 115 | #[cfg(not(feature = "json_payload"))] 116 | fn to_payload(value: &'static str) -> &'static str { 117 | value 118 | } 119 | 120 | #[cfg(feature = "json_payload")] 121 | fn to_payload(value: &'static str) -> TracePayloadT { 122 | json!({ "test": value }) 123 | } 124 | 125 | #[cfg(feature = "chrome_trace_event")] 126 | #[test] 127 | fn test_chrome_trace_serialization() { 128 | use super::super::*; 129 | 130 | let trace = Trace::enabled(Config::with_limit_count(10)); 131 | trace.instant("sample1", &["test", "chrome"]); 132 | trace.instant_payload("sample2", &["test", "chrome"], to_payload("payload 2")); 133 | trace.instant_payload("sample3", &["test", "chrome"], to_payload("payload 3")); 134 | trace.closure_payload( 135 | "sample4", 136 | &["test", "chrome"], 137 | || { 138 | let _guard = trace.block("sample5", &["test,chrome"]); 139 | }, 140 | to_payload("payload 4"), 141 | ); 142 | 143 | let samples = trace.samples_cloned_unsorted(); 144 | 145 | let mut serialized = Vec::::new(); 146 | 147 | let result = serialize(&samples, &mut serialized); 148 | assert!(result.is_ok(), "{:?}", result); 149 | 150 | let decoded_result: Vec = serde_json::from_slice(&serialized).unwrap(); 151 | assert_eq!(decoded_result.len(), 8); 152 | assert_eq!(decoded_result[0]["name"].as_str().unwrap(), "process_name"); 153 | assert_eq!(decoded_result[1]["name"].as_str().unwrap(), "thread_name"); 154 | for i in 2..5 { 155 | assert_eq!(decoded_result[i]["name"].as_str().unwrap(), samples[i].name); 156 | assert_eq!(decoded_result[i]["cat"].as_str().unwrap(), "test,chrome"); 157 | assert_eq!(decoded_result[i]["ph"].as_str().unwrap(), "i"); 158 | assert_eq!(decoded_result[i]["ts"], samples[i].timestamp_us); 159 | let nth_sample = &samples[i]; 160 | let nth_args = nth_sample.args.as_ref().unwrap(); 161 | assert_eq!( 162 | decoded_result[i]["args"]["xi_payload"], 163 | json!(nth_args.payload.as_ref()) 164 | ); 165 | } 166 | assert_eq!(decoded_result[5]["ph"], "B"); 167 | assert_eq!(decoded_result[6]["ph"], "E"); 168 | assert_eq!(decoded_result[7]["ph"], "X"); 169 | } 170 | 171 | #[cfg(feature = "chrome_trace_event")] 172 | #[test] 173 | fn test_chrome_trace_deserialization() { 174 | use super::super::*; 175 | 176 | let trace = Trace::enabled(Config::with_limit_count(10)); 177 | trace.instant("sample1", &["test", "chrome"]); 178 | trace.instant_payload("sample2", &["test", "chrome"], to_payload("payload 2")); 179 | trace.instant_payload("sample3", &["test", "chrome"], to_payload("payload 3")); 180 | trace.closure_payload( 181 | "sample4", 182 | &["test", "chrome"], 183 | || (), 184 | to_payload("payload 4"), 185 | ); 186 | 187 | let samples = trace.samples_cloned_unsorted(); 188 | 189 | let mut serialized = Vec::::new(); 190 | let result = serialize(&samples, &mut serialized); 191 | assert!(result.is_ok(), "{:?}", result); 192 | 193 | let deserialized_samples = deserialize(serialized.as_slice()).unwrap(); 194 | assert_eq!(deserialized_samples, samples); 195 | } 196 | 197 | #[cfg(all(feature = "chrome_trace_event", feature = "benchmarks"))] 198 | #[bench] 199 | fn bench_chrome_trace_serialization_one_element(b: &mut Bencher) { 200 | use super::*; 201 | 202 | let mut serialized = Vec::::new(); 203 | let samples = vec![super::Sample::new_instant( 204 | "trace1", 205 | &["benchmark", "test"], 206 | None, 207 | )]; 208 | b.iter(|| { 209 | serialized.clear(); 210 | serialize(&samples, &mut serialized).unwrap(); 211 | }); 212 | } 213 | 214 | #[cfg(all(feature = "chrome_trace_event", feature = "benchmarks"))] 215 | #[bench] 216 | fn bench_chrome_trace_serialization_multiple_elements(b: &mut Bencher) { 217 | use super::super::*; 218 | use super::*; 219 | 220 | let mut serialized = Vec::::new(); 221 | let samples = vec![ 222 | Sample::new_instant("trace1", &["benchmark", "test"], None), 223 | Sample::new_instant("trace2", &["benchmark"], None), 224 | Sample::new_duration( 225 | "trace3", 226 | &["benchmark"], 227 | Some(to_payload("some payload")), 228 | 0, 229 | 0, 230 | ), 231 | Sample::new_instant("trace4", &["benchmark"], None), 232 | ]; 233 | 234 | b.iter(|| { 235 | serialized.clear(); 236 | serialize(&samples, &mut serialized).unwrap(); 237 | }); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /trace/src/fixed_lifo_deque.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::cmp::{self, Ordering}; 16 | use std::collections::vec_deque::{Drain, IntoIter, Iter, IterMut, VecDeque}; 17 | use std::hash::{Hash, Hasher}; 18 | use std::ops::{Index, IndexMut, RangeBounds}; 19 | 20 | /// Provides fixed size ring buffer that overwrites elements in FIFO order on 21 | /// insertion when full. API provided is similar to VecDeque & uses a VecDeque 22 | /// internally. One distinction is that only append-like insertion is allowed. 23 | /// This means that insert & push_front are not allowed. The reasoning is that 24 | /// there is ambiguity on how such functions should operate since it would be 25 | /// pretty impossible to maintain a FIFO ordering. 26 | /// 27 | /// All operations that would cause growth beyond the limit drop the appropriate 28 | /// number of elements from the front. For example, on a full buffer push_front 29 | /// replaces the first element. 30 | /// 31 | /// The removal of elements on operation that would cause excess beyond the 32 | /// limit happens first to make sure the space is available in the underlying 33 | /// VecDeque, thus guaranteeing O(1) operations always. 34 | #[derive(Clone, Debug)] 35 | pub struct FixedLifoDeque { 36 | storage: VecDeque, 37 | limit: usize, 38 | } 39 | 40 | impl FixedLifoDeque { 41 | /// Constructs a ring buffer that will reject all insertions as no-ops. 42 | /// This also construct the underlying VecDeque with_capacity(0) which 43 | /// in the current stdlib implementation allocates 2 Ts. 44 | #[inline] 45 | pub fn new() -> Self { 46 | FixedLifoDeque::with_limit(0) 47 | } 48 | 49 | /// Constructs a fixed size ring buffer with the given number of elements. 50 | /// Attempts to insert more than this number of elements will cause excess 51 | /// elements to first be evicted in FIFO order (i.e. from the front). 52 | pub fn with_limit(n: usize) -> Self { 53 | FixedLifoDeque { 54 | storage: VecDeque::with_capacity(n), 55 | limit: n, 56 | } 57 | } 58 | 59 | /// This sets a new limit on the container. Excess elements are dropped in 60 | /// FIFO order. The new capacity is reset to the requested limit which will 61 | /// likely result in re-allocation + copies/clones even if the limit 62 | /// shrinks. 63 | pub fn reset_limit(&mut self, n: usize) { 64 | if n < self.limit { 65 | let overflow = self.limit - n; 66 | self.drop_excess_for_inserting(overflow); 67 | } 68 | self.limit = n; 69 | self.storage.reserve_exact(n); 70 | self.storage.shrink_to_fit(); 71 | debug_assert!(self.storage.len() <= self.limit); 72 | } 73 | 74 | /// Returns the current limit this ring buffer is configured with. 75 | #[inline] 76 | pub fn limit(&self) -> usize { 77 | self.limit 78 | } 79 | 80 | #[inline] 81 | pub fn get(&self, index: usize) -> Option<&T> { 82 | self.storage.get(index) 83 | } 84 | 85 | #[inline] 86 | pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { 87 | self.storage.get_mut(index) 88 | } 89 | 90 | #[inline] 91 | pub fn swap(&mut self, i: usize, j: usize) { 92 | self.storage.swap(i, j); 93 | } 94 | 95 | #[inline] 96 | pub fn capacity(&self) -> usize { 97 | self.limit 98 | } 99 | 100 | #[inline] 101 | pub fn iter(&self) -> Iter { 102 | self.storage.iter() 103 | } 104 | 105 | #[inline] 106 | pub fn iter_mut(&mut self) -> IterMut { 107 | self.storage.iter_mut() 108 | } 109 | 110 | /// Returns a tuple of 2 slices that represents the ring buffer. [0] is the 111 | /// beginning of the buffer to the physical end of the array or the last 112 | /// element (whichever comes first). [1] is the continuation of [0] if the 113 | /// ring buffer has wrapped the contiguous storage. 114 | #[inline] 115 | pub fn as_slices(&self) -> (&[T], &[T]) { 116 | self.storage.as_slices() 117 | } 118 | 119 | #[inline] 120 | pub fn as_mut_slices(&mut self) -> (&mut [T], &mut [T]) { 121 | self.storage.as_mut_slices() 122 | } 123 | 124 | #[inline] 125 | pub fn len(&self) -> usize { 126 | self.storage.len() 127 | } 128 | 129 | #[inline] 130 | pub fn is_empty(&self) -> bool { 131 | self.storage.is_empty() 132 | } 133 | 134 | #[inline] 135 | pub fn drain(&mut self, range: R) -> Drain 136 | where 137 | R: RangeBounds, 138 | { 139 | self.storage.drain(range) 140 | } 141 | 142 | #[inline] 143 | pub fn clear(&mut self) { 144 | self.storage.clear(); 145 | } 146 | 147 | #[inline] 148 | pub fn contains(&self, x: &T) -> bool 149 | where 150 | T: PartialEq, 151 | { 152 | self.storage.contains(x) 153 | } 154 | 155 | #[inline] 156 | pub fn front(&self) -> Option<&T> { 157 | self.storage.front() 158 | } 159 | 160 | #[inline] 161 | pub fn front_mut(&mut self) -> Option<&mut T> { 162 | self.storage.front_mut() 163 | } 164 | 165 | #[inline] 166 | pub fn back(&self) -> Option<&T> { 167 | self.storage.back() 168 | } 169 | 170 | #[inline] 171 | pub fn back_mut(&mut self) -> Option<&mut T> { 172 | self.storage.back_mut() 173 | } 174 | 175 | #[inline] 176 | fn drop_excess_for_inserting(&mut self, n_to_be_inserted: usize) { 177 | if self.storage.len() + n_to_be_inserted > self.limit { 178 | let overflow = self 179 | .storage 180 | .len() 181 | .min(self.storage.len() + n_to_be_inserted - self.limit); 182 | self.storage.drain(..overflow); 183 | } 184 | } 185 | 186 | /// Always an O(1) operation. Memory is never reclaimed. 187 | #[inline] 188 | pub fn pop_front(&mut self) -> Option { 189 | self.storage.pop_front() 190 | } 191 | 192 | /// Always an O(1) operation. If the number of elements is at the limit, 193 | /// the element at the front is overwritten. 194 | /// 195 | /// Post condition: The number of elements is <= limit 196 | pub fn push_back(&mut self, value: T) { 197 | self.drop_excess_for_inserting(1); 198 | self.storage.push_back(value); 199 | // For when limit == 0 200 | self.drop_excess_for_inserting(0); 201 | } 202 | 203 | /// Always an O(1) operation. Memory is never reclaimed. 204 | #[inline] 205 | pub fn pop_back(&mut self) -> Option { 206 | self.storage.pop_back() 207 | } 208 | 209 | #[inline] 210 | pub fn swap_remove_back(&mut self, index: usize) -> Option { 211 | self.storage.swap_remove_back(index) 212 | } 213 | 214 | #[inline] 215 | pub fn swap_remove_front(&mut self, index: usize) -> Option { 216 | self.storage.swap_remove_front(index) 217 | } 218 | 219 | /// Always an O(1) operation. 220 | #[inline] 221 | pub fn remove(&mut self, index: usize) -> Option { 222 | self.storage.remove(index) 223 | } 224 | 225 | pub fn split_off(&mut self, at: usize) -> FixedLifoDeque { 226 | FixedLifoDeque { 227 | storage: self.storage.split_off(at), 228 | limit: self.limit, 229 | } 230 | } 231 | 232 | /// Always an O(m) operation where m is the length of `other'. 233 | pub fn append(&mut self, other: &mut VecDeque) { 234 | self.drop_excess_for_inserting(other.len()); 235 | self.storage.append(other); 236 | // For when limit == 0 237 | self.drop_excess_for_inserting(0); 238 | } 239 | 240 | #[inline] 241 | pub fn retain(&mut self, f: F) 242 | where 243 | F: FnMut(&T) -> bool, 244 | { 245 | self.storage.retain(f); 246 | } 247 | } 248 | 249 | impl FixedLifoDeque { 250 | /// Resizes a fixed queue. This doesn't change the limit so the resize is 251 | /// capped to the limit. Additionally, resizing drops the elements from the 252 | /// front unlike with a regular VecDeque. 253 | pub fn resize(&mut self, new_len: usize, value: T) { 254 | if new_len < self.len() { 255 | let to_drop = self.len() - new_len; 256 | self.storage.drain(..to_drop); 257 | } else { 258 | self.storage.resize(cmp::min(self.limit, new_len), value); 259 | } 260 | } 261 | } 262 | 263 | impl PartialEq for FixedLifoDeque { 264 | #[inline] 265 | fn eq(&self, other: &FixedLifoDeque) -> bool { 266 | self.storage == other.storage 267 | } 268 | } 269 | 270 | impl Eq for FixedLifoDeque {} 271 | 272 | impl PartialOrd for FixedLifoDeque { 273 | #[inline] 274 | fn partial_cmp(&self, other: &FixedLifoDeque) -> Option { 275 | self.storage.partial_cmp(&other.storage) 276 | } 277 | } 278 | 279 | impl Ord for FixedLifoDeque { 280 | #[inline] 281 | fn cmp(&self, other: &FixedLifoDeque) -> Ordering { 282 | self.storage.cmp(&other.storage) 283 | } 284 | } 285 | 286 | impl Hash for FixedLifoDeque { 287 | #[inline] 288 | fn hash(&self, state: &mut H) { 289 | self.storage.hash(state); 290 | } 291 | } 292 | 293 | impl Index for FixedLifoDeque { 294 | type Output = A; 295 | 296 | #[inline] 297 | fn index(&self, index: usize) -> &A { 298 | &self.storage[index] 299 | } 300 | } 301 | 302 | impl IndexMut for FixedLifoDeque { 303 | #[inline] 304 | fn index_mut(&mut self, index: usize) -> &mut A { 305 | &mut self.storage[index] 306 | } 307 | } 308 | 309 | impl IntoIterator for FixedLifoDeque { 310 | type Item = T; 311 | type IntoIter = IntoIter; 312 | 313 | /// Consumes the list into a front-to-back iterator yielding elements by 314 | /// value. 315 | #[inline] 316 | fn into_iter(self) -> IntoIter { 317 | self.storage.into_iter() 318 | } 319 | } 320 | 321 | impl<'a, T> IntoIterator for &'a FixedLifoDeque { 322 | type Item = &'a T; 323 | type IntoIter = Iter<'a, T>; 324 | 325 | #[inline] 326 | fn into_iter(self) -> Iter<'a, T> { 327 | self.storage.iter() 328 | } 329 | } 330 | 331 | impl<'a, T> IntoIterator for &'a mut FixedLifoDeque { 332 | type Item = &'a mut T; 333 | type IntoIter = IterMut<'a, T>; 334 | 335 | #[inline] 336 | fn into_iter(self) -> IterMut<'a, T> { 337 | self.storage.iter_mut() 338 | } 339 | } 340 | 341 | impl Extend for FixedLifoDeque { 342 | fn extend>(&mut self, iter: T) { 343 | for elt in iter { 344 | self.push_back(elt); 345 | } 346 | } 347 | } 348 | 349 | impl<'a, T: 'a + Copy> Extend<&'a T> for FixedLifoDeque { 350 | fn extend>(&mut self, iter: I) { 351 | self.extend(iter.into_iter().cloned()); 352 | } 353 | } 354 | 355 | #[cfg(test)] 356 | mod tests { 357 | use super::*; 358 | #[cfg(feature = "benchmarks")] 359 | use test::Bencher; 360 | 361 | #[test] 362 | fn test_basic_insertions() { 363 | let mut tester = FixedLifoDeque::with_limit(3); 364 | assert_eq!(tester.len(), 0); 365 | assert_eq!(tester.capacity(), 3); 366 | assert_eq!(tester.front(), None); 367 | assert_eq!(tester.back(), None); 368 | 369 | tester.push_back(1); 370 | assert_eq!(tester.len(), 1); 371 | assert_eq!(tester.front(), Some(1).as_ref()); 372 | assert_eq!(tester.back(), Some(1).as_ref()); 373 | 374 | tester.push_back(2); 375 | assert_eq!(tester.len(), 2); 376 | assert_eq!(tester.front(), Some(1).as_ref()); 377 | assert_eq!(tester.back(), Some(2).as_ref()); 378 | 379 | tester.push_back(3); 380 | tester.push_back(4); 381 | assert_eq!(tester.len(), 3); 382 | assert_eq!(tester.front(), Some(2).as_ref()); 383 | assert_eq!(tester.back(), Some(4).as_ref()); 384 | assert_eq!(tester[0], 2); 385 | assert_eq!(tester[1], 3); 386 | assert_eq!(tester[2], 4); 387 | } 388 | 389 | #[cfg(feature = "benchmarks")] 390 | #[bench] 391 | fn bench_push_back(b: &mut Bencher) { 392 | let mut q = FixedLifoDeque::with_limit(10); 393 | b.iter(|| q.push_back(5)); 394 | } 395 | 396 | #[cfg(feature = "benchmarks")] 397 | #[bench] 398 | fn bench_deletion_from_empty(b: &mut Bencher) { 399 | let mut q = FixedLifoDeque::::with_limit(10000); 400 | b.iter(|| q.pop_front()); 401 | } 402 | 403 | #[cfg(feature = "benchmarks")] 404 | #[bench] 405 | fn bench_deletion_from_non_empty(b: &mut Bencher) { 406 | let mut q = FixedLifoDeque::with_limit(1000000); 407 | for i in 0..q.limit() { 408 | q.push_back(i); 409 | } 410 | b.iter(|| q.pop_front()); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /trace/src/sys_pid.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[cfg(all(target_family = "unix", not(target_os = "fuchsia")))] 16 | #[inline] 17 | pub fn current_pid() -> u64 { 18 | extern "C" { 19 | fn getpid() -> libc::pid_t; 20 | } 21 | 22 | unsafe { getpid() as u64 } 23 | } 24 | 25 | #[cfg(target_os = "fuchsia")] 26 | pub fn current_pid() -> u64 { 27 | // TODO: implement for fuchsia (does getpid work?) 28 | 0 29 | } 30 | 31 | #[cfg(target_family = "windows")] 32 | #[inline] 33 | pub fn current_pid() -> u64 { 34 | extern "C" { 35 | fn GetCurrentProcessId() -> libc::c_ulong; 36 | } 37 | 38 | unsafe { u64::from(GetCurrentProcessId()) } 39 | } 40 | -------------------------------------------------------------------------------- /trace/src/sys_tid.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The xi-editor Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[cfg(any(target_os = "macos", target_os = "ios"))] 16 | #[inline] 17 | pub fn current_tid() -> Result { 18 | #[link(name = "pthread")] 19 | extern "C" { 20 | fn pthread_threadid_np(thread: libc::pthread_t, thread_id: *mut u64) -> libc::c_int; 21 | } 22 | 23 | unsafe { 24 | let mut tid = 0; 25 | let err = pthread_threadid_np(0, &mut tid); 26 | match err { 27 | 0 => Ok(tid), 28 | _ => Err(err), 29 | } 30 | } 31 | } 32 | 33 | #[cfg(target_os = "fuchsia")] 34 | #[inline] 35 | pub fn current_tid() -> Result { 36 | // TODO: fill in for fuchsia. This is the native C API but maybe there are 37 | // rust-specific bindings already. 38 | /* 39 | extern { 40 | fn thrd_get_zx_handle(thread: thrd_t) -> zx_handle_t; 41 | fn thrd_current() -> thrd_t; 42 | } 43 | 44 | Ok(thrd_get_zx_handle(thrd_current()) as u64) 45 | */ 46 | Ok(0) 47 | } 48 | 49 | #[cfg(any(target_os = "linux", target_os = "android"))] 50 | #[inline] 51 | pub fn current_tid() -> Result { 52 | unsafe { Ok(libc::syscall(libc::SYS_gettid) as u64) } 53 | } 54 | 55 | // TODO: maybe use https://github.com/alexcrichton/cfg-if to simplify this? 56 | // pthread-based fallback 57 | #[cfg(all( 58 | target_family = "unix", 59 | not(any( 60 | target_os = "macos", 61 | target_os = "ios", 62 | target_os = "linux", 63 | target_os = "android", 64 | target_os = "fuchsia" 65 | )) 66 | ))] 67 | pub fn current_tid() -> Result { 68 | unsafe { Ok(libc::pthread_self() as u64) } 69 | } 70 | 71 | #[cfg(target_os = "windows")] 72 | #[inline] 73 | pub fn current_tid() -> Result { 74 | extern "C" { 75 | fn GetCurrentThreadId() -> libc::c_ulong; 76 | } 77 | 78 | unsafe { Ok(u64::from(GetCurrentThreadId())) } 79 | } 80 | --------------------------------------------------------------------------------