binaryninja/
headless.rs

1// Copyright 2021-2025 Vector 35 Inc.
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
15use crate::{
16    binary_view, bundled_plugin_directory, enterprise, is_license_validated, is_main_thread,
17    is_ui_enabled, license_path, set_bundled_plugin_directory, set_license, string::IntoJson,
18};
19use std::io;
20use std::path::{Path, PathBuf};
21use std::sync::atomic::AtomicUsize;
22use std::sync::atomic::Ordering::SeqCst;
23use thiserror::Error;
24
25use crate::enterprise::EnterpriseCheckoutStatus;
26use crate::main_thread::{MainThreadAction, MainThreadHandler};
27use crate::progress::ProgressCallback;
28use crate::rc::Ref;
29use binaryninjacore_sys::{BNInitPlugins, BNInitRepoPlugins};
30use std::sync::mpsc::Sender;
31use std::sync::Mutex;
32use std::thread::JoinHandle;
33use std::time::Duration;
34
35static MAIN_THREAD_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
36
37/// Used to prevent shutting down Binary Ninja if there is another active [`Session`].
38static SESSION_COUNT: AtomicUsize = AtomicUsize::new(0);
39
40#[derive(Error, Debug)]
41pub enum InitializationError {
42    #[error("main thread could not be started: {0}")]
43    MainThreadNotStarted(#[from] io::Error),
44    #[error("enterprise license checkout failed: {0:?}")]
45    FailedEnterpriseCheckout(#[from] enterprise::EnterpriseCheckoutError),
46    #[error("invalid license")]
47    InvalidLicense,
48    #[error("no license could located, please see `binaryninja::set_license` for details")]
49    NoLicenseFound,
50    #[error("initialization already managed by ui")]
51    AlreadyManaged,
52}
53
54/// Loads plugins, core architecture, platform, etc.
55///
56/// ⚠️ Important! Must be called at the beginning of scripts.  Plugins do not need to call this. ⚠️
57///
58/// The preferred method for core initialization is [`Session`], use that instead of this where possible.
59///
60/// If you need to customize initialization, use [`init_with_opts`] instead.
61pub fn init() -> Result<(), InitializationError> {
62    let options = InitializationOptions::default();
63    init_with_opts(options)
64}
65
66/// Unloads plugins, stops all worker threads, and closes open logs.
67///
68/// This function does _NOT_ release floating licenses; it is expected that you call [`enterprise::release_license`].
69pub fn shutdown() {
70    match crate::product().as_str() {
71        "Binary Ninja Enterprise Client" | "Binary Ninja Ultimate" => {
72            // By default, we do not release floating licenses.
73            enterprise::release_license(false)
74        }
75        _ => {}
76    }
77    unsafe { binaryninjacore_sys::BNShutdown() };
78    // TODO: We might want to drop the main thread here, however that requires getting the handler ctx to drop the sender.
79}
80
81pub fn is_shutdown_requested() -> bool {
82    unsafe { binaryninjacore_sys::BNIsShutdownRequested() }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub struct InitializationOptions {
87    /// A license to override with, you can use this to make sure you initialize with a specific license.
88    pub license: Option<String>,
89    /// If you need to make sure that you do not check out a license, set this to false.
90    ///
91    /// This is really only useful if you have a headless license but are using an enterprise-enabled core.
92    pub checkout_license: bool,
93    /// Whether to register the default main thread handler.
94    ///
95    /// Set this to false if you have your own main thread handler.
96    pub register_main_thread_handler: bool,
97    /// How long you want to check out for.
98    pub floating_license_duration: Duration,
99    /// The bundled plugin directory to use.
100    pub bundled_plugin_directory: PathBuf,
101    /// Whether to initialize user plugins.
102    ///
103    /// Set this to false if your use might be impacted by a user-installed plugin.
104    pub user_plugins: bool,
105    /// Whether to initialize repo plugins.
106    ///
107    /// Set this to false if your use might be impacted by a repo-installed plugin.
108    pub repo_plugins: bool,
109}
110
111impl InitializationOptions {
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// A license to override with, you can use this to make sure you initialize with a specific license.
117    ///
118    /// This takes the form of a JSON array. The string should be formed like:
119    /// ```json
120    /// [{ /* json object with license data */ }]
121    /// ```
122    pub fn with_license(mut self, license: impl Into<String>) -> Self {
123        self.license = Some(license.into());
124        self
125    }
126
127    /// If you need to make sure that you do not check out a license, set this to false.
128    ///
129    /// This is really only useful if you have a headless license but are using an enterprise-enabled core.
130    pub fn with_license_checkout(mut self, should_checkout: bool) -> Self {
131        self.checkout_license = should_checkout;
132        self
133    }
134
135    /// Whether to register the default main thread handler.
136    ///
137    /// Set this to false if you have your own main thread handler.
138    pub fn with_main_thread_handler(mut self, should_register: bool) -> Self {
139        self.register_main_thread_handler = should_register;
140        self
141    }
142
143    /// How long you want to check out for, only used if you are using a floating license.
144    pub fn with_floating_license_duration(mut self, duration: Duration) -> Self {
145        self.floating_license_duration = duration;
146        self
147    }
148
149    /// Set this to false if your use might be impacted by a user-installed plugin.
150    pub fn with_user_plugins(mut self, should_initialize: bool) -> Self {
151        self.user_plugins = should_initialize;
152        self
153    }
154
155    /// Set this to false if your use might be impacted by a repo-installed plugin.
156    pub fn with_repo_plugins(mut self, should_initialize: bool) -> Self {
157        self.repo_plugins = should_initialize;
158        self
159    }
160}
161
162impl Default for InitializationOptions {
163    fn default() -> Self {
164        Self {
165            license: None,
166            checkout_license: true,
167            register_main_thread_handler: true,
168            floating_license_duration: Duration::from_secs(900),
169            bundled_plugin_directory: bundled_plugin_directory()
170                .expect("Failed to get bundled plugin directory"),
171            user_plugins: false,
172            repo_plugins: false,
173        }
174    }
175}
176
177/// This initializes the core with the given [`InitializationOptions`].
178pub fn init_with_opts(options: InitializationOptions) -> Result<(), InitializationError> {
179    if is_ui_enabled() {
180        return Err(InitializationError::AlreadyManaged);
181    }
182
183    // If we are the main thread, that means there is no main thread, we should register a main thread handler.
184    if options.register_main_thread_handler && is_main_thread() {
185        let mut main_thread_handle = MAIN_THREAD_HANDLE.lock().unwrap();
186        if main_thread_handle.is_none() {
187            let (sender, receiver) = std::sync::mpsc::channel();
188            let main_thread = HeadlessMainThreadSender::new(sender);
189
190            // This thread will act as our main thread.
191            let join_handle = std::thread::Builder::new()
192                .name("HeadlessMainThread".to_string())
193                .spawn(move || {
194                    // We must register the main thread within the thread.
195                    main_thread.register();
196                    while let Ok(action) = receiver.recv() {
197                        action.execute();
198                    }
199                })?;
200
201            // Set the static MAIN_THREAD_HANDLER so that we can close the thread on shutdown.
202            *main_thread_handle = Some(join_handle);
203        }
204    }
205
206    if is_enterprise_product() && options.checkout_license {
207        // We are allowed to check out a license, so do it!
208        let checkout_status = enterprise::checkout_license(options.floating_license_duration)?;
209        if checkout_status == EnterpriseCheckoutStatus::AlreadyManaged {
210            // Should be impossible, but just in case.
211            return Err(InitializationError::AlreadyManaged);
212        }
213    }
214
215    if let Some(license) = &options.license {
216        // We were given a license override, use it!
217        set_license(Some(license));
218    }
219
220    set_bundled_plugin_directory(options.bundled_plugin_directory);
221
222    unsafe {
223        BNInitPlugins(options.user_plugins);
224        if options.repo_plugins {
225            // We are allowed to initialize repo plugins, so do it!
226            BNInitRepoPlugins();
227        }
228    }
229
230    if !is_license_validated() {
231        // Unfortunately, you must have a valid license to use Binary Ninja.
232        Err(InitializationError::InvalidLicense)
233    } else {
234        Ok(())
235    }
236}
237
238#[derive(Debug)]
239pub struct HeadlessMainThreadSender {
240    sender: Sender<Ref<MainThreadAction>>,
241}
242
243impl HeadlessMainThreadSender {
244    pub fn new(sender: Sender<Ref<MainThreadAction>>) -> Self {
245        Self { sender }
246    }
247}
248
249impl MainThreadHandler for HeadlessMainThreadSender {
250    fn add_action(&self, action: Ref<MainThreadAction>) {
251        self.sender
252            .send(action)
253            .expect("Failed to send action to main thread");
254    }
255}
256
257fn is_enterprise_product() -> bool {
258    matches!(
259        crate::product().as_str(),
260        "Binary Ninja Enterprise Client" | "Binary Ninja Ultimate"
261    )
262}
263
264#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
265pub enum LicenseLocation {
266    /// The license used when initializing will be the environment variable `BN_LICENSE`.
267    EnvironmentVariable,
268    /// The license used when initializing will be the file in the Binary Ninja user directory.
269    File,
270    /// The license is retrieved using keychain credentials, this is only available for floating enterprise licenses.
271    Keychain,
272}
273
274/// Attempts to identify the license location type, this follows the same order as core initialization.
275///
276/// This is useful if you want to know whether the core will use your license. If this returns `None`
277/// you should look into setting the `BN_LICENSE` environment variable or calling [`set_license`].
278pub fn license_location() -> Option<LicenseLocation> {
279    match std::env::var("BN_LICENSE") {
280        Ok(_) => Some(LicenseLocation::EnvironmentVariable),
281        Err(_) => {
282            // Check the license_path to see if a file is there.
283            if license_path().exists() {
284                Some(LicenseLocation::File)
285            } else if is_enterprise_product() {
286                // If we can't initialize enterprise, we probably are missing enterprise.server.url
287                // and our license surely is not valid.
288                if !enterprise::is_server_initialized() && !enterprise::initialize_server() {
289                    return None;
290                }
291                // If Enterprise thinks we are using a floating license, then report it will be in the keychain
292                enterprise::is_server_floating_license().then_some(LicenseLocation::Keychain)
293            } else {
294                // If we are not using an enterprise license, we can't check the keychain, nowhere else to check.
295                None
296            }
297        }
298    }
299}
300
301/// Wrapper for [`init`] and [`shutdown`]. Instantiating this at the top of your script will initialize everything correctly and then clean itself up at exit as well.
302#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
303pub struct Session {
304    license_duration: Option<Duration>,
305}
306
307impl Session {
308    /// Get a registered [`Session`] for use.
309    ///
310    /// This is required so that we can keep track of the [`SESSION_COUNT`].
311    fn registered_session() -> Self {
312        let _previous_count = SESSION_COUNT.fetch_add(1, SeqCst);
313        Self::default()
314    }
315
316    /// Before calling new you must make sure that the license is retrievable, otherwise the core won't be able to initialize.
317    ///
318    /// If you cannot otherwise provide a license via `BN_LICENSE_FILE` environment variable or the Binary Ninja user directory
319    /// you can call [`Session::new_with_opts`] instead of this function.
320    pub fn new() -> Result<Self, InitializationError> {
321        if license_location().is_some() {
322            // We were able to locate a license, continue with initialization.
323            Self::new_with_opts(InitializationOptions::default())
324        } else {
325            // There was no license that could be automatically retrieved, you must call [Self::new_with_license].
326            Err(InitializationError::NoLicenseFound)
327        }
328    }
329
330    /// Initialize with options, the same rules apply as [`Session::new`], see [`InitializationOptions::default`] for the regular options passed.
331    ///
332    /// This differs from [`Session::new`] in that it does not check to see if there is a license that the core
333    /// can discover by itself, therefore, it is expected that you know where your license is when calling this directly.
334    pub fn new_with_opts(options: InitializationOptions) -> Result<Self, InitializationError> {
335        let session = Self::registered_session();
336        init_with_opts(options)?;
337        Ok(session)
338    }
339
340    /// ```no_run
341    /// let headless_session = binaryninja::headless::Session::new().unwrap();
342    ///
343    /// let bv = headless_session
344    ///     .load("/bin/cat")
345    ///     .expect("Couldn't open `/bin/cat`");
346    /// ```
347    pub fn load(&self, file_path: impl AsRef<Path>) -> Option<Ref<binary_view::BinaryView>> {
348        crate::load(file_path)
349    }
350
351    /// Load the file with a progress callback, the callback will _only_ be called for BNDBs currently.
352    ///
353    /// ```no_run
354    /// let headless_session = binaryninja::headless::Session::new().unwrap();
355    ///
356    /// let print_progress = |progress, total| {
357    ///     println!("{}/{}", progress, total);
358    ///     true
359    /// };
360    ///
361    /// let bv = headless_session
362    ///     .load_with_progress("cat.bndb", print_progress)
363    ///     .expect("Couldn't open `cat.bndb`");
364    /// ```
365    pub fn load_with_progress(
366        &self,
367        file_path: impl AsRef<Path>,
368        progress: impl ProgressCallback,
369    ) -> Option<Ref<binary_view::BinaryView>> {
370        crate::load_with_progress(file_path, progress)
371    }
372
373    /// ```no_run
374    /// use binaryninja::{metadata::Metadata, rc::Ref};
375    /// use std::collections::HashMap;
376    ///
377    /// let settings: Ref<Metadata> =
378    ///     HashMap::from([("analysis.linearSweep.autorun", false.into())]).into();
379    /// let headless_session = binaryninja::headless::Session::new().unwrap();
380    ///
381    /// let bv = headless_session
382    ///     .load_with_options("/bin/cat", true, Some(settings))
383    ///     .expect("Couldn't open `/bin/cat`");
384    /// ```
385    pub fn load_with_options<O: IntoJson>(
386        &self,
387        file_path: impl AsRef<Path>,
388        update_analysis_and_wait: bool,
389        options: Option<O>,
390    ) -> Option<Ref<binary_view::BinaryView>> {
391        crate::load_with_options(file_path, update_analysis_and_wait, options)
392    }
393
394    /// Load the file with options and a progress callback, the callback will _only_ be called for BNDBs currently.
395    ///
396    /// ```no_run
397    /// use binaryninja::{metadata::Metadata, rc::Ref};
398    /// use std::collections::HashMap;
399    ///
400    /// let print_progress = |progress, total| {
401    ///     println!("{}/{}", progress, total);
402    ///     true
403    /// };
404    ///
405    /// let settings: Ref<Metadata> =
406    ///     HashMap::from([("analysis.linearSweep.autorun", false.into())]).into();
407    /// let headless_session = binaryninja::headless::Session::new().unwrap();
408    ///
409    /// let bv = headless_session
410    ///     .load_with_options_and_progress("cat.bndb", true, Some(settings), print_progress)
411    ///     .expect("Couldn't open `cat.bndb`");
412    /// ```
413    pub fn load_with_options_and_progress<O: IntoJson>(
414        &self,
415        file_path: impl AsRef<Path>,
416        update_analysis_and_wait: bool,
417        options: Option<O>,
418        progress: impl ProgressCallback,
419    ) -> Option<Ref<binary_view::BinaryView>> {
420        crate::load_with_options_and_progress(
421            file_path,
422            update_analysis_and_wait,
423            options,
424            progress,
425        )
426    }
427}
428
429impl Drop for Session {
430    fn drop(&mut self) {
431        let previous_count = SESSION_COUNT.fetch_sub(1, SeqCst);
432        if previous_count == 1 {
433            // We were the last session, therefore, we can safely shut down.
434            shutdown();
435        }
436    }
437}