Skip to content

Commit a3f9e99

Browse files
justindbaurHinton
andauthored
Add run_command_async (#993)
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> ## 📔 Objective <!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. --> - Bumps .NET to 8 (latest LTS) - Uses `[LibraryImport]` for auto-marshalling of function - Adds `run_command_async` method in `bitwarden-c` that allows for a non-blocking call that can notify the caller when the action is completed. - `Task`-ifies one of the C# SDK calls into the SDK. - This only updates one method to be async for now but all of them can be changed - This is a breaking change so would be a good idea before a `1.0.0` release. ## 📸 Screenshots <!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. --> ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Oscar Hinton <[email protected]>
1 parent 81dc653 commit a3f9e99

21 files changed

+459
-54
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-c/src/c.rs

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
use std::{ffi::CStr, os::raw::c_char, str};
1+
use std::{
2+
ffi::{CStr, CString},
3+
os::raw::c_char,
4+
str,
5+
};
26

37
use bitwarden_json::client::Client;
8+
use tokio::task::JoinHandle;
49

510
use crate::{box_ptr, ffi_ref};
611

@@ -28,6 +33,46 @@ pub extern "C" fn run_command(c_str_ptr: *const c_char, client_ptr: *const CClie
2833
}
2934
}
3035

36+
type OnCompletedCallback = unsafe extern "C" fn(result: *mut c_char) -> ();
37+
38+
#[no_mangle]
39+
pub extern "C" fn run_command_async(
40+
c_str_ptr: *const c_char,
41+
client_ptr: *const CClient,
42+
on_completed_callback: OnCompletedCallback,
43+
is_cancellable: bool,
44+
) -> *mut JoinHandle<()> {
45+
let client = unsafe { ffi_ref!(client_ptr) };
46+
let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes())
47+
.expect("Input should be a valid string")
48+
// Languages may assume that the string is collectable as soon as this method exits
49+
// but it's not since the request will be run in the background
50+
// so we need to make our own copy.
51+
.to_owned();
52+
53+
let join_handle = client.runtime.spawn(async move {
54+
let result = client.client.run_command(input_str.as_str()).await;
55+
let str_result = match std::ffi::CString::new(result) {
56+
Ok(cstr) => cstr.into_raw(),
57+
Err(_) => panic!("failed to return comment result: null encountered"),
58+
};
59+
60+
// run completed function
61+
unsafe {
62+
on_completed_callback(str_result);
63+
let _ = CString::from_raw(str_result);
64+
}
65+
});
66+
67+
// We only want to box the join handle the caller has said that they may want to cancel,
68+
// essentially promising to us that they will take care of the returned pointer.
69+
if is_cancellable {
70+
box_ptr!(join_handle)
71+
} else {
72+
std::ptr::null_mut()
73+
}
74+
}
75+
3176
// Init client, potential leak! You need to call free_mem after this!
3277
#[no_mangle]
3378
pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
@@ -56,3 +101,15 @@ pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
56101
pub extern "C" fn free_mem(client_ptr: *mut CClient) {
57102
std::mem::drop(unsafe { Box::from_raw(client_ptr) });
58103
}
104+
105+
#[no_mangle]
106+
pub extern "C" fn abort_and_free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
107+
let join_handle = unsafe { Box::from_raw(join_handle_ptr) };
108+
join_handle.abort();
109+
std::mem::drop(join_handle);
110+
}
111+
112+
#[no_mangle]
113+
pub extern "C" fn free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
114+
std::mem::drop(unsafe { Box::from_raw(join_handle_ptr) });
115+
}

crates/bitwarden-json/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@ schemars = { workspace = true }
2525
serde = { version = ">=1.0, <2.0", features = ["derive"] }
2626
serde_json = ">=1.0.96, <2.0"
2727

28+
[target.'cfg(debug_assertions)'.dependencies]
29+
tokio = { version = "1.36.0", features = ["time"] }
30+
2831
[lints]
2932
workspace = true

crates/bitwarden-json/src/client.rs

+28
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ impl Client {
8585
client.generator().password(req).into_string()
8686
}
8787
},
88+
#[cfg(debug_assertions)]
89+
Command::Debug(cmd) => {
90+
use bitwarden::Error;
91+
92+
use crate::command::DebugCommand;
93+
94+
match cmd {
95+
DebugCommand::CancellationTest { duration_millis } => {
96+
use tokio::time::sleep;
97+
let duration = std::time::Duration::from_millis(duration_millis);
98+
sleep(duration).await;
99+
println!("After wait #1");
100+
sleep(duration).await;
101+
println!("After wait #2");
102+
sleep(duration).await;
103+
println!("After wait #3");
104+
Ok::<i32, Error>(42).into_string()
105+
}
106+
DebugCommand::ErrorTest {} => {
107+
use bitwarden::Error;
108+
109+
Err::<i32, Error>(Error::Internal(std::borrow::Cow::Borrowed(
110+
"This is an error.",
111+
)))
112+
.into_string()
113+
}
114+
}
115+
}
88116
}
89117
}
90118

crates/bitwarden-json/src/command.rs

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub enum Command {
3333
Projects(ProjectsCommand),
3434
#[cfg(feature = "secrets")]
3535
Generators(GeneratorsCommand),
36+
#[cfg(debug_assertions)]
37+
Debug(DebugCommand),
3638
}
3739

3840
#[cfg(feature = "secrets")]
@@ -142,3 +144,11 @@ pub enum GeneratorsCommand {
142144
/// Returns: [String]
143145
GeneratePassword(PasswordGeneratorRequest),
144146
}
147+
148+
#[cfg(debug_assertions)]
149+
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
150+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
151+
pub enum DebugCommand {
152+
CancellationTest { duration_millis: u64 },
153+
ErrorTest {},
154+
}

languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net6.0</TargetFramework>
5+
<TargetFramework>net8.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
</PropertyGroup>

languages/csharp/Bitwarden.Sdk.Samples/Program.cs

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Bitwarden.Sdk;
1+
using Bitwarden.Sdk;
22

33
// Get environment variables
44
var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!;
@@ -15,10 +15,10 @@
1515
});
1616

1717
// Authenticate
18-
bitwardenClient.Auth.LoginAccessToken(accessToken, stateFile);
18+
await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile);
1919

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

3131
// Projects Create, Update, & Get
3232
Console.WriteLine("Creating and updating a project");
33-
var projectResponse = bitwardenClient.Projects.Create(organizationId, "NewTestProject");
34-
projectResponse = bitwardenClient.Projects.Update(organizationId, projectResponse.Id, "NewTestProject Renamed");
35-
projectResponse = bitwardenClient.Projects.Get(projectResponse.Id);
33+
var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject");
34+
projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed");
35+
projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id);
3636
Console.WriteLine("Here is the project we created and updated:");
3737
Console.WriteLine(projectResponse.Name);
3838

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

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

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

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

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

6767
// Secrets Sync
68-
var syncResponse = bitwardenClient.Secrets.Sync(organizationId, null);
68+
var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null);
6969

7070
// Secrets & Projects Delete
7171
Console.WriteLine("Deleting our secret and project");
72-
bitwardenClient.Secrets.Delete(new[] { secretResponse.Id });
73-
bitwardenClient.Projects.Delete(new[] { projectResponse.Id });
72+
await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id });
73+
await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
14+
<PackageReference Include="xunit" Version="2.4.2" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="6.0.0">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\Bitwarden.Sdk\Bitwarden.Sdk.csproj" />
27+
</ItemGroup>
28+
29+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Bitwarden.Sdk;
2+
using System.Diagnostics;
3+
4+
namespace Bitwarden.Sdk.Tests;
5+
6+
public class InteropTests
7+
{
8+
[Fact]
9+
public async void CancelingTest_ThrowsTaskCanceledException()
10+
{
11+
var client = new BitwardenClient();
12+
13+
var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250));
14+
15+
await Assert.ThrowsAsync<TaskCanceledException>(async () => await client.CancellationTestAsync(cts.Token));
16+
}
17+
18+
[Fact]
19+
public async void NoCancel_TaskCompletesSuccessfully()
20+
{
21+
var client = new BitwardenClient();
22+
23+
var result = await client.CancellationTestAsync(CancellationToken.None);
24+
Assert.Equal(42, result);
25+
}
26+
27+
[Fact]
28+
public async void Error_ThrowsException()
29+
{
30+
var client = new BitwardenClient();
31+
32+
var bitwardenException = await Assert.ThrowsAsync<BitwardenException>(async () => await client.ErrorTestAsync());
33+
Assert.Equal("Internal error: This is an error.", bitwardenException.Message);
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
namespace Bitwarden.Sdk.Tests;
2+
3+
public class SampleTests
4+
{
5+
[SecretsManagerFact]
6+
public async Task RunSample_Works()
7+
{
8+
// Get environment variables
9+
var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!;
10+
var apiUrl = Environment.GetEnvironmentVariable("API_URL")!;
11+
var organizationId = Guid.Parse(Environment.GetEnvironmentVariable("ORGANIZATION_ID")!);
12+
var accessToken = Environment.GetEnvironmentVariable("ACCESS_TOKEN")!;
13+
var stateFile = Environment.GetEnvironmentVariable("STATE_FILE")!;
14+
15+
// Create the SDK Client
16+
using var bitwardenClient = new BitwardenClient(new BitwardenSettings
17+
{
18+
ApiUrl = apiUrl,
19+
IdentityUrl = identityUrl
20+
});
21+
22+
// Authenticate
23+
await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile);
24+
25+
// Projects Create, Update, & Get
26+
var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject");
27+
projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed");
28+
projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id);
29+
30+
Assert.Equal("NewTestProject Renamed", projectResponse.Name);
31+
32+
var projectList = await bitwardenClient.Projects.ListAsync(organizationId);
33+
34+
Assert.True(projectList.Data.Count() >= 1);
35+
36+
// Secrets list
37+
var secretsList = await bitwardenClient.Secrets.ListAsync(organizationId);
38+
39+
// Secrets Create, Update, Get
40+
var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
41+
secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
42+
secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id);
43+
44+
Assert.Equal("New Secret Name", secretResponse.Key);
45+
46+
// Secrets GetByIds
47+
var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id });
48+
49+
// Secrets Sync
50+
var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null);
51+
52+
// Secrets & Projects Delete
53+
await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id });
54+
await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id });
55+
}
56+
}

0 commit comments

Comments
 (0)