dotnetrawfilereader_sys/
runtime.rs

1//! Manage the creation of and access to the self-hosted .NET runtime.
2//!
3//! The [`DotNetLibraryBundle`] is the main entry point.
4//!
5//! The environment variable `DOTNET_RAWFILEREADER_BUNDLE_PATH` can be used to set a default location
6//! for where DLLs will be written to that persists for recurring use.
7use std::fmt::Debug;
8use std::fs;
9use std::env;
10use std::io::{self, prelude::*};
11use std::path::{self, Path, PathBuf};
12use std::sync::{Arc, OnceLock, RwLock};
13
14use include_dir::{include_dir, Dir};
15use tempfile::{TempDir, Builder as TempDirBuilder};
16
17use netcorehost::{hostfxr::AssemblyDelegateLoader, nethost, pdcstring::PdCString};
18
19use crate::buffer::configure_allocator;
20
21static DOTNET_LIB_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/lib/");
22
23const TMP_NAME: &str = concat!("rawfilereader_libs_", env!("CARGO_PKG_VERSION"));
24const DEFAULT_VAR_NAME: &str = "DOTNET_RAWFILEREADER_BUNDLE_PATH";
25
26
27/// Things that can go wrong while creating a .NET runtime
28#[derive(Debug, thiserror::Error)]
29pub enum DotNetRuntimeCreationError {
30    /// An error might occur while creating the .NET DLL bundle
31    #[error("Failed to create a directory on the file system to hold .NET DLLs")]
32    FailedToWriteDLLBundle(#[source] io::Error),
33    /// An error might occur while loading the Hostfxr layer of the .NET runtime
34    #[error("Failed to load hostfxr. Is there a .NET runtime available?")]
35    LoadHostfxrError(#[source] #[from] nethost::LoadHostfxrError),
36    /// An error might occur while loading the core .NET runtime or library invocation
37    #[error("Failed to load .NET host runtime. Is there a .NET runtime available?")]
38    HostingError(#[source] #[from] netcorehost::error::HostingError),
39    /// Any other I/O error that might occur
40    #[error(transparent)]
41    IOError(io::Error)
42}
43
44
45/// Represent a directory to store bundled files within.
46#[derive(Debug)]
47pub enum BundleStore {
48    /// Use a temporary directory that will be cleaned up automatically.
49    TempDir(TempDir),
50    /// Use a specific directory that will persist after the process ends.
51    Path(PathBuf),
52}
53
54/// A location on the file system and an associated .NET DLL bundle to host a
55/// .NET runtime for.
56///
57/// Uses the `DOTNET_RAWFILEREADER_BUNDLE_PATH` environment variable when a default
58/// is required, otherwise creates a temporary directory whose lifespan is linked to this
59/// object.
60#[derive()]
61pub struct DotNetLibraryBundle {
62    /// Where to write the DLLs
63    dir: BundleStore,
64    /// A reference to the actual runtime
65    assembly_loader: RwLock<Option<Arc<AssemblyDelegateLoader>>>,
66}
67
68impl Debug for DotNetLibraryBundle {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("DotNetLibraryBundle").field("dir", &self.dir).field("assembly_loader", &"?").finish()
71    }
72}
73
74impl Default for DotNetLibraryBundle {
75    fn default() -> Self {
76        match env::var(DEFAULT_VAR_NAME) {
77            Ok(val) => Self::new(Some(&val)).unwrap(),
78            Err(err) => {
79                match err {
80                    env::VarError::NotPresent => Self::new(None).unwrap(),
81                    env::VarError::NotUnicode(err) => {
82                        eprintln!("Failed to decode `{DEFAULT_VAR_NAME}` {}", err.to_string_lossy());
83                        Self::new(None).unwrap()
84                    },
85                }
86            },
87        }
88    }
89}
90
91impl DotNetLibraryBundle {
92    /// Create a new bundle directory. If a path string is provided, that path
93    /// will be used. Otherwise a temporary directory will be created.
94    pub fn new(dir: Option<&str>) -> io::Result<Self> {
95        let dir = if let Some(path) = dir {
96            let pathbuf = PathBuf::from(path);
97            if !pathbuf.exists() {
98                fs::create_dir_all(&pathbuf)?;
99            }
100            BundleStore::Path(pathbuf)
101        } else {
102            env::var(DEFAULT_VAR_NAME).map(|path| -> io::Result<BundleStore> {
103                let pathbuf = PathBuf::from(path);
104                if !pathbuf.exists() {
105                    fs::create_dir_all(&pathbuf)?;
106                }
107                Ok(BundleStore::Path(pathbuf))
108            }).unwrap_or_else(|_| {
109                Ok(BundleStore::TempDir(TempDirBuilder::new().prefix(TMP_NAME).tempdir()?))
110            })?
111        };
112        Ok(Self {
113            dir,
114            assembly_loader: RwLock::new(None),
115        })
116    }
117
118    /// Get a path reference to the directory
119    pub fn path(&self) -> &path::Path {
120        match &self.dir {
121            BundleStore::TempDir(d) => d.path(),
122            BundleStore::Path(d) => d.as_path(),
123        }
124    }
125
126    /// Get a reference to the .NET runtime, creating it if one has not yet been created.
127    ///
128    /// This function will panic if a runtime cannot be found. Calls [`DotNetLibraryBundle::try_runtime`] and unwraps.
129    ///
130    /// See [`DotNetLibraryBundle::try_create_runtime`] for specific runtime creation
131    pub fn runtime(&self) -> Arc<AssemblyDelegateLoader> {
132        self.try_create_runtime().unwrap()
133    }
134
135    /// Get a reference to the .NET runtime, creating it if one has not yet been created
136    /// or return an error if the runtime could not be created.
137    ///
138    /// See [`DotNetLibraryBundle::try_create_runtime`] for specific runtime creation
139    pub fn try_runtime(&self) -> Result<Arc<AssemblyDelegateLoader>, DotNetRuntimeCreationError> {
140        if let Ok(mut guard) = self.assembly_loader.write() {
141            if guard.is_none() {
142                *guard = Some(self.try_create_runtime()?);
143            }
144        }
145        let a = self
146            .assembly_loader
147            .read()
148            .map(|a| a.clone().unwrap())
149            .unwrap();
150        Ok(a)
151    }
152
153    /// Write all of the bundled .NET DLLs to the file system at this location
154    pub fn write_bundle(&self) -> io::Result<()> {
155        let path = self.path();
156        let do_write = if !path.exists() {
157            fs::create_dir_all(path)?;
158            true
159        } else if path.join("checksum").exists(){
160            let checksum = fs::read(path.join("checksum"))?;
161            let lib_checksum = DOTNET_LIB_DIR.get_file("checksum").unwrap().contents();
162            checksum != lib_checksum
163        } else {
164            true
165        };
166
167        if !do_write {
168            return Ok(())
169        }
170
171        for entry in DOTNET_LIB_DIR.entries() {
172            if let Some(data_handle) = entry.as_file() {
173                let destintation = path.join(entry.path());
174                let mut outhandle = io::BufWriter::new(fs::File::create(destintation)?);
175                outhandle.write_all(data_handle.contents())?;
176            }
177        }
178
179        Ok(())
180    }
181
182    pub fn try_create_runtime(&self) -> Result<Arc<AssemblyDelegateLoader>, DotNetRuntimeCreationError> {
183        let hostfxr = nethost::load_hostfxr()?;
184        self.write_bundle().map_err(|e| {
185            match e.kind() {
186                io::ErrorKind::PermissionDenied | io::ErrorKind::NotFound => DotNetRuntimeCreationError::FailedToWriteDLLBundle(e),
187                _ => DotNetRuntimeCreationError::IOError(e),
188            }
189        })?;
190
191        let runtime_path = self.path().join("librawfilereader.runtimeconfig.json");
192        let runtime_path_encoded: PdCString = runtime_path.to_string_lossy().parse().unwrap();
193
194        let context = hostfxr
195            .initialize_for_runtime_config(runtime_path_encoded)?;
196
197
198        let assembly_path = self.path().join("librawfilereader.dll");
199        let assembly_path_encoded: PdCString = assembly_path.to_string_lossy().parse().unwrap();
200
201        let delegate_loader = Arc::new(
202            context
203                .get_delegate_loader_for_assembly(assembly_path_encoded)?
204        );
205
206        configure_allocator(&delegate_loader);
207        Ok(delegate_loader)
208    }
209}
210
211static BUNDLE: OnceLock<DotNetLibraryBundle> = OnceLock::new();
212
213/// Set the default runtime directory to `path` that will be accessed by [`get_runtime`]
214pub fn set_runtime_dir<P: AsRef<Path>>(path: P) -> io::Result<()> {
215    let path: &Path = path.as_ref();
216    if !path.exists() {
217        fs::DirBuilder::new().recursive(true).create(path)?;
218    }
219
220    let bundle = DotNetLibraryBundle::new(Some(path.to_str().unwrap())).unwrap();
221    BUNDLE.set(bundle).unwrap();
222    Ok(())
223}
224
225/// Get a reference to a shared .NET runtime and associated DLL bundle
226///
227/// Panics if a runtime cannot be created. Calls [`try_get_runtime`] and unwraps.
228pub fn get_runtime() -> Arc<AssemblyDelegateLoader> {
229    try_get_runtime().unwrap()
230}
231
232
233/// Get a reference to a shared .NET runtime and associated DLL bundle
234/// or return and error if the runtime cannot be created.
235pub fn try_get_runtime() -> Result<Arc<AssemblyDelegateLoader>, DotNetRuntimeCreationError> {
236    let bundle = BUNDLE.get_or_init(DotNetLibraryBundle::default);
237
238    bundle.try_runtime()
239}
240
241
242#[cfg(test)]
243mod test {
244    use super::*;
245
246    #[test]
247    fn test_bundle_writing() -> io::Result<()> {
248        let handle = DotNetLibraryBundle::new(None)?;
249        let _runtime = handle.runtime();
250        Ok(())
251    }
252}