Skip to content

Commit 42f1908

Browse files
committed
[Rust] Simplify enterprise initialization
We don't need to introduce extra global state when the goal is to not release floating licenses in shutdown.
1 parent fc16c03 commit 42f1908

File tree

3 files changed

+90
-98
lines changed

3 files changed

+90
-98
lines changed

rust/src/enterprise.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@ pub enum EnterpriseCheckoutError {
1919
RefreshExpiredLicenseFailed(String),
2020
}
2121

22+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
23+
pub enum EnterpriseCheckoutStatus {
24+
/// The UI is managing the enterprise checkout.
25+
AlreadyManaged,
26+
/// Checkout was successful, attached duration is the duration of the license, if any.
27+
Success(Option<Duration>),
28+
}
29+
2230
/// Initialize the enterprise server connection to check out a floating license.
2331
/// Result value is if we actually checked out a license (i.e. Ok(false) means we already have a
2432
/// license checked out and will not need to release it later)
25-
pub fn checkout_license(duration: Duration) -> Result<bool, EnterpriseCheckoutError> {
33+
pub fn checkout_license(
34+
duration: Duration,
35+
) -> Result<EnterpriseCheckoutStatus, EnterpriseCheckoutError> {
2636
if crate::is_ui_enabled() {
2737
// We only need to check out a license if running headlessly.
28-
return Ok(false);
38+
return Ok(EnterpriseCheckoutStatus::AlreadyManaged);
2939
}
3040

3141
// The disparate core functions we call here might already have mutexes to guard.
@@ -67,20 +77,19 @@ pub fn checkout_license(duration: Duration) -> Result<bool, EnterpriseCheckoutEr
6777
if !is_server_license_still_activated()
6878
|| (!is_server_floating_license() && crate::license_expiration_time() < SystemTime::now())
6979
{
70-
// If the license is expired we should refresh the license.
80+
// If the license is expired, we should refresh the license.
7181
if !update_server_license(duration) {
7282
let last_error = server_last_error().to_string();
7383
return Err(EnterpriseCheckoutError::RefreshExpiredLicenseFailed(
7484
last_error,
7585
));
7686
}
77-
Ok(true)
78-
} else {
79-
Ok(false)
8087
}
88+
89+
Ok(EnterpriseCheckoutStatus::Success(license_duration()))
8190
}
8291

83-
pub fn release_license() {
92+
pub fn release_license(release_floating: bool) {
8493
if !crate::is_ui_enabled() {
8594
// This might look dumb, why would we want to connect to the server, would that not just mean
8695
// we don't need to release the license? Well no, you could have run a script, acquired a license for 10 hours
@@ -92,6 +101,10 @@ pub fn release_license() {
92101
if !is_server_connected() {
93102
connect_server();
94103
}
104+
// We optionally release floating licenses as users typically want to keep them around.
105+
if is_server_floating_license() && !release_floating {
106+
return;
107+
}
95108
// We should only release the license if we are running headlessly.
96109
release_server_license();
97110
}
@@ -141,8 +154,14 @@ pub fn server_token() -> String {
141154
unsafe { BnString::into_string(binaryninjacore_sys::BNGetEnterpriseServerToken()) }
142155
}
143156

144-
pub fn license_duration() -> Duration {
145-
Duration::from_secs(unsafe { binaryninjacore_sys::BNGetEnterpriseServerLicenseDuration() })
157+
pub fn license_duration() -> Option<Duration> {
158+
let duration =
159+
Duration::from_secs(unsafe { binaryninjacore_sys::BNGetEnterpriseServerLicenseDuration() });
160+
match duration {
161+
// If the core returns 0 there is no license duration.
162+
Duration::ZERO => None,
163+
_ => Some(duration),
164+
}
146165
}
147166

148167
pub fn license_expiration_time() -> SystemTime {

rust/src/headless.rs

Lines changed: 60 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414

1515
use crate::{
1616
binary_view, bundled_plugin_directory, enterprise, is_license_validated, is_main_thread,
17-
license_path, set_bundled_plugin_directory, set_license, string::IntoJson,
17+
is_ui_enabled, license_path, set_bundled_plugin_directory, set_license, string::IntoJson,
1818
};
1919
use std::io;
2020
use std::path::{Path, PathBuf};
21+
use std::sync::atomic::AtomicUsize;
2122
use std::sync::atomic::Ordering::SeqCst;
22-
use std::sync::atomic::{AtomicBool, AtomicUsize};
2323
use thiserror::Error;
2424

25+
use crate::enterprise::EnterpriseCheckoutStatus;
2526
use crate::main_thread::{MainThreadAction, MainThreadHandler};
2627
use crate::progress::ProgressCallback;
2728
use crate::rc::Ref;
@@ -32,12 +33,9 @@ use std::thread::JoinHandle;
3233
use std::time::Duration;
3334

3435
static MAIN_THREAD_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
35-
/// Prevent two threads from calling init() at the same time
36-
static INIT_LOCK: Mutex<()> = Mutex::new(());
37-
/// Used to prevent shutting down Binary Ninja if there are other [`Session`]'s.
36+
37+
/// Used to prevent shutting down Binary Ninja if there is another active [`Session`].
3838
static SESSION_COUNT: AtomicUsize = AtomicUsize::new(0);
39-
/// If we checked out a floating license and should release it on shutdown
40-
static NEED_LICENSE_RELEASE: AtomicBool = AtomicBool::new(false);
4139

4240
#[derive(Error, Debug)]
4341
pub enum InitializationError {
@@ -49,15 +47,15 @@ pub enum InitializationError {
4947
InvalidLicense,
5048
#[error("no license could located, please see `binaryninja::set_license` for details")]
5149
NoLicenseFound,
52-
#[error("could not acquire initialization mutex")]
53-
InitMutex,
50+
#[error("initialization already managed by ui")]
51+
AlreadyManaged,
5452
}
5553

5654
/// Loads plugins, core architecture, platform, etc.
5755
///
5856
/// ⚠️ Important! Must be called at the beginning of scripts. Plugins do not need to call this. ⚠️
5957
///
60-
/// You can instead call this through [`Session`].
58+
/// The preferred method for core initialization is [`Session`], use that instead of this where possible.
6159
///
6260
/// If you need to customize initialization, use [`init_with_opts`] instead.
6361
pub fn init() -> Result<(), InitializationError> {
@@ -67,15 +65,12 @@ pub fn init() -> Result<(), InitializationError> {
6765

6866
/// Unloads plugins, stops all worker threads, and closes open logs.
6967
///
70-
/// If the core was initialized using an enterprise license, that will also be freed.
71-
///
72-
/// ⚠️ Important! Must be called at the end of scripts. ⚠️
68+
/// This function does _NOT_ release floating licenses; it is expected that you call [`enterprise::release_license`].
7369
pub fn shutdown() {
74-
match crate::product().to_string().as_str() {
70+
match crate::product().as_str() {
7571
"Binary Ninja Enterprise Client" | "Binary Ninja Ultimate" => {
76-
if NEED_LICENSE_RELEASE.load(SeqCst) {
77-
enterprise::release_license()
78-
}
72+
// By default, we do not release floating licenses.
73+
enterprise::release_license(false)
7974
}
8075
_ => {}
8176
}
@@ -91,9 +86,9 @@ pub fn is_shutdown_requested() -> bool {
9186
pub struct InitializationOptions {
9287
/// A license to override with, you can use this to make sure you initialize with a specific license.
9388
pub license: Option<String>,
94-
/// If you need to make sure that you do not check out a license set this to false.
89+
/// If you need to make sure that you do not check out a license, set this to false.
9590
///
96-
/// This is really only useful if you have a headless license but are using an enterprise enabled core.
91+
/// This is really only useful if you have a headless license but are using an enterprise-enabled core.
9792
pub checkout_license: bool,
9893
/// Whether to register the default main thread handler.
9994
///
@@ -105,11 +100,11 @@ pub struct InitializationOptions {
105100
pub bundled_plugin_directory: PathBuf,
106101
/// Whether to initialize user plugins.
107102
///
108-
/// Set this to false if your use might be impacted by a user installed plugin.
103+
/// Set this to false if your use might be impacted by a user-installed plugin.
109104
pub user_plugins: bool,
110105
/// Whether to initialize repo plugins.
111106
///
112-
/// Set this to false if your use might be impacted by a repo installed plugin.
107+
/// Set this to false if your use might be impacted by a repo-installed plugin.
113108
pub repo_plugins: bool,
114109
}
115110

@@ -129,9 +124,9 @@ impl InitializationOptions {
129124
self
130125
}
131126

132-
/// If you need to make sure that you do not check out a license set this to false.
127+
/// If you need to make sure that you do not check out a license, set this to false.
133128
///
134-
/// This is really only useful if you have a headless license but are using an enterprise enabled core.
129+
/// This is really only useful if you have a headless license but are using an enterprise-enabled core.
135130
pub fn with_license_checkout(mut self, should_checkout: bool) -> Self {
136131
self.checkout_license = should_checkout;
137132
self
@@ -151,13 +146,13 @@ impl InitializationOptions {
151146
self
152147
}
153148

154-
/// Set this to false if your use might be impacted by a user installed plugin.
149+
/// Set this to false if your use might be impacted by a user-installed plugin.
155150
pub fn with_user_plugins(mut self, should_initialize: bool) -> Self {
156151
self.user_plugins = should_initialize;
157152
self
158153
}
159154

160-
/// Set this to false if your use might be impacted by a repo installed plugin.
155+
/// Set this to false if your use might be impacted by a repo-installed plugin.
161156
pub fn with_repo_plugins(mut self, should_initialize: bool) -> Self {
162157
self.repo_plugins = should_initialize;
163158
self
@@ -181,7 +176,11 @@ impl Default for InitializationOptions {
181176

182177
/// This initializes the core with the given [`InitializationOptions`].
183178
pub fn init_with_opts(options: InitializationOptions) -> Result<(), InitializationError> {
184-
// If we are the main thread that means there is no main thread, we should register a main thread handler.
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.
185184
if options.register_main_thread_handler && is_main_thread() {
186185
let mut main_thread_handle = MAIN_THREAD_HANDLE.lock().unwrap();
187186
if main_thread_handle.is_none() {
@@ -192,7 +191,7 @@ pub fn init_with_opts(options: InitializationOptions) -> Result<(), Initializati
192191
let join_handle = std::thread::Builder::new()
193192
.name("HeadlessMainThread".to_string())
194193
.spawn(move || {
195-
// We must register the main thread within said thread.
194+
// We must register the main thread within the thread.
196195
main_thread.register();
197196
while let Ok(action) = receiver.recv() {
198197
action.execute();
@@ -204,16 +203,13 @@ pub fn init_with_opts(options: InitializationOptions) -> Result<(), Initializati
204203
}
205204
}
206205

207-
match crate::product().as_str() {
208-
"Binary Ninja Enterprise Client" | "Binary Ninja Ultimate" => {
209-
if options.checkout_license {
210-
// We are allowed to check out a license, so do it!
211-
if enterprise::checkout_license(options.floating_license_duration)? {
212-
NEED_LICENSE_RELEASE.store(true, SeqCst);
213-
}
214-
}
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);
215212
}
216-
_ => {}
217213
}
218214

219215
if let Some(license) = options.license {
@@ -232,7 +228,7 @@ pub fn init_with_opts(options: InitializationOptions) -> Result<(), Initializati
232228
}
233229

234230
if !is_license_validated() {
235-
// Unfortunately you must have a valid license to use Binary Ninja.
231+
// Unfortunately, you must have a valid license to use Binary Ninja.
236232
Err(InitializationError::InvalidLicense)
237233
} else {
238234
Ok(())
@@ -258,65 +254,63 @@ impl MainThreadHandler for HeadlessMainThreadSender {
258254
}
259255
}
260256

257+
fn is_enterprise_product() -> bool {
258+
match crate::product().as_str() {
259+
"Binary Ninja Enterprise Client" | "Binary Ninja Ultimate" => true,
260+
_ => false,
261+
}
262+
}
263+
261264
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
262265
pub enum LicenseLocation {
263266
/// The license used when initializing will be the environment variable `BN_LICENSE`.
264267
EnvironmentVariable,
265268
/// The license used when initializing will be the file in the Binary Ninja user directory.
266269
File,
267-
/// The license is retrieved using keychain credentials for `BN_ENTERPRISE_USERNAME`.
270+
/// The license is retrieved using keychain credentials, this is only available for floating enterprise licenses.
268271
Keychain,
269272
}
270273

271274
/// Attempts to identify the license location type, this follows the same order as core initialization.
272275
///
273276
/// This is useful if you want to know whether the core will use your license. If this returns `None`
274-
/// you should look setting the `BN_LICENSE` environment variable, or calling [`set_license`].
277+
/// you should look into setting the `BN_LICENSE` environment variable or calling [`set_license`].
275278
pub fn license_location() -> Option<LicenseLocation> {
276279
match std::env::var("BN_LICENSE") {
277280
Ok(_) => Some(LicenseLocation::EnvironmentVariable),
278281
Err(_) => {
279282
// Check the license_path to see if a file is there.
280283
if license_path().exists() {
281284
Some(LicenseLocation::File)
282-
} else {
283-
// Check to see if we might be authorizing with enterprise
284-
if crate::product().as_str() == "Binary Ninja Enterprise Client"
285-
|| crate::product().as_str() == "Binary Ninja Ultimate"
286-
{
287-
// If we can't initialize enterprise, we probably are missing enterprise.server.url
288-
// and our license surely is not valid.
289-
if !enterprise::is_server_initialized() && !enterprise::initialize_server() {
290-
return None;
291-
}
292-
// If Enterprise thinks we are using a floating license, then report it will be in the keychain
293-
if enterprise::is_server_floating_license() {
294-
Some(LicenseLocation::Keychain)
295-
} else {
296-
None
297-
}
298-
} else {
299-
None
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;
300290
}
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
301296
}
302297
}
303298
}
304299
}
305300

306301
/// 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)]
307303
pub struct Session {
308-
index: usize,
304+
license_duration: Option<Duration>,
309305
}
310306

311307
impl Session {
312308
/// Get a registered [`Session`] for use.
313309
///
314310
/// This is required so that we can keep track of the [`SESSION_COUNT`].
315311
fn registered_session() -> Self {
316-
let previous_count = SESSION_COUNT.fetch_add(1, SeqCst);
317-
Self {
318-
index: previous_count,
319-
}
312+
let _previous_count = SESSION_COUNT.fetch_add(1, SeqCst);
313+
Self::default()
320314
}
321315

322316
/// Before calling new you must make sure that the license is retrievable, otherwise the core won't be able to initialize.
@@ -326,19 +320,7 @@ impl Session {
326320
pub fn new() -> Result<Self, InitializationError> {
327321
if license_location().is_some() {
328322
// We were able to locate a license, continue with initialization.
329-
330-
// Grab the lock before initialization to prevent another thread from initializing
331-
// and racing the call to BNInitPlugins.
332-
let _lock = INIT_LOCK
333-
.lock()
334-
.map_err(|_| InitializationError::InitMutex)?;
335-
let session = Self::registered_session();
336-
// Since this whole section is locked, we're guaranteed to be index 0 if we're first.
337-
// Only the first thread hitting this should be allowed to call BNInitPlugins
338-
if session.index == 0 {
339-
init()?;
340-
}
341-
Ok(session)
323+
Self::new_with_opts(InitializationOptions::default())
342324
} else {
343325
// There was no license that could be automatically retrieved, you must call [Self::new_with_license].
344326
Err(InitializationError::NoLicenseFound)
@@ -348,19 +330,10 @@ impl Session {
348330
/// Initialize with options, the same rules apply as [`Session::new`], see [`InitializationOptions::default`] for the regular options passed.
349331
///
350332
/// This differs from [`Session::new`] in that it does not check to see if there is a license that the core
351-
/// can discover by itself, therefor it is expected that you know where your license is when calling this directly.
333+
/// can discover by itself, therefore, it is expected that you know where your license is when calling this directly.
352334
pub fn new_with_opts(options: InitializationOptions) -> Result<Self, InitializationError> {
353-
// Grab the lock before initialization to prevent another thread from initializing
354-
// and racing the call to BNInitPlugins.
355-
let _lock = INIT_LOCK
356-
.lock()
357-
.map_err(|_| InitializationError::InitMutex)?;
358335
let session = Self::registered_session();
359-
// Since this whole section is locked, we're guaranteed to be index 0 if we're first.
360-
// Only the first thread hitting this should be allowed to call BNInitPlugins
361-
if session.index == 0 {
362-
init_with_opts(options)?;
363-
}
336+
init_with_opts(options)?;
364337
Ok(session)
365338
}
366339

@@ -457,7 +430,7 @@ impl Drop for Session {
457430
fn drop(&mut self) {
458431
let previous_count = SESSION_COUNT.fetch_sub(1, SeqCst);
459432
if previous_count == 1 {
460-
// We were the last session, therefor we can safely shut down.
433+
// We were the last session, therefore, we can safely shut down.
461434
shutdown();
462435
}
463436
}

0 commit comments

Comments
 (0)