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 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 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
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 @@
}
}

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"),

Check warning on line 57 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L39-L57

Added lines #L39 - L57 were not covered by tests
};

// 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.
justindbaur marked this conversation as resolved.
Show resolved Hide resolved
if is_cancellable {
box_ptr!(join_handle)

Check warning on line 72 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L61-L72

Added lines #L61 - L72 were not covered by tests
} else {
std::ptr::null_mut()

Check warning on line 74 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L74

Added line #L74 was not covered by tests
}
}

Check warning on line 76 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L76

Added line #L76 was not covered by tests

// 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 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);
}

Check warning on line 112 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L108-L112

Added lines #L108 - L112 were not covered by tests

#[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) });
}

Check warning on line 117 in crates/bitwarden-c/src/c.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-c/src/c.rs#L115-L117

Added lines #L115 - L117 were not covered by tests
4 changes: 3 additions & 1 deletion crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ validator = { version = "0.18.1", features = ["derive"] }
zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] }
zxcvbn = { version = ">=3.0.1, <4.0", optional = true }

[target.'cfg(debug_assertions)'.dependencies]
tokio = { version = "1.36.0", features = ["rt", "macros", "time"] }
Copy link
Member Author

Choose a reason for hiding this comment

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

I barely know what I did here, I need the time feature but only in debug, is this the right way to go?

Copy link
Member

Choose a reason for hiding this comment

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

It is, but I can't say I'm a fan of different behaviors on debug and release builds.

Copy link
Member Author

Choose a reason for hiding this comment

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

This feels like the exact thing a debug build is for, it helps you write tests to make sure you've built the language wrapper correctly. That said I was able to move it into the bitwarden-json crate instead of core.


[target.'cfg(all(not(target_arch="wasm32"), not(windows)))'.dependencies]
# By default, we use rustls as the TLS stack and rust-platform-verifier to support user-installed root certificates
# The only exception is WASM, as it just uses the browsers/node fetch
Expand All @@ -71,7 +74,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
Expand Up @@ -24,6 +24,28 @@
) -> 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> {
use std::time::Duration;

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)
}

Check warning on line 39 in crates/bitwarden-core/src/platform/client_platform.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/client_platform.rs#L29-L39

Added lines #L29 - L39 were not covered by tests

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

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

Check warning on line 48 in crates/bitwarden-core/src/platform/client_platform.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/client_platform.rs#L42-L48

Added lines #L42 - L48 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

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

Are these needed in the core crate? Consider moving them to a separate test crate, or bitwarden-c, or remove it completely.

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved them to bitwarden-json, if I moved it into bitwarden-c I would have to double parse the input string to check for special commands. Do you think that is alright?

Copy link
Member

Choose a reason for hiding this comment

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

Why not just expose two regular functions for it?

Copy link
Member Author

Choose a reason for hiding this comment

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

The cancellation test is mostly testing the very code written in run_command_async so we'd have to expose a method with very similar internals to that and repeat ourselves. The error test is testing that the C# wrapper properly handles how errors are given back to it through the bitwarden-json crate.

}

impl<'a> Client {
Expand Down
12 changes: 12 additions & 0 deletions crates/bitwarden-json/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@
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().into_string(),

Check warning on line 110 in crates/bitwarden-json/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-json/src/client.rs#L102-L110

Added lines #L102 - L110 were not covered by tests
}
}
}
}

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 @@
Projects(ProjectsCommand),
#[cfg(feature = "secrets")]
Generators(GeneratorsCommand),
#[cfg(debug_assertions)]
Debug(DebugCommand),
}

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

#[cfg(debug_assertions)]
#[derive(Serialize, Deserialize, JsonSchema, Debug)]

Check warning on line 195 in crates/bitwarden-json/src/command.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-json/src/command.rs#L195

Added line #L195 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum DebugCommand {
CancellationTest { duration_millis: u64 },
ErrorTest {},
}
Comment on lines +194 to +200
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