Skip to content

Commit 88167f7

Browse files
committed
Simplify rust enterprise initialization
We don't need to introduce extra global state when the goal is to not release floating licenses in shutdown.
1 parent 4dab79b commit 88167f7

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)