Skip to content

Commit 58baecb

Browse files
release: ironposh-web v0.3.5
1 parent b778d58 commit 58baecb

File tree

13 files changed

+667
-50
lines changed

13 files changed

+667
-50
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironposh-client-tokio/src/main.rs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use futures::StreamExt;
99
use ironposh_async::RemoteAsyncPowershellClient;
1010
use ironposh_terminal::Terminal;
1111
use std::sync::Arc;
12-
use tracing::{error, info, instrument, warn};
12+
use tracing::{debug, error, info, instrument, warn};
1313

1414
use config::{create_connector_config, init_logging, Args};
1515
use http_client::ReqwestHttpClient;
@@ -76,12 +76,87 @@ async fn main() -> anyhow::Result<()> {
7676
// Spawn connection task
7777
let connection_handle = tokio::spawn(connection_task);
7878

79-
// Execute command
80-
let mut stream = client.send_script(command).await?;
79+
// Execute command (raw output to inspect PSValue representation)
80+
let mut stream = client.send_script_raw(command).await?;
8181

82-
#[expect(clippy::never_loop)]
8382
while let Some(event) = stream.next().await {
84-
unimplemented!("{event:?}");
83+
match event {
84+
ironposh_client_core::connector::active_session::UserEvent::PipelineCreated {
85+
pipeline,
86+
} => {
87+
info!(pipeline = ?pipeline, "pipeline created");
88+
}
89+
ironposh_client_core::connector::active_session::UserEvent::PipelineFinished {
90+
pipeline,
91+
} => {
92+
info!(pipeline = ?pipeline, "pipeline finished");
93+
}
94+
ironposh_client_core::connector::active_session::UserEvent::PipelineOutput {
95+
output,
96+
pipeline: _,
97+
} => {
98+
debug!(output = ?output, "pipeline output (raw)");
99+
match output.format_as_displyable_string() {
100+
Ok(text) => {
101+
println!("{text}");
102+
}
103+
Err(e) => {
104+
error!(error = %e, "failed to format pipeline output");
105+
println!("Error formatting output: {e}");
106+
}
107+
}
108+
}
109+
ironposh_client_core::connector::active_session::UserEvent::ErrorRecord {
110+
error_record,
111+
handle,
112+
} => {
113+
error!(
114+
pipeline = ?handle,
115+
error_record = ?error_record,
116+
"received error record"
117+
);
118+
println!("{}", error_record.render_concise());
119+
}
120+
ironposh_client_core::connector::active_session::UserEvent::PipelineRecord {
121+
record,
122+
pipeline: _,
123+
} => {
124+
use ironposh_client_core::psrp_record::PsrpRecord;
125+
debug!(record = ?record, "pipeline record (raw)");
126+
127+
match record {
128+
PsrpRecord::Debug { message, .. } => {
129+
println!("[debug] {message}");
130+
}
131+
PsrpRecord::Verbose { message, .. } => {
132+
println!("[verbose] {message}");
133+
}
134+
PsrpRecord::Warning { message, .. } => {
135+
println!("[warning] {message}");
136+
}
137+
PsrpRecord::Information { record, .. } => {
138+
let text = match record.message_data {
139+
ironposh_psrp::InformationMessageData::String(s) => s,
140+
ironposh_psrp::InformationMessageData::HostInformationMessage(m) => {
141+
m.message
142+
}
143+
ironposh_psrp::InformationMessageData::Object(v) => v.to_string(),
144+
};
145+
println!("[information] {text}");
146+
}
147+
PsrpRecord::Progress { record, .. } => {
148+
let status = record.status_description.unwrap_or_default();
149+
println!(
150+
"[progress] {}: {} ({}%)",
151+
record.activity, status, record.percent_complete
152+
);
153+
}
154+
PsrpRecord::Unsupported { data_preview, .. } => {
155+
println!("[unsupported] {data_preview}");
156+
}
157+
}
158+
}
159+
}
85160
}
86161
// Clean up
87162
connection_handle.abort();

crates/ironposh-psrp/src/ps_value/deserialize.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ impl<'a> XmlVisitor<'a> for PsPrimitiveValueVisitor<'a> {
8080
let text = node.text().unwrap_or("").to_string();
8181
self.value = Some(PsPrimitiveValue::DateTime(text));
8282
}
83+
"TS" => {
84+
let text = node.text().unwrap_or("").to_string();
85+
self.value = Some(PsPrimitiveValue::TimeSpan(text));
86+
}
8387
"G" => {
8488
let text = node.text().unwrap_or("").to_string();
8589
self.value = Some(PsPrimitiveValue::Guid(text));
@@ -440,7 +444,7 @@ impl<'a> PsXmlVisitor<'a> for ComplexObjectContextVisitor<'a> {
440444
}
441445
// Handle primitive content for ExtendedPrimitive objects
442446
"S" | "B" | "I32" | "U32" | "I64" | "U64" | "G" | "C" | "Nil" | "BA"
443-
| "Version" | "DT" => {
447+
| "Version" | "DT" | "TS" => {
444448
let primitive = PsPrimitiveValue::from_node(child)?;
445449
self.content = ComplexObjectContent::ExtendedPrimitive(primitive);
446450
}
@@ -535,7 +539,7 @@ impl<'a> PsXmlVisitor<'a> for PsValueContextVisitor<'a> {
535539
match tag_name {
536540
// Handle primitive values
537541
"S" | "B" | "I32" | "U32" | "I64" | "U64" | "G" | "C" | "Nil" | "BA" | "Version"
538-
| "DT" => {
542+
| "DT" | "TS" => {
539543
let primitive = PsPrimitiveValue::from_node(node)?;
540544
self.value = Some(PsValue::Primitive(primitive));
541545
}

crates/ironposh-psrp/src/ps_value/primitive.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub enum PsPrimitiveValue {
1818
SecureString(Vec<u8>),
1919
Version(String),
2020
DateTime(String), // Store as string for now
21+
TimeSpan(String), // Store as string for now
2122
// Add more primitive types as needed
2223
}
2324

@@ -37,6 +38,7 @@ impl Display for PsPrimitiveValue {
3738
Self::SecureString(_bytes) => write!(f, "System.Security.SecureString"),
3839
Self::Version(v) => write!(f, "{v}"),
3940
Self::DateTime(d) => write!(f, "{d}"),
41+
Self::TimeSpan(t) => write!(f, "{t}"),
4042
}
4143
}
4244
}

crates/ironposh-psrp/src/ps_value/serialize.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ impl<'a> PsPrimitiveValue {
6868
Self::SecureString(b) => Element::new("SS").set_text_owned(B64.encode(b)),
6969
Self::Version(v) => Element::new("Version").set_text_owned(v.clone()),
7070
Self::DateTime(dt) => Element::new("DT").set_text_owned(dt.clone()),
71+
Self::TimeSpan(ts) => Element::new("TS").set_text_owned(ts.clone()),
7172
})
7273
}
7374
}

crates/ironposh-web/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ironposh-web"
3-
version = "0.3.4"
3+
version = "0.3.5"
44
authors = ["irving ou <jou@devolutions.net>"]
55
edition = "2018"
66
description = "PowerShell Remoting over WinRM for WebAssembly"

crates/ironposh-web/src/client.rs

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ use crate::{
22
error::WasmError,
33
hostcall::handle_host_calls,
44
http_client::GatewayHttpViaWSClient,
5-
types::{SecurityWarningCallback, WasmCommandCompletion, WasmWinRmConfig},
6-
JsSessionEvent, WasmPowerShellStream,
5+
types::{
6+
JsRunCommandEvent, SecurityWarningCallback, WasmCommandCompletion, WasmHostInformationMessage,
7+
WasmInformationMessageData, WasmPsrpRecord, WasmPsrpRecordMeta, WasmWinRmConfig,
8+
},
9+
JsPsValue, JsSessionEvent, WasmErrorRecord, WasmPowerShellStream,
710
};
811
use futures::StreamExt;
912
use ironposh_async::RemoteAsyncPowershellClient;
10-
use ironposh_client_core::{connector::WinRmConfig, powershell::PipelineHandle};
13+
use ironposh_client_core::{
14+
connector::WinRmConfig, powershell::PipelineHandle, psrp_record::PsrpRecord,
15+
};
1116
use js_sys::{Array, Function, Promise};
1217
use std::convert::TryFrom;
1318
use tracing::{error, info, warn};
@@ -28,6 +33,9 @@ extern "C" {
2833

2934
#[wasm_bindgen(typescript_type = "(session_event: JsSessionEvent) => void")]
3035
pub type SessionEventHandler;
36+
37+
#[wasm_bindgen(typescript_type = "(event: JsRunCommandEvent) => void")]
38+
pub type RunCommandCallback;
3139
}
3240

3341
#[wasm_bindgen]
@@ -243,6 +251,41 @@ impl WasmPowerShellClient {
243251
Ok(stream)
244252
}
245253

254+
/// Execute a PowerShell script and emit structured pipeline events to the callback.
255+
/// This uses raw PSRP output (no Out-String) so callers can inspect JsPsValue.
256+
#[wasm_bindgen(js_name = "runCommand")]
257+
pub async fn run_command(
258+
&mut self,
259+
script: String,
260+
on_event: RunCommandCallback,
261+
) -> Result<(), WasmError> {
262+
if !on_event.is_function() {
263+
return Err(WasmError::InvalidArgument(
264+
"on_event must be a function".into(),
265+
));
266+
}
267+
268+
let script_len = script.len();
269+
tracing::info!(script_len = %script_len, "run_command requested");
270+
271+
let mut stream = self.client.send_script_raw(script).await.map_err(|e| {
272+
tracing::info!(error = ?e, "run_command failed to send script");
273+
e
274+
})?;
275+
276+
let callback = on_event.unchecked_into::<Function>();
277+
278+
while let Some(event) = stream.next().await {
279+
280+
let js_event = user_event_to_run_command_event(&event)?;
281+
if let Err(e) = callback.call1(&JsValue::NULL, &js_event.into()) {
282+
tracing::info!(error = ?e, "run_command callback failed");
283+
}
284+
}
285+
286+
Ok(())
287+
}
288+
246289
#[wasm_bindgen]
247290
pub async fn tab_complete(
248291
&mut self,
@@ -320,3 +363,123 @@ impl WasmPowerShellClient {
320363
})
321364
}
322365
}
366+
367+
fn user_event_to_run_command_event(
368+
event: &ironposh_client_core::connector::active_session::UserEvent,
369+
) -> Result<JsRunCommandEvent, WasmError> {
370+
let res = match event {
371+
ironposh_client_core::connector::active_session::UserEvent::PipelineCreated { pipeline } => {
372+
JsRunCommandEvent::PipelineCreated {
373+
pipeline_id: pipeline.id().to_string(),
374+
}
375+
}
376+
ironposh_client_core::connector::active_session::UserEvent::PipelineFinished { pipeline } => {
377+
JsRunCommandEvent::PipelineFinished {
378+
pipeline_id: pipeline.id().to_string(),
379+
}
380+
}
381+
ironposh_client_core::connector::active_session::UserEvent::PipelineOutput {
382+
pipeline,
383+
output,
384+
} => JsRunCommandEvent::PipelineOutput {
385+
pipeline_id: pipeline.id().to_string(),
386+
value: JsPsValue::from(output.data.clone()),
387+
},
388+
ironposh_client_core::connector::active_session::UserEvent::ErrorRecord {
389+
error_record,
390+
handle,
391+
} => JsRunCommandEvent::PipelineError {
392+
pipeline_id: handle.id().to_string(),
393+
error: WasmErrorRecord::from(error_record),
394+
},
395+
ironposh_client_core::connector::active_session::UserEvent::PipelineRecord {
396+
pipeline,
397+
record,
398+
} => JsRunCommandEvent::PipelineRecord {
399+
pipeline_id: pipeline.id().to_string(),
400+
record: psrp_record_to_wasm(record),
401+
},
402+
};
403+
404+
Ok(res)
405+
}
406+
407+
fn psrp_record_to_wasm(record: &PsrpRecord) -> WasmPsrpRecord {
408+
let meta = match record {
409+
PsrpRecord::Debug { meta, .. }
410+
| PsrpRecord::Verbose { meta, .. }
411+
| PsrpRecord::Warning { meta, .. }
412+
| PsrpRecord::Information { meta, .. }
413+
| PsrpRecord::Progress { meta, .. }
414+
| PsrpRecord::Unsupported { meta, .. } => meta,
415+
};
416+
417+
let meta = WasmPsrpRecordMeta {
418+
message_type: format!("{:?}", meta.message_type),
419+
message_type_value: meta.message_type_value,
420+
stream: meta.stream.clone(),
421+
command_id: meta.command_id.map(|id| id.to_string()),
422+
data_len: meta.data_len,
423+
};
424+
425+
match record {
426+
PsrpRecord::Debug { message, .. } => WasmPsrpRecord::Debug {
427+
meta,
428+
message: message.clone(),
429+
},
430+
PsrpRecord::Verbose { message, .. } => WasmPsrpRecord::Verbose {
431+
meta,
432+
message: message.clone(),
433+
},
434+
PsrpRecord::Warning { message, .. } => WasmPsrpRecord::Warning {
435+
meta,
436+
message: message.clone(),
437+
},
438+
PsrpRecord::Information { record, .. } => {
439+
let message_data = match &record.message_data {
440+
ironposh_psrp::InformationMessageData::String(s) => {
441+
WasmInformationMessageData::String { value: s.clone() }
442+
}
443+
ironposh_psrp::InformationMessageData::HostInformationMessage(m) => {
444+
WasmInformationMessageData::HostInformationMessage {
445+
value: WasmHostInformationMessage {
446+
message: m.message.clone(),
447+
foreground_color: m.foreground_color,
448+
background_color: m.background_color,
449+
no_new_line: m.no_new_line,
450+
},
451+
}
452+
}
453+
ironposh_psrp::InformationMessageData::Object(v) => {
454+
WasmInformationMessageData::Object {
455+
value: JsPsValue::from(v.clone()),
456+
}
457+
}
458+
};
459+
WasmPsrpRecord::Information {
460+
meta,
461+
message_data,
462+
source: record.source.clone(),
463+
time_generated: record.time_generated.clone(),
464+
tags: record.tags.clone(),
465+
user: record.user.clone(),
466+
computer: record.computer.clone(),
467+
process_id: record.process_id,
468+
}
469+
}
470+
PsrpRecord::Progress { record, .. } => WasmPsrpRecord::Progress {
471+
meta,
472+
activity: record.activity.clone(),
473+
activity_id: record.activity_id,
474+
status_description: record.status_description.clone(),
475+
current_operation: record.current_operation.clone(),
476+
parent_activity_id: record.parent_activity_id,
477+
percent_complete: record.percent_complete,
478+
seconds_remaining: record.seconds_remaining,
479+
},
480+
PsrpRecord::Unsupported { data_preview, .. } => WasmPsrpRecord::Unsupported {
481+
meta,
482+
data_preview: data_preview.clone(),
483+
},
484+
}
485+
}

crates/ironposh-web/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ pub mod error;
1212
pub mod hostcall;
1313
pub mod http_client;
1414
pub mod http_convert;
15+
pub mod runner;
1516
pub mod stream;
1617
pub mod types;
1718
pub mod websocket;
1819
pub mod ws_http_decoder;
1920

2021
// Re-export the main types for JS/TS
2122
pub use client::WasmPowerShellClient;
23+
pub use runner::WasmPowerShellRunner;
2224
pub use stream::WasmPowerShellStream;
2325
pub use types::*;
2426

0 commit comments

Comments
 (0)