running-process 3.3.0__tar.gz → 3.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {running_process-3.3.0 → running_process-3.4.1}/Cargo.lock +18 -7
  2. {running_process-3.3.0 → running_process-3.4.1}/Cargo.toml +1 -1
  3. {running_process-3.3.0 → running_process-3.4.1}/PKG-INFO +1 -1
  4. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-client/Cargo.toml +1 -1
  5. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-client/src/client.rs +148 -7
  6. running_process-3.4.1/crates/running-process-client/src/lib.rs +11 -0
  7. running_process-3.4.1/crates/running-process-client/src/pipe_session.rs +359 -0
  8. running_process-3.4.1/crates/running-process-client/src/pty_session.rs +464 -0
  9. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/Cargo.toml +1 -1
  10. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/lib.rs +298 -10
  11. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/public_symbols.rs +11 -0
  12. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/process_core_test.rs +143 -0
  13. running_process-3.4.1/crates/running-process-proto/proto/daemon.proto +780 -0
  14. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/Cargo.toml +3 -3
  15. {running_process-3.3.0 → running_process-3.4.1}/pyproject.toml +1 -1
  16. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/__init__.py +1 -1
  17. running_process-3.4.1/src/running_process/pty/__init__.py +152 -0
  18. running_process-3.4.1/src/running_process/pty/_command.py +124 -0
  19. running_process-3.4.1/src/running_process/pty/_console_io.py +107 -0
  20. running_process-3.4.1/src/running_process/pty/_errors.py +39 -0
  21. running_process-3.4.1/src/running_process/pty/_idle_helpers.py +288 -0
  22. running_process-3.4.1/src/running_process/pty/_idle_state.py +51 -0
  23. running_process-3.4.1/src/running_process/pty/_interactive.py +184 -0
  24. running_process-3.4.1/src/running_process/pty/_process_helpers.py +34 -0
  25. running_process-3.3.0/src/running_process/pty.py → running_process-3.4.1/src/running_process/pty/_pseudo_terminal.py +49 -1073
  26. running_process-3.4.1/src/running_process/pty/_terminal_strip.py +130 -0
  27. running_process-3.4.1/src/running_process/pty/_types.py +171 -0
  28. running_process-3.4.1/src/running_process/pty/_wait_input.py +28 -0
  29. running_process-3.4.1/src/running_process/running_process/__init__.py +21 -0
  30. running_process-3.3.0/src/running_process/running_process.py → running_process-3.4.1/src/running_process/running_process/_core.py +21 -279
  31. running_process-3.4.1/src/running_process/running_process/_helpers.py +113 -0
  32. running_process-3.4.1/src/running_process/running_process/_iter.py +64 -0
  33. running_process-3.4.1/src/running_process/running_process/_subprocess.py +33 -0
  34. running_process-3.4.1/src/running_process/running_process/_types.py +104 -0
  35. running_process-3.3.0/crates/running-process-client/src/lib.rs +0 -7
  36. running_process-3.3.0/crates/running-process-proto/proto/daemon.proto +0 -377
  37. {running_process-3.3.0 → running_process-3.4.1}/LICENSE +0 -0
  38. {running_process-3.3.0 → running_process-3.4.1}/README.md +0 -0
  39. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-client/src/paths.rs +0 -0
  40. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/console_detect.rs +0 -0
  41. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/containment.rs +0 -0
  42. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/originator.rs +0 -0
  43. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/pty/mod.rs +0 -0
  44. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/pty/native_pty_process.rs +0 -0
  45. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/pty/pty_posix.rs +0 -0
  46. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/pty/pty_windows.rs +0 -0
  47. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/pty/terminal_input.rs +0 -0
  48. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/rust_debug.rs +0 -0
  49. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/spawn.rs +0 -0
  50. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/spawn_imp_unix.rs +0 -0
  51. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/spawn_imp_windows.rs +0 -0
  52. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/src/tests.rs +0 -0
  53. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/containment_test.rs +0 -0
  54. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/fs_adversarial_test.rs +0 -0
  55. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/originator_test.rs +0 -0
  56. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/pty_conhost_job_test.rs +0 -0
  57. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-core/tests/spawn_test.rs +0 -0
  58. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-proto/Cargo.toml +0 -0
  59. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-proto/build.rs +0 -0
  60. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-proto/src/lib.rs +0 -0
  61. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/containment.rs +0 -0
  62. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/daemon_client.rs +0 -0
  63. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/debug_traces.rs +0 -0
  64. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/helpers.rs +0 -0
  65. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/idle_detector.rs +0 -0
  66. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/lib.rs +0 -0
  67. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/metrics.rs +0 -0
  68. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/originator.rs +0 -0
  69. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/pid_tracking.rs +0 -0
  70. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/priority.rs +0 -0
  71. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/process.rs +0 -0
  72. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/process_tree.rs +0 -0
  73. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/pty_buffer.rs +0 -0
  74. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/pty_process.rs +0 -0
  75. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/public_symbols.rs +0 -0
  76. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/py_native_process.rs +0 -0
  77. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/registry.rs +0 -0
  78. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/signal_bool.rs +0 -0
  79. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/terminal_input.rs +0 -0
  80. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/control_churn.rs +0 -0
  81. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/expect_match.rs +0 -0
  82. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/idle_detector.rs +0 -0
  83. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/mod.rs +0 -0
  84. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/parse_command.rs +0 -0
  85. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/process_tree.rs +0 -0
  86. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/pty_buffer.rs +0 -0
  87. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/pty_process.rs +0 -0
  88. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/registry.rs +0 -0
  89. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/signal_bool.rs +0 -0
  90. {running_process-3.3.0 → running_process-3.4.1}/crates/running-process-py/src/tests/terminal_input.rs +0 -0
  91. {running_process-3.3.0 → running_process-3.4.1}/crates/test-watchdog/Cargo.toml +0 -0
  92. {running_process-3.3.0 → running_process-3.4.1}/crates/test-watchdog/src/lib.rs +0 -0
  93. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/assets/example.txt +0 -0
  94. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/cli.py +0 -0
  95. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/command_render.py +0 -0
  96. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/compat.py +0 -0
  97. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/console_encoding.py +0 -0
  98. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/daemon.py +0 -0
  99. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/dashboard.py +0 -0
  100. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/exit_status.py +0 -0
  101. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/expect.py +0 -0
  102. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/interrupt_handler.py +0 -0
  103. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/launch.py +0 -0
  104. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/line_iterator.py +0 -0
  105. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/output_formatter.py +0 -0
  106. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/priority.py +0 -0
  107. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/process_utils.py +0 -0
  108. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/processor_cli.py +0 -0
  109. {running_process-3.3.0 → running_process-3.4.1}/src/running_process/running_process_manager.py +0 -0
@@ -210,7 +210,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
210
210
 
211
211
  [[package]]
212
212
  name = "daemon-trampoline"
213
- version = "3.3.0"
213
+ version = "3.4.1"
214
214
  dependencies = [
215
215
  "libc",
216
216
  "serde",
@@ -1036,7 +1036,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
1036
1036
 
1037
1037
  [[package]]
1038
1038
  name = "running-process-client"
1039
- version = "3.3.0"
1039
+ version = "3.4.1"
1040
1040
  dependencies = [
1041
1041
  "dirs",
1042
1042
  "interprocess",
@@ -1047,7 +1047,7 @@ dependencies = [
1047
1047
 
1048
1048
  [[package]]
1049
1049
  name = "running-process-core"
1050
- version = "3.3.0"
1050
+ version = "3.4.1"
1051
1051
  dependencies = [
1052
1052
  "libc",
1053
1053
  "portable-pty",
@@ -1061,7 +1061,7 @@ dependencies = [
1061
1061
 
1062
1062
  [[package]]
1063
1063
  name = "running-process-daemon"
1064
- version = "3.3.0"
1064
+ version = "3.4.1"
1065
1065
  dependencies = [
1066
1066
  "bytes",
1067
1067
  "clap",
@@ -1088,7 +1088,7 @@ dependencies = [
1088
1088
 
1089
1089
  [[package]]
1090
1090
  name = "running-process-proto"
1091
- version = "3.3.0"
1091
+ version = "3.4.1"
1092
1092
  dependencies = [
1093
1093
  "prost",
1094
1094
  "prost-build",
@@ -1098,7 +1098,7 @@ dependencies = [
1098
1098
 
1099
1099
  [[package]]
1100
1100
  name = "running-process-py"
1101
- version = "3.3.0"
1101
+ version = "3.4.1"
1102
1102
  dependencies = [
1103
1103
  "interprocess",
1104
1104
  "libc",
@@ -1114,7 +1114,7 @@ dependencies = [
1114
1114
 
1115
1115
  [[package]]
1116
1116
  name = "runpm-cli"
1117
- version = "3.3.0"
1117
+ version = "3.4.1"
1118
1118
  dependencies = [
1119
1119
  "anyhow",
1120
1120
  "clap",
@@ -1368,6 +1368,10 @@ dependencies = [
1368
1368
  "running-process-core",
1369
1369
  ]
1370
1370
 
1371
+ [[package]]
1372
+ name = "testbin-emitter"
1373
+ version = "0.0.0"
1374
+
1371
1375
  [[package]]
1372
1376
  name = "testbin-env-dump"
1373
1377
  version = "0.0.0"
@@ -1387,6 +1391,13 @@ dependencies = [
1387
1391
  "running-process-core",
1388
1392
  ]
1389
1393
 
1394
+ [[package]]
1395
+ name = "testbin-stubborn"
1396
+ version = "0.0.0"
1397
+ dependencies = [
1398
+ "libc",
1399
+ ]
1400
+
1390
1401
  [[package]]
1391
1402
  name = "thiserror"
1392
1403
  version = "1.0.69"
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/running-process-core", "crates/running-process-proto", "crates/running-process-client", "crates/running-process-py", "crates/test-watchdog"]
4
4
 
5
5
  [workspace.package]
6
- version = "3.3.0"
6
+ version = "3.4.1"
7
7
  edition = "2021"
8
8
  rust-version = "1.85"
9
9
  license = "BSD-3-Clause"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: running_process
3
- Version: 3.3.0
3
+ Version: 3.4.1
4
4
  License-File: LICENSE
5
5
  Summary: A Rust-backed subprocess wrapper with split stdout/stderr streaming
6
6
  Home-Page: https://github.com/zackees/running-process
@@ -9,7 +9,7 @@ homepage.workspace = true
9
9
  description = "Lightweight synchronous IPC client for the running-process daemon"
10
10
 
11
11
  [dependencies]
12
- running-process-proto = { path = "../running-process-proto", version = "3.3.0" }
12
+ running-process-proto = { path = "../running-process-proto", version = "3.4.1" }
13
13
  prost = "0.14"
14
14
  interprocess = "2"
15
15
  dirs = "6"
@@ -9,12 +9,14 @@ use interprocess::local_socket::Stream;
9
9
  use interprocess::TryClone;
10
10
  use prost::Message;
11
11
  use running_process_proto::daemon::{
12
- DaemonRequest, DaemonResponse, GetProcessTreeRequest, KeyValue, KillTreeRequest,
13
- KillZombiesRequest, ListActiveRequest, ListByOriginatorRequest, PingRequest, RequestType,
14
- ServiceConfig, ServiceDeleteRequest, ServiceDescribeRequest, ServiceFlushRequest,
15
- ServiceListRequest, ServiceLogsRequest, ServiceRestartRequest, ServiceResurrectRequest,
16
- ServiceSaveRequest, ServiceStartRequest, ServiceStopRequest, ShutdownRequest,
17
- SpawnDaemonRequest as ProtoSpawnDaemonRequest, StatusCode, StatusRequest,
12
+ BulkTerminateSessionsRequest, BulkTerminateSessionsResponse, DaemonRequest, DaemonResponse,
13
+ GetProcessTreeRequest, GetSessionBacklogRequest, GetSessionBacklogResponse, KeyValue,
14
+ KillTreeRequest, KillZombiesRequest, ListActiveRequest, ListByOriginatorRequest, PingRequest,
15
+ PipeStreamKind, PurgeExitedSessionsRequest, PurgeExitedSessionsResponse, RequestType,
16
+ ResizePtySessionRequest, ServiceConfig, ServiceDeleteRequest, ServiceDescribeRequest,
17
+ ServiceFlushRequest, ServiceListRequest, ServiceLogsRequest, ServiceRestartRequest,
18
+ ServiceResurrectRequest, ServiceSaveRequest, ServiceStartRequest, ServiceStopRequest,
19
+ ShutdownRequest, SpawnDaemonRequest as ProtoSpawnDaemonRequest, StatusCode, StatusRequest,
18
20
  };
19
21
  use std::io::{BufReader, BufWriter, Read, Write};
20
22
  use std::path::PathBuf;
@@ -270,7 +272,7 @@ impl DaemonClient {
270
272
  // -----------------------------------------------------------------------
271
273
 
272
274
  /// Allocate the next request ID.
273
- fn next_request_id(&self) -> u64 {
275
+ pub(crate) fn next_request_id(&self) -> u64 {
274
276
  self.next_id.fetch_add(1, Ordering::Relaxed)
275
277
  }
276
278
 
@@ -615,6 +617,145 @@ impl DaemonClient {
615
617
  };
616
618
  self.send_request(request)
617
619
  }
620
+
621
+ /// Resize a PTY session without going through an attach
622
+ /// (#130 M5 follow-up). The new size persists for the lifetime of
623
+ /// the session; subsequent attaches can override it via their own
624
+ /// rows/cols fields.
625
+ pub fn resize_pty_session(
626
+ &mut self,
627
+ session_id: &str,
628
+ rows: u16,
629
+ cols: u16,
630
+ ) -> Result<(), ClientError> {
631
+ let request = DaemonRequest {
632
+ id: self.next_request_id(),
633
+ r#type: RequestType::ResizePtySession.into(),
634
+ protocol_version: 1,
635
+ client_name: String::from("running-process-client"),
636
+ resize_pty_session: Some(ResizePtySessionRequest {
637
+ session_id: session_id.into(),
638
+ rows: rows as u32,
639
+ cols: cols as u32,
640
+ }),
641
+ ..Default::default()
642
+ };
643
+ let response = self.send_request(request)?;
644
+ if response.code != StatusCode::Ok as i32 {
645
+ let code =
646
+ StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
647
+ return Err(ClientError::Server {
648
+ code,
649
+ message: response.message,
650
+ });
651
+ }
652
+ Ok(())
653
+ }
654
+
655
+ /// Purge exited sessions from both daemon-side registries (#130 M9
656
+ /// H4). Returns counts of PTY and pipe sessions reaped.
657
+ pub fn purge_exited_sessions(
658
+ &mut self,
659
+ originator: &str,
660
+ ) -> Result<PurgeExitedSessionsResponse, ClientError> {
661
+ let request = DaemonRequest {
662
+ id: self.next_request_id(),
663
+ r#type: RequestType::PurgeExitedSessions.into(),
664
+ protocol_version: 1,
665
+ client_name: String::from("running-process-client"),
666
+ purge_exited_sessions: Some(PurgeExitedSessionsRequest {
667
+ originator: originator.into(),
668
+ }),
669
+ ..Default::default()
670
+ };
671
+ let response = self.send_request(request)?;
672
+ if response.code != StatusCode::Ok as i32 {
673
+ let code =
674
+ StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
675
+ return Err(ClientError::Server {
676
+ code,
677
+ message: response.message,
678
+ });
679
+ }
680
+ response
681
+ .purge_exited_sessions
682
+ .ok_or_else(|| ClientError::Server {
683
+ code: StatusCode::Internal,
684
+ message: "purge_exited_sessions response missing payload".into(),
685
+ })
686
+ }
687
+
688
+ /// Schedule termination of every session older than the threshold
689
+ /// (#130 M9 H4). `older_than_secs=0` terminates everything in scope.
690
+ pub fn bulk_terminate_sessions(
691
+ &mut self,
692
+ older_than_secs: u64,
693
+ originator: &str,
694
+ grace_ms: u32,
695
+ ) -> Result<BulkTerminateSessionsResponse, ClientError> {
696
+ let request = DaemonRequest {
697
+ id: self.next_request_id(),
698
+ r#type: RequestType::BulkTerminateSessions.into(),
699
+ protocol_version: 1,
700
+ client_name: String::from("running-process-client"),
701
+ bulk_terminate_sessions: Some(BulkTerminateSessionsRequest {
702
+ older_than_secs,
703
+ originator: originator.into(),
704
+ grace_ms,
705
+ }),
706
+ ..Default::default()
707
+ };
708
+ let response = self.send_request(request)?;
709
+ if response.code != StatusCode::Ok as i32 {
710
+ let code =
711
+ StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
712
+ return Err(ClientError::Server {
713
+ code,
714
+ message: response.message,
715
+ });
716
+ }
717
+ response
718
+ .bulk_terminate_sessions
719
+ .ok_or_else(|| ClientError::Server {
720
+ code: StatusCode::Internal,
721
+ message: "bulk_terminate_sessions response missing payload".into(),
722
+ })
723
+ }
724
+
725
+ /// Snapshot a PTY or pipe session's output backlog without consuming
726
+ /// it. For pipe sessions, `pipe_stream` selects between stdout and
727
+ /// stderr (default stdout). For PTY sessions `pipe_stream` is ignored.
728
+ /// Returns `None` when the session is not found.
729
+ pub fn get_session_backlog(
730
+ &mut self,
731
+ session_id: &str,
732
+ pipe_stream: PipeStreamKind,
733
+ ) -> Result<Option<GetSessionBacklogResponse>, ClientError> {
734
+ let request = DaemonRequest {
735
+ id: self.next_request_id(),
736
+ r#type: RequestType::GetSessionBacklog.into(),
737
+ protocol_version: 1,
738
+ client_name: String::from("running-process-client"),
739
+ get_session_backlog: Some(GetSessionBacklogRequest {
740
+ session_id: session_id.into(),
741
+ pipe_stream: pipe_stream as i32,
742
+ }),
743
+ ..Default::default()
744
+ };
745
+ let response = self.send_request(request)?;
746
+ if response.code == StatusCode::NotFound as i32 {
747
+ return Ok(None);
748
+ }
749
+ if response.code != StatusCode::Ok as i32 {
750
+ let code =
751
+ StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
752
+ return Err(ClientError::Server {
753
+ code,
754
+ message: response.message,
755
+ });
756
+ }
757
+ Ok(response.get_session_backlog)
758
+ }
618
759
  }
619
760
 
620
761
  // ---------------------------------------------------------------------------
@@ -0,0 +1,11 @@
1
+ pub mod client;
2
+ pub mod paths;
3
+ pub mod pipe_session;
4
+ pub mod pty_session;
5
+
6
+ pub use client::{
7
+ connect_or_start, daemonize_command, launch_detached, ClientError, DaemonClient,
8
+ SpawnCommandRequest, SpawnedDaemon,
9
+ };
10
+ pub use pipe_session::{PipeSpawnRequest, PipeStreamAttachment, SpawnedPipeSession};
11
+ pub use pty_session::{AttachError, PtyAttachment, PtySpawnRequest, SpawnedPtySession};
@@ -0,0 +1,359 @@
1
+ //! Client-side helpers for daemon-owned pipe-backed sessions
2
+ //! (issue #130 milestone 3).
3
+ //!
4
+ //! Mirrors [`crate::pty_session`] for the pipe case. Sessions are spawned,
5
+ //! listed, detached, and terminated via the regular [`DaemonClient`] RPC
6
+ //! channel. Stdin is also an RPC (`write_pipe_stdin`). Stdout/stderr are
7
+ //! attached via [`PipeStreamAttachment`], which owns its own connection
8
+ //! and pumps `PipeStreamFrame` payloads.
9
+
10
+ use crate::client::{ClientError, DaemonClient};
11
+ use crate::paths;
12
+ use interprocess::local_socket::Stream;
13
+ use interprocess::TryClone;
14
+ use prost::Message;
15
+ use running_process_proto::daemon::{
16
+ AttachPipeStreamRequest, AttachPipeStreamResponse, DaemonRequest, DaemonResponse,
17
+ DetachPipeStreamRequest, KeyValue, ListPipeSessionsRequest, ListPipeSessionsResponse,
18
+ PipeSessionInfo, PipeStreamFrame, PipeStreamKind, RequestType, SpawnPipeSessionRequest,
19
+ SpawnPipeSessionResponse, StatusCode, TerminatePipeSessionRequest, WritePipeStdinRequest,
20
+ WritePipeStdinResponse,
21
+ };
22
+ use std::io::{BufReader, BufWriter, Read, Write};
23
+ use std::path::PathBuf;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Spawn / list / terminate / write helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ #[derive(Debug, Clone)]
30
+ pub struct PipeSpawnRequest {
31
+ pub argv: Vec<String>,
32
+ pub cwd: Option<PathBuf>,
33
+ pub env: Vec<(String, String)>,
34
+ pub clear_inherited_env: bool,
35
+ pub originator: Option<String>,
36
+ pub merge_stderr_into_stdout: bool,
37
+ }
38
+
39
+ impl PipeSpawnRequest {
40
+ pub fn new<S: Into<String>>(argv: impl IntoIterator<Item = S>) -> Self {
41
+ Self {
42
+ argv: argv.into_iter().map(Into::into).collect(),
43
+ cwd: None,
44
+ env: Vec::new(),
45
+ clear_inherited_env: false,
46
+ originator: None,
47
+ merge_stderr_into_stdout: false,
48
+ }
49
+ }
50
+
51
+ pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
52
+ self.cwd = Some(cwd.into());
53
+ self
54
+ }
55
+
56
+ pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
57
+ self.originator = Some(originator.into());
58
+ self
59
+ }
60
+
61
+ pub fn merge_stderr(mut self) -> Self {
62
+ self.merge_stderr_into_stdout = true;
63
+ self
64
+ }
65
+
66
+ pub fn with_envs<I, K, V>(mut self, env: I) -> Self
67
+ where
68
+ I: IntoIterator<Item = (K, V)>,
69
+ K: Into<String>,
70
+ V: Into<String>,
71
+ {
72
+ self.env = env
73
+ .into_iter()
74
+ .map(|(k, v)| (k.into(), v.into()))
75
+ .collect();
76
+ self
77
+ }
78
+ }
79
+
80
+ #[derive(Debug, Clone)]
81
+ pub struct SpawnedPipeSession {
82
+ pub session_id: String,
83
+ pub pid: u32,
84
+ pub created_at: f64,
85
+ }
86
+
87
+ impl DaemonClient {
88
+ pub fn spawn_pipe_session(
89
+ &mut self,
90
+ request: &PipeSpawnRequest,
91
+ ) -> Result<SpawnedPipeSession, ClientError> {
92
+ let proto = SpawnPipeSessionRequest {
93
+ argv: request.argv.clone(),
94
+ cwd: request
95
+ .cwd
96
+ .as_ref()
97
+ .map(|p| p.to_string_lossy().into_owned())
98
+ .unwrap_or_default(),
99
+ env: request
100
+ .env
101
+ .iter()
102
+ .map(|(k, v)| KeyValue {
103
+ key: k.clone(),
104
+ value: v.clone(),
105
+ })
106
+ .collect(),
107
+ clear_inherited_env: request.clear_inherited_env,
108
+ originator: request.originator.clone().unwrap_or_default(),
109
+ merge_stderr_into_stdout: request.merge_stderr_into_stdout,
110
+ };
111
+ let daemon_request = DaemonRequest {
112
+ id: self.next_request_id(),
113
+ r#type: RequestType::SpawnPipeSession.into(),
114
+ protocol_version: 1,
115
+ client_name: "running-process-client".into(),
116
+ spawn_pipe_session: Some(proto),
117
+ ..Default::default()
118
+ };
119
+ let response = self.send_request(daemon_request)?;
120
+ ensure_ok(&response)?;
121
+ let payload: SpawnPipeSessionResponse = response
122
+ .spawn_pipe_session
123
+ .ok_or_else(|| ClientError::Server {
124
+ code: StatusCode::Internal,
125
+ message: "spawn_pipe_session response missing payload".into(),
126
+ })?;
127
+ Ok(SpawnedPipeSession {
128
+ session_id: payload.session_id,
129
+ pid: payload.pid,
130
+ created_at: payload.created_at,
131
+ })
132
+ }
133
+
134
+ pub fn list_pipe_sessions(
135
+ &mut self,
136
+ originator_filter: &str,
137
+ ) -> Result<Vec<PipeSessionInfo>, ClientError> {
138
+ let req = DaemonRequest {
139
+ id: self.next_request_id(),
140
+ r#type: RequestType::ListPipeSessions.into(),
141
+ protocol_version: 1,
142
+ client_name: "running-process-client".into(),
143
+ list_pipe_sessions: Some(ListPipeSessionsRequest {
144
+ originator: originator_filter.into(),
145
+ }),
146
+ ..Default::default()
147
+ };
148
+ let response = self.send_request(req)?;
149
+ ensure_ok(&response)?;
150
+ let payload: ListPipeSessionsResponse = response
151
+ .list_pipe_sessions
152
+ .ok_or_else(|| ClientError::Server {
153
+ code: StatusCode::Internal,
154
+ message: "list_pipe_sessions response missing payload".into(),
155
+ })?;
156
+ Ok(payload.sessions)
157
+ }
158
+
159
+ pub fn detach_pipe_stream(
160
+ &mut self,
161
+ session_id: &str,
162
+ stream: PipeStreamKind,
163
+ ) -> Result<(), ClientError> {
164
+ let req = DaemonRequest {
165
+ id: self.next_request_id(),
166
+ r#type: RequestType::DetachPipeStream.into(),
167
+ protocol_version: 1,
168
+ client_name: "running-process-client".into(),
169
+ detach_pipe_stream: Some(DetachPipeStreamRequest {
170
+ session_id: session_id.into(),
171
+ stream: stream as i32,
172
+ }),
173
+ ..Default::default()
174
+ };
175
+ let response = self.send_request(req)?;
176
+ ensure_ok(&response)?;
177
+ Ok(())
178
+ }
179
+
180
+ pub fn terminate_pipe_session(
181
+ &mut self,
182
+ session_id: &str,
183
+ grace_ms: u32,
184
+ ) -> Result<(), ClientError> {
185
+ let req = DaemonRequest {
186
+ id: self.next_request_id(),
187
+ r#type: RequestType::TerminatePipeSession.into(),
188
+ protocol_version: 1,
189
+ client_name: "running-process-client".into(),
190
+ terminate_pipe_session: Some(TerminatePipeSessionRequest {
191
+ session_id: session_id.into(),
192
+ grace_ms,
193
+ }),
194
+ ..Default::default()
195
+ };
196
+ let response = self.send_request(req)?;
197
+ ensure_ok(&response)?;
198
+ Ok(())
199
+ }
200
+
201
+ pub fn write_pipe_stdin(
202
+ &mut self,
203
+ session_id: &str,
204
+ data: &[u8],
205
+ close_after: bool,
206
+ ) -> Result<u64, ClientError> {
207
+ let req = DaemonRequest {
208
+ id: self.next_request_id(),
209
+ r#type: RequestType::WritePipeStdin.into(),
210
+ protocol_version: 1,
211
+ client_name: "running-process-client".into(),
212
+ write_pipe_stdin: Some(WritePipeStdinRequest {
213
+ session_id: session_id.into(),
214
+ data: data.to_vec(),
215
+ close: close_after,
216
+ }),
217
+ ..Default::default()
218
+ };
219
+ let response = self.send_request(req)?;
220
+ ensure_ok(&response)?;
221
+ let payload: WritePipeStdinResponse = response
222
+ .write_pipe_stdin
223
+ .ok_or_else(|| ClientError::Server {
224
+ code: StatusCode::Internal,
225
+ message: "write_pipe_stdin response missing payload".into(),
226
+ })?;
227
+ Ok(payload.bytes_written)
228
+ }
229
+ }
230
+
231
+ fn ensure_ok(response: &DaemonResponse) -> Result<(), ClientError> {
232
+ if response.code == StatusCode::Ok as i32 {
233
+ return Ok(());
234
+ }
235
+ let code = StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
236
+ Err(ClientError::Server {
237
+ code,
238
+ message: response.message.clone(),
239
+ })
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // PipeStreamAttachment
244
+ // ---------------------------------------------------------------------------
245
+
246
+ pub struct PipeStreamAttachment {
247
+ reader: BufReader<Stream>,
248
+ pub initial_backlog: Vec<u8>,
249
+ pub bytes_missed: u64,
250
+ }
251
+
252
+ #[derive(Debug)]
253
+ pub enum PipeAttachError {
254
+ Connect(std::io::Error),
255
+ Io(std::io::Error),
256
+ Decode(prost::DecodeError),
257
+ Server { code: StatusCode, message: String },
258
+ MissingPayload,
259
+ }
260
+
261
+ impl std::fmt::Display for PipeAttachError {
262
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263
+ match self {
264
+ Self::Connect(e) => write!(f, "pipe attach connect failed: {e}"),
265
+ Self::Io(e) => write!(f, "pipe attach io error: {e}"),
266
+ Self::Decode(e) => write!(f, "pipe attach decode error: {e}"),
267
+ Self::Server { code, message } => {
268
+ write!(f, "pipe attach server error {code:?}: {message}")
269
+ }
270
+ Self::MissingPayload => write!(f, "pipe attach response missing payload"),
271
+ }
272
+ }
273
+ }
274
+
275
+ impl std::error::Error for PipeAttachError {}
276
+
277
+ impl PipeStreamAttachment {
278
+ pub fn attach(
279
+ scope_hash: Option<&str>,
280
+ session_id: &str,
281
+ stream: PipeStreamKind,
282
+ steal: bool,
283
+ ) -> Result<Self, PipeAttachError> {
284
+ let socket_path = paths::socket_path(scope_hash);
285
+ Self::attach_to(&socket_path, session_id, stream, steal)
286
+ }
287
+
288
+ pub fn attach_to(
289
+ socket_path: &str,
290
+ session_id: &str,
291
+ stream: PipeStreamKind,
292
+ steal: bool,
293
+ ) -> Result<Self, PipeAttachError> {
294
+ let name = paths::make_socket_name(socket_path).map_err(PipeAttachError::Connect)?;
295
+ use interprocess::local_socket::traits::Stream as _;
296
+ let s = Stream::connect(name).map_err(PipeAttachError::Connect)?;
297
+ let s_clone = s.try_clone().map_err(PipeAttachError::Connect)?;
298
+ let mut reader = BufReader::new(s);
299
+ let mut writer = BufWriter::new(s_clone);
300
+
301
+ let attach_request = DaemonRequest {
302
+ id: 1,
303
+ r#type: RequestType::AttachPipeStream.into(),
304
+ protocol_version: 1,
305
+ client_name: "running-process-client".into(),
306
+ attach_pipe_stream: Some(AttachPipeStreamRequest {
307
+ session_id: session_id.into(),
308
+ stream: stream as i32,
309
+ steal,
310
+ }),
311
+ ..Default::default()
312
+ };
313
+ write_length_prefixed(&mut writer, &attach_request.encode_to_vec())
314
+ .map_err(PipeAttachError::Io)?;
315
+ // We do not need writer after this, but keep it alive via reader's
316
+ // duplex socket. Drop here.
317
+ drop(writer);
318
+
319
+ let response_bytes = read_length_prefixed(&mut reader).map_err(PipeAttachError::Io)?;
320
+ let response = DaemonResponse::decode(&response_bytes[..]).map_err(PipeAttachError::Decode)?;
321
+ if response.code != StatusCode::Ok as i32 {
322
+ let code = StatusCode::try_from(response.code).unwrap_or(StatusCode::UnknownRequest);
323
+ return Err(PipeAttachError::Server {
324
+ code,
325
+ message: response.message,
326
+ });
327
+ }
328
+ let payload: AttachPipeStreamResponse = response
329
+ .attach_pipe_stream
330
+ .ok_or(PipeAttachError::MissingPayload)?;
331
+
332
+ Ok(Self {
333
+ reader,
334
+ initial_backlog: payload.backlog,
335
+ bytes_missed: payload.bytes_missed,
336
+ })
337
+ }
338
+
339
+ pub fn recv_frame(&mut self) -> Result<PipeStreamFrame, PipeAttachError> {
340
+ let bytes = read_length_prefixed(&mut self.reader).map_err(PipeAttachError::Io)?;
341
+ PipeStreamFrame::decode(&bytes[..]).map_err(PipeAttachError::Decode)
342
+ }
343
+ }
344
+
345
+ fn write_length_prefixed<W: Write>(w: &mut W, payload: &[u8]) -> Result<(), std::io::Error> {
346
+ let len = payload.len() as u32;
347
+ w.write_all(&len.to_be_bytes())?;
348
+ w.write_all(payload)?;
349
+ w.flush()
350
+ }
351
+
352
+ fn read_length_prefixed<R: Read>(r: &mut R) -> Result<Vec<u8>, std::io::Error> {
353
+ let mut len_buf = [0u8; 4];
354
+ r.read_exact(&mut len_buf)?;
355
+ let len = u32::from_be_bytes(len_buf) as usize;
356
+ let mut buf = vec![0u8; len];
357
+ r.read_exact(&mut buf)?;
358
+ Ok(buf)
359
+ }