rustkernel/
program.rs

1//! This module is responsible for keeping the program in state
2//! between API calls. The `Program` struct holds the cells in a
3//! hashmap
4use crate::handlers::CodeRequest;
5use std::collections::HashMap;
6use std::fs;
7use std::fs::write;
8use std::process::Command;
9
10/// This is what sits in state as long as the program is running
11/// when a new request is made, it will check if the same cell has
12/// been executed by checking `cells` which is a `HashMap`
13///
14/// # Terms
15/// - temp_dir: Whatever the OS temp directory is
16/// - filename: Last executed filename so we can restart state if necessary
17/// - cells: VS Code name for a notebook cell
18
19#[derive(Debug)]
20pub struct Program {
21    pub filename: String,
22    pub workspace: String,
23    pub cells: HashMap<u32, Cell>,
24}
25
26/// A cell represent a notebook cell in VS Code
27///
28/// # Terms
29/// - fragment: unique id of the cell that executed
30/// - index: current index by order in VS Code
31/// - contents: the source code from inside the cell
32/// used to determine what output to return to caller
33#[derive(Debug)]
34pub struct Cell {
35    pub fragment: u32,
36    pub index: u32,
37    pub contents: String,
38}
39
40impl Program {
41    /// Create a new program which is retained in state
42    /// between `http` requests
43    pub fn new() -> Program {
44        Program {
45            filename: String::new(),
46            workspace: String::new(),
47            cells: HashMap::new(),
48        }
49    }
50
51    /// Initial cell creation if it doesn't exist yet
52    /// Contents Use clone as we need to retain
53    /// a pointer to data that will stick around after the request has finished
54    pub fn create_cell(&mut self, cr: &CodeRequest) {
55        let cell = Cell {
56            contents: cr.contents.clone(),
57            fragment: cr.fragment,
58            index: cr.index,
59        };
60        self.cells.insert(cr.fragment, cell);
61    }
62
63    /// As the cells are implemented with a HashMap this is a fast lookup,
64    /// Only updates what's required
65    pub fn update_cell(&mut self, cr: &CodeRequest) {
66        if let Some(cell) = self.cells.get_mut(&cr.fragment) {
67            cell.contents = cr.contents.clone();
68            cell.index = cr.index;
69        }
70    }
71
72    /// First sorts the hashmap by index into a vector, then writes the output with
73    /// `rustkernel-start` and `rustkernel-end` to determine which cell's was executing
74    // so we return only that output.
75    pub fn write_to_file(&mut self, fragment: u32) {
76        // Sort based on current cell indices in VS Code
77        let mut cells_vec: Vec<&Cell> = self.cells.iter().map(|(_, cell)| (cell)).collect();
78        cells_vec.sort_by(|a, b| a.index.cmp(&b.index));
79
80        // Write the file output
81        let mut crates = String::new();
82        let mut outer_scope = String::new();
83        let mut inner_scope = String::new();
84        let mut contains_main = false;
85        for cell in cells_vec {
86            let lines = cell.contents.split('\n');
87            for line in lines {
88                let line = line.trim();
89                // If it contains main we won't add a main function
90                if line.starts_with("fn main()") {
91                    contains_main = true;
92                    continue;
93                }
94                // Don't print if it's not the executing cell
95                if line.starts_with("print") && cell.fragment != fragment {
96                    continue;
97                }
98
99                if line.starts_with("use") {
100                    outer_scope += line;
101                    outer_scope += "\n";
102                    if !line.starts_with("use std") {
103                        let (_, full_path) = line.split_once(' ').unwrap();
104                        let (crate_name, _) = full_path.split_once(':').unwrap();
105                        let crate_name_fixed = str::replace(crate_name, "_", "-");
106                        crates += &crate_name_fixed;
107                        crates += "=\"*\"\n";
108                    }
109                } else {
110                    inner_scope += line;
111                    inner_scope += "\n";
112                }
113            }
114            if contains_main {
115                inner_scope = inner_scope.trim_end().to_string();
116                inner_scope.pop();
117                contains_main = false;
118            }
119        }
120
121        let mut output = "#![allow(dead_code)]\n".to_string();
122        output += outer_scope.as_str();
123        output += "fn main() {\n";
124        output += &inner_scope;
125        output += "}";
126
127        // Use the folder name sent from VS Code to create the
128        // file structure required for a cargo project
129        let mut dir = self.workspace.to_owned();
130
131        let mut cargo_file = dir.clone();
132        cargo_file += "/Cargo.toml";
133
134        dir += "/src";
135        let mut main_file = dir.to_owned();
136        main_file += "/main.rs";
137
138        if let Err(err) = fs::create_dir_all(&dir) {
139            if err.kind() != std::io::ErrorKind::AlreadyExists {
140                panic!("Can't create dir: {}, err {}", &dir, err)
141            }
142        };
143
144        let cargo_contents = format!(
145            "{}\n{}",
146            r#"
147[package]
148name = 'output'
149version = '0.0.1'
150edition = '2021'
151[dependencies]
152"#,
153            crates
154        );
155
156        // Write the files
157        write(&main_file, &output).expect("Error writing file");
158        write(&cargo_file, &cargo_contents).expect("Error writing file");
159    }
160
161    /// Run the program by running `cargo run` in the temp directory
162    /// then uses regex to determine what part of the output to send back
163    /// to the caller
164    pub fn run(&self) -> String {
165        let output = Command::new("cargo")
166            .current_dir(&self.workspace)
167            .arg("run")
168            .output()
169            .expect("Failed to run cargo");
170
171        let err = String::from_utf8(output.stderr).expect("Failed to parse utf8");
172
173        if err.contains("error: ") || err.contains("panicked at") {
174            // 1 denotes an error
175            return format!("1\0{}", err);
176        }
177
178        let output = String::from_utf8(output.stdout).unwrap();
179        format!("0\0{}", output)
180    }
181
182    pub fn fmt(&self) {
183        Command::new("cargo")
184            .current_dir(&self.workspace)
185            .arg("fmt")
186            .output()
187            .expect("Failed to run cargo fmt");
188    }
189}