binaryninja/headless.rs
1// Copyright 2021-2026 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;
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 }
225
226 if !is_license_validated() {
227 // Unfortunately, you must have a valid license to use Binary Ninja.
228 Err(InitializationError::InvalidLicense)
229 } else {
230 Ok(())
231 }
232}
233
234#[derive(Debug)]
235pub struct HeadlessMainThreadSender {
236 sender: Sender<Ref<MainThreadAction>>,
237}
238
239impl HeadlessMainThreadSender {
240 pub fn new(sender: Sender<Ref<MainThreadAction>>) -> Self {
241 Self { sender }
242 }
243}
244
245impl MainThreadHandler for HeadlessMainThreadSender {
246 fn add_action(&self, action: Ref<MainThreadAction>) {
247 self.sender
248 .send(action)
249 .expect("Failed to send action to main thread");
250 }
251}
252
253fn is_enterprise_product() -> bool {
254 matches!(
255 crate::product().as_str(),
256 "Binary Ninja Enterprise Client" | "Binary Ninja Ultimate"
257 )
258}
259
260#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
261pub enum LicenseLocation {
262 /// The license used when initializing will be the environment variable `BN_LICENSE`.
263 EnvironmentVariable,
264 /// The license used when initializing will be the file in the Binary Ninja user directory.
265 File,
266 /// The license is retrieved using keychain credentials, this is only available for floating enterprise licenses.
267 Keychain,
268}
269
270/// Attempts to identify the license location type, this follows the same order as core initialization.
271///
272/// This is useful if you want to know whether the core will use your license. If this returns `None`
273/// you should look into setting the `BN_LICENSE` environment variable or calling [`set_license`].
274pub fn license_location() -> Option<LicenseLocation> {
275 match std::env::var("BN_LICENSE") {
276 Ok(_) => Some(LicenseLocation::EnvironmentVariable),
277 Err(_) => {
278 // Check the license_path to see if a file is there.
279 if license_path().exists() {
280 Some(LicenseLocation::File)
281 } else if is_enterprise_product() {
282 // If we can't initialize enterprise, we probably are missing enterprise.server.url
283 // and our license surely is not valid.
284 if !enterprise::is_server_initialized() && !enterprise::initialize_server() {
285 return None;
286 }
287 // If Enterprise thinks we are using a floating license, then report it will be in the keychain
288 enterprise::is_server_floating_license().then_some(LicenseLocation::Keychain)
289 } else {
290 // If we are not using an enterprise license, we can't check the keychain, nowhere else to check.
291 None
292 }
293 }
294 }
295}
296
297/// 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.
298#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
299pub struct Session {
300 license_duration: Option<Duration>,
301}
302
303impl Session {
304 /// Get a registered [`Session`] for use.
305 ///
306 /// This is required so that we can keep track of the [`SESSION_COUNT`].
307 fn registered_session() -> Self {
308 let _previous_count = SESSION_COUNT.fetch_add(1, SeqCst);
309 Self::default()
310 }
311
312 /// Before calling new you must make sure that the license is retrievable, otherwise the core won't be able to initialize.
313 ///
314 /// If you cannot otherwise provide a license via `BN_LICENSE_FILE` environment variable or the Binary Ninja user directory
315 /// you can call [`Session::new_with_opts`] instead of this function.
316 pub fn new() -> Result<Self, InitializationError> {
317 if license_location().is_some() {
318 // We were able to locate a license, continue with initialization.
319 Self::new_with_opts(InitializationOptions::default())
320 } else {
321 // There was no license that could be automatically retrieved, you must call [Self::new_with_license].
322 Err(InitializationError::NoLicenseFound)
323 }
324 }
325
326 /// Initialize with options, the same rules apply as [`Session::new`], see [`InitializationOptions::default`] for the regular options passed.
327 ///
328 /// This differs from [`Session::new`] in that it does not check to see if there is a license that the core
329 /// can discover by itself, therefore, it is expected that you know where your license is when calling this directly.
330 pub fn new_with_opts(options: InitializationOptions) -> Result<Self, InitializationError> {
331 let session = Self::registered_session();
332 init_with_opts(options)?;
333 Ok(session)
334 }
335
336 /// ```no_run
337 /// let headless_session = binaryninja::headless::Session::new().unwrap();
338 ///
339 /// let bv = headless_session
340 /// .load("/bin/cat")
341 /// .expect("Couldn't open `/bin/cat`");
342 /// ```
343 pub fn load(&self, file_path: impl AsRef<Path>) -> Option<Ref<binary_view::BinaryView>> {
344 crate::load(file_path)
345 }
346
347 /// Load the file with a progress callback, the callback will _only_ be called for BNDBs currently.
348 ///
349 /// ```no_run
350 /// let headless_session = binaryninja::headless::Session::new().unwrap();
351 ///
352 /// let print_progress = |progress, total| {
353 /// println!("{}/{}", progress, total);
354 /// true
355 /// };
356 ///
357 /// let bv = headless_session
358 /// .load_with_progress("cat.bndb", print_progress)
359 /// .expect("Couldn't open `cat.bndb`");
360 /// ```
361 pub fn load_with_progress(
362 &self,
363 file_path: impl AsRef<Path>,
364 progress: impl ProgressCallback,
365 ) -> Option<Ref<binary_view::BinaryView>> {
366 crate::load_with_progress(file_path, progress)
367 }
368
369 /// ```no_run
370 /// use binaryninja::{metadata::Metadata, rc::Ref};
371 /// use std::collections::HashMap;
372 ///
373 /// let settings: Ref<Metadata> =
374 /// HashMap::from([("analysis.linearSweep.autorun", false.into())]).into();
375 /// let headless_session = binaryninja::headless::Session::new().unwrap();
376 ///
377 /// let bv = headless_session
378 /// .load_with_options("/bin/cat", true, Some(settings))
379 /// .expect("Couldn't open `/bin/cat`");
380 /// ```
381 pub fn load_with_options<O: IntoJson>(
382 &self,
383 file_path: impl AsRef<Path>,
384 update_analysis_and_wait: bool,
385 options: Option<O>,
386 ) -> Option<Ref<binary_view::BinaryView>> {
387 crate::load_with_options(file_path, update_analysis_and_wait, options)
388 }
389
390 /// Load the file with options and a progress callback, the callback will _only_ be called for BNDBs currently.
391 ///
392 /// ```no_run
393 /// use binaryninja::{metadata::Metadata, rc::Ref};
394 /// use std::collections::HashMap;
395 ///
396 /// let print_progress = |progress, total| {
397 /// println!("{}/{}", progress, total);
398 /// true
399 /// };
400 ///
401 /// let settings: Ref<Metadata> =
402 /// HashMap::from([("analysis.linearSweep.autorun", false.into())]).into();
403 /// let headless_session = binaryninja::headless::Session::new().unwrap();
404 ///
405 /// let bv = headless_session
406 /// .load_with_options_and_progress("cat.bndb", true, Some(settings), print_progress)
407 /// .expect("Couldn't open `cat.bndb`");
408 /// ```
409 pub fn load_with_options_and_progress<O: IntoJson>(
410 &self,
411 file_path: impl AsRef<Path>,
412 update_analysis_and_wait: bool,
413 options: Option<O>,
414 progress: impl ProgressCallback,
415 ) -> Option<Ref<binary_view::BinaryView>> {
416 crate::load_with_options_and_progress(
417 file_path,
418 update_analysis_and_wait,
419 options,
420 progress,
421 )
422 }
423}
424
425impl Drop for Session {
426 fn drop(&mut self) {
427 let previous_count = SESSION_COUNT.fetch_sub(1, SeqCst);
428 if previous_count == 1 {
429 // We were the last session, therefore, we can safely shut down.
430 shutdown();
431 }
432 }
433}