Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add run_command_async #993

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
520a958
Add `run_command_async`
justindbaur Aug 23, 2024
a913189
Remove Logs
justindbaur Aug 23, 2024
497979e
Formatting
justindbaur Aug 23, 2024
c5c8d17
Formatting
justindbaur Aug 23, 2024
2d2801b
Free String On Rust Side
justindbaur Aug 23, 2024
adb445f
Formatting
justindbaur Aug 25, 2024
60bf783
Merge remote-tracking branch 'origin/main' into add-run-command-async
justindbaur Sep 3, 2024
a58121a
Migrate All C# Clients to Async
justindbaur Sep 4, 2024
bb1bc1e
Support Cancellation
justindbaur Sep 4, 2024
9ed29b7
Remove Comment
justindbaur Sep 4, 2024
6014b51
Cleanup
justindbaur Sep 4, 2024
cb73715
Formatting
justindbaur Sep 4, 2024
c1dc017
More Formatting
justindbaur Sep 4, 2024
5787db5
Use More Local Import
justindbaur Sep 4, 2024
d28a9e2
Remove Unnecessary async
justindbaur Sep 4, 2024
3709f6d
Format
justindbaur Sep 4, 2024
d101914
Format Again...
justindbaur Sep 4, 2024
5c433eb
Try A Thing
justindbaur Sep 5, 2024
ec3004d
Pass `CancellationToken` to `SetCancelled`
justindbaur Sep 5, 2024
447d6e7
Update crates/bitwarden-c/src/c.rs
justindbaur Sep 10, 2024
7bcd6d7
Update languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs
justindbaur Sep 10, 2024
8b16ece
Move Debug Stuff To bitwarden-json
justindbaur Sep 10, 2024
9d3a052
Merge branch 'add-run-command-async' of github.com:bitwarden/sdk intoโ€ฆ
justindbaur Sep 10, 2024
78a1ad6
Reorder Deps
justindbaur Sep 10, 2024
38c7cc5
Format
justindbaur Sep 10, 2024
0c8566b
Add Sample Tests
justindbaur Sep 18, 2024
aaff4f5
Merge branch 'main' into add-run-command-async
justindbaur Oct 10, 2024
56853fe
Merge branch 'main' into add-run-command-async
justindbaur Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion crates/bitwarden-c/src/c.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::{ffi::CStr, os::raw::c_char, str};
use std::{
ffi::{CStr, CString},
os::raw::c_char,
str,
};

use bitwarden_json::client::Client;
use tokio::task::JoinHandle;

use crate::{box_ptr, ffi_ref};

Expand Down Expand Up @@ -28,6 +33,48 @@ pub extern "C" fn run_command(c_str_ptr: *const c_char, client_ptr: *const CClie
}
}

type OnCompletedCallback = unsafe extern "C" fn(result: *mut c_char) -> ();

#[no_mangle]
pub extern "C" fn run_command_async(
c_str_ptr: *const c_char,
client_ptr: *const CClient,
on_completed_callback: OnCompletedCallback,
is_cancellable: bool,
) -> *mut JoinHandle<()> {
let client = unsafe { ffi_ref!(client_ptr) };
let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes())
.expect("Input should be a valid string")
// Languages may assume that the string is collectable as soon as this method exits
// but it's not since the request will be run in the background
// so we need to make our own copy.
.to_owned();

let join_handle = client.runtime.spawn(async move {
let result = client.client.run_command(input_str.as_str()).await;
let str_result = match std::ffi::CString::new(result) {
Ok(cstr) => cstr.into_raw(),
Err(_) => panic!("failed to return comment result: null encountered"),
};

// run completed function
unsafe {
on_completed_callback(str_result);
let _ = CString::from_raw(str_result);
}
});

// We only want to box the join handle the caller
// has said that they may want to cancel, essentially
// promising to us that they will take care of the
// returned pointer.
if is_cancellable {
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
box_ptr!(join_handle)
} else {
std::ptr::null_mut()
}
}

// Init client, potential leak! You need to call free_mem after this!
#[no_mangle]
pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
Expand Down Expand Up @@ -56,3 +103,15 @@ pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
pub extern "C" fn free_mem(client_ptr: *mut CClient) {
std::mem::drop(unsafe { Box::from_raw(client_ptr) });
}

#[no_mangle]
pub extern "C" fn abort_and_free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
let join_handle = unsafe { Box::from_raw(join_handle_ptr) };
join_handle.abort();
std::mem::drop(join_handle);
}

#[no_mangle]
pub extern "C" fn free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
std::mem::drop(unsafe { Box::from_raw(join_handle_ptr) });
}
2 changes: 1 addition & 1 deletion crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ serde_repr = ">=0.1.12, <0.2"
sha1 = ">=0.10.5, <0.11"
sha2 = ">=0.10.6, <0.11"
thiserror = ">=1.0.40, <2.0"
tokio = { version = "1.36.0", features = ["rt", "macros"] }
uniffi = { version = "=0.28.1", optional = true, features = ["tokio"] }
uuid = { version = ">=1.3.3, <2.0", features = ["serde"] }
validator = { version = "0.18.1", features = ["derive"] }
Expand All @@ -71,7 +72,6 @@ reqwest = { version = ">=0.12.5, <0.13", features = [
[dev-dependencies]
bitwarden-crypto = { workspace = true }
rand_chacha = "0.3.1"
tokio = { version = "1.36.0", features = ["rt", "macros"] }
wiremock = "0.6.0"
zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] }

Expand Down
22 changes: 22 additions & 0 deletions crates/bitwarden-core/src/platform/client_platform.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use super::{
generate_fingerprint::{generate_fingerprint, generate_user_fingerprint},
get_user_api_key, FingerprintRequest, FingerprintResponse, SecretVerificationRequest,
Expand All @@ -24,6 +26,26 @@
) -> Result<UserApiKeyResponse> {
get_user_api_key(self.client, &input).await
}

#[cfg(debug_assertions)]
pub async fn cancellation_test(&mut self, duration_millis: u64) -> Result<i32> {
tokio::time::sleep(Duration::from_millis(duration_millis)).await;
println!("After wait #1");
tokio::time::sleep(Duration::from_millis(duration_millis)).await;
println!("After wait #2");
tokio::time::sleep(Duration::from_millis(duration_millis)).await;
println!("After wait #3");
Ok(42)
}

#[cfg(debug_assertions)]
pub async fn error_test(&mut self) -> Result<i32> {
use crate::Error;

Err(Error::Internal(std::borrow::Cow::Borrowed(
"This is an error.",
)))
}
Fixed Show fixed Hide fixed
}

impl<'a> Client {
Expand Down
14 changes: 14 additions & 0 deletions crates/bitwarden-json/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ impl Client {
client.generator().password(req).into_string()
}
},
#[cfg(debug_assertions)]
Command::Debug(cmd) => {
use crate::command::DebugCommand;
match cmd {
DebugCommand::CancellationTest { duration_millis } => client
.platform()
.cancellation_test(duration_millis)
.await
.into_string(),
DebugCommand::ErrorTest {} => {
client.platform().error_test().await.into_string()
}
}
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions crates/bitwarden-json/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub enum Command {
Projects(ProjectsCommand),
#[cfg(feature = "secrets")]
Generators(GeneratorsCommand),
#[cfg(debug_assertions)]
Debug(DebugCommand),
}

#[cfg(feature = "secrets")]
Expand Down Expand Up @@ -188,3 +190,11 @@ pub enum GeneratorsCommand {
/// Returns: [String]
GeneratePassword(PasswordGeneratorRequest),
}

#[cfg(debug_assertions)]
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum DebugCommand {
CancellationTest { duration_millis: u64 },
ErrorTest {},
}
Comment on lines +148 to +154
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative way. of doing this would be to add one or two separate commands to bitwarden-c.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll let @bitwarden/team-secrets-manager-dev decide if they want to accept net 8 as the new target.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
28 changes: 14 additions & 14 deletions languages/csharp/Bitwarden.Sdk.Samples/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
๏ปฟusing Bitwarden.Sdk;
using Bitwarden.Sdk;

// Get environment variables
var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!;
Expand All @@ -15,10 +15,10 @@
});

// Authenticate
bitwardenClient.Auth.LoginAccessToken(accessToken, stateFile);
await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile);

// Projects List
var projectsList = bitwardenClient.Projects.List(organizationId).Data;
var projectsList = (await bitwardenClient.Projects.ListAsync(organizationId)).Data;
Console.WriteLine("A list of all projects:");
foreach (ProjectResponse pr in projectsList)
{
Expand All @@ -30,17 +30,17 @@

// Projects Create, Update, & Get
Console.WriteLine("Creating and updating a project");
var projectResponse = bitwardenClient.Projects.Create(organizationId, "NewTestProject");
projectResponse = bitwardenClient.Projects.Update(organizationId, projectResponse.Id, "NewTestProject Renamed");
projectResponse = bitwardenClient.Projects.Get(projectResponse.Id);
var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject");
projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed");
projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id);
Console.WriteLine("Here is the project we created and updated:");
Console.WriteLine(projectResponse.Name);

Console.Write("Press enter to continue...");
Console.ReadLine();

// Secrets list
var secretsList = bitwardenClient.Secrets.List(organizationId).Data;
var secretsList = (await bitwardenClient.Secrets.ListAsync(organizationId)).Data;
Console.WriteLine("A list of all secrets:");
foreach (SecretIdentifierResponse sr in secretsList)
{
Expand All @@ -52,22 +52,22 @@

// Secrets Create, Update, Get
Console.WriteLine("Creating and updating a secret");
var secretResponse = bitwardenClient.Secrets.Create(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
secretResponse = bitwardenClient.Secrets.Update(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
secretResponse = bitwardenClient.Secrets.Get(secretResponse.Id);
var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id);
Console.WriteLine("Here is the secret we created and updated:");
Console.WriteLine(secretResponse.Key);

Console.Write("Press enter to continue...");
Console.ReadLine();

// Secrets GetByIds
var secretsResponse = bitwardenClient.Secrets.GetByIds(new[] { secretResponse.Id });
var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id });

// Secrets Sync
var syncResponse = bitwardenClient.Secrets.Sync(organizationId, null);
var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null);

// Secrets & Projects Delete
Console.WriteLine("Deleting our secret and project");
bitwardenClient.Secrets.Delete(new[] { secretResponse.Id });
bitwardenClient.Projects.Delete(new[] { projectResponse.Id });
await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id });
await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id });
29 changes: 29 additions & 0 deletions languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Bitwarden.Sdk\Bitwarden.Sdk.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 35 additions & 0 deletions languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Bitwarden.Sdk;
using System.Diagnostics;

namespace Bitwarden.Sdk.Tests;

public class InteropTests
{
[Fact]
public async void CancelingTest_ThrowsTaskCanceledException()
{
var client = new BitwardenClient();

var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250));

await Assert.ThrowsAsync<TaskCanceledException>(async () => await client.CancellationTestAsync(cts.Token));
}

[Fact]
public async void NoCancel_TaskCompletesSuccessfully()
{
var client = new BitwardenClient();

var result = await client.CancellationTestAsync(CancellationToken.None);
Assert.Equal(42, result);
}

[Fact]
public async void Error_ThrowsException()
{
var client = new BitwardenClient();

var bitwardenException = await Assert.ThrowsAsync<BitwardenException>(async () => await client.ErrorTestAsync());
Assert.Equal("Internal error: This is an error.", bitwardenException.Message);
}
}
4 changes: 2 additions & 2 deletions languages/csharp/Bitwarden.Sdk/AuthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ internal AuthClient(CommandRunner commandRunner)
_commandRunner = commandRunner;
}

public void LoginAccessToken(string accessToken, string stateFile = "")
public async Task LoginAccessTokenAsync(string accessToken, string stateFile = "", CancellationToken cancellationToken = default)
{
var command = new Command { LoginAccessToken = new AccessTokenLoginRequest { AccessToken = accessToken, StateFile = stateFile } };
var response = _commandRunner.RunCommand<ResponseForApiKeyLoginResponse>(command);
var response = await _commandRunner.RunCommandAsync<ResponseForApiKeyLoginResponse>(command, cancellationToken);
if (response is not { Success: true })
{
throw new BitwardenAuthException(response != null ? response.ErrorMessage : "Login failed");
Expand Down
8 changes: 5 additions & 3 deletions languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Hinton marked this conversation as resolved.
Show resolved Hide resolved
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Bitwarden.Sdk</RootNamespace>

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Hinton marked this conversation as resolved.
Show resolved Hide resolved
<Title>Bitwarden Secrets Manager SDK</Title>
<Authors>Bitwarden Inc.</Authors>
<Description>.NET bindings for interacting with the Bitwarden Secrets Manager</Description>
<Copyright>Bitwarden Inc.</Copyright>
<Product>SDK</Product>

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>

<RepositoryUrl>https://github.com/bitwarden/sdk/tree/main/languages/csharp</RepositoryUrl>
<RepositoryType>Git</RepositoryType>

Expand Down Expand Up @@ -74,4 +76,4 @@
<PackagePath>runtimes/win-x64/native</PackagePath>
</Content>
</ItemGroup>
</Project>
</Project>
Loading
Loading