@midscene/computer 1.10.7 → 1.10.8-beta-20260723091821.0

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.
Binary file
Binary file
package/dist/es/cli.mjs CHANGED
@@ -750,7 +750,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
750
750
  }
751
751
  async healthCheck() {
752
752
  console.log('[HealthCheck] Starting health check...');
753
- console.log("[HealthCheck] @midscene/computer v1.10.7");
753
+ console.log("[HealthCheck] @midscene/computer v1.10.8-beta-20260723091821.0");
754
754
  console.log('[HealthCheck] Taking screenshot...');
755
755
  const screenshotTimeout = 15000;
756
756
  let timeoutId;
@@ -2148,7 +2148,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
2148
2148
  const tools = new ComputerMidsceneTools();
2149
2149
  runToolsCLI(tools, 'midscene-computer', {
2150
2150
  stripPrefix: 'computer_',
2151
- version: "1.10.7",
2151
+ version: "1.10.8-beta-20260723091821.0",
2152
2152
  extraCommands: createReportCliCommands()
2153
2153
  }).catch((e)=>{
2154
2154
  process.exit(reportCLIError(e));
package/dist/es/index.mjs CHANGED
@@ -750,7 +750,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
750
750
  }
751
751
  async healthCheck() {
752
752
  console.log('[HealthCheck] Starting health check...');
753
- console.log("[HealthCheck] @midscene/computer v1.10.7");
753
+ console.log("[HealthCheck] @midscene/computer v1.10.8-beta-20260723091821.0");
754
754
  console.log('[HealthCheck] Taking screenshot...');
755
755
  const screenshotTimeout = 15000;
756
756
  let timeoutId;
@@ -2189,7 +2189,7 @@ class ComputerMidsceneTools extends BaseMidsceneTools {
2189
2189
  }
2190
2190
  }
2191
2191
  function version() {
2192
- const currentVersion = "1.10.7";
2192
+ const currentVersion = "1.10.8-beta-20260723091821.0";
2193
2193
  console.log(`@midscene/computer v${currentVersion}`);
2194
2194
  return currentVersion;
2195
2195
  }
package/dist/lib/cli.js CHANGED
@@ -776,7 +776,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
776
776
  }
777
777
  async healthCheck() {
778
778
  console.log('[HealthCheck] Starting health check...');
779
- console.log("[HealthCheck] @midscene/computer v1.10.7");
779
+ console.log("[HealthCheck] @midscene/computer v1.10.8-beta-20260723091821.0");
780
780
  console.log('[HealthCheck] Taking screenshot...');
781
781
  const screenshotTimeout = 15000;
782
782
  let timeoutId;
@@ -2175,7 +2175,7 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2175
2175
  const tools = new ComputerMidsceneTools();
2176
2176
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-computer', {
2177
2177
  stripPrefix: 'computer_',
2178
- version: "1.10.7",
2178
+ version: "1.10.8-beta-20260723091821.0",
2179
2179
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
2180
2180
  }).catch((e)=>{
2181
2181
  process.exit((0, cli_namespaceObject.reportCLIError)(e));
package/dist/lib/index.js CHANGED
@@ -802,7 +802,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
802
802
  }
803
803
  async healthCheck() {
804
804
  console.log('[HealthCheck] Starting health check...');
805
- console.log("[HealthCheck] @midscene/computer v1.10.7");
805
+ console.log("[HealthCheck] @midscene/computer v1.10.8-beta-20260723091821.0");
806
806
  console.log('[HealthCheck] Taking screenshot...');
807
807
  const screenshotTimeout = 15000;
808
808
  let timeoutId;
@@ -2247,7 +2247,7 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2247
2247
  }
2248
2248
  const env_namespaceObject = require("@midscene/shared/env");
2249
2249
  function version() {
2250
- const currentVersion = "1.10.7";
2250
+ const currentVersion = "1.10.8-beta-20260723091821.0";
2251
2251
  console.log(`@midscene/computer v${currentVersion}`);
2252
2252
  return currentVersion;
2253
2253
  }
@@ -66,16 +66,32 @@ if(BUILD_TESTING)
66
66
  tests/connection_policy_test.cpp
67
67
  )
68
68
 
69
+ add_executable(
70
+ rdp-session-policy-test
71
+ tests/session_policy_test.cpp
72
+ )
73
+
69
74
  target_include_directories(
70
75
  rdp-connection-policy-test
71
76
  PRIVATE
72
77
  ${CMAKE_CURRENT_SOURCE_DIR}/include
73
78
  )
74
79
 
80
+ target_include_directories(
81
+ rdp-session-policy-test
82
+ PRIVATE
83
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
84
+ )
85
+
75
86
  add_test(
76
87
  NAME rdp-connection-policy-test
77
88
  COMMAND rdp-connection-policy-test
78
89
  )
90
+
91
+ add_test(
92
+ NAME rdp-session-policy-test
93
+ COMMAND rdp-session-policy-test
94
+ )
79
95
  endif()
80
96
 
81
97
  if(APPLE)
@@ -77,6 +77,9 @@ class FreeRdpSessionTransport final : public SessionTransport {
77
77
  // Records that at least one paint has reached the local framebuffer and
78
78
  // wakes Connect().
79
79
  void MarkFramePainted(std::optional<RawFrame> first_frame = std::nullopt);
80
+ // Records a framebuffer update even when it is too blank or uniform to count
81
+ // as the first informative frame.
82
+ void MarkFramebufferUpdated();
80
83
  bool HasFramePainted() const;
81
84
  BOOL CallOriginalEndPaint(rdpContext* context);
82
85
  // Mark the session as no longer active and wake any first-frame waiter.
@@ -98,6 +101,9 @@ class FreeRdpSessionTransport final : public SessionTransport {
98
101
  // RDP session exposes a zero-filled (all black) buffer until the first paint
99
102
  // is processed, so screenshots taken before this is > 0 would be blank.
100
103
  std::atomic<uint64_t> frames_painted_{0};
104
+ // Number of valid framebuffer updates observed before the first informative
105
+ // frame. This distinguishes a silent desktop from a blank or locked one.
106
+ std::atomic<uint64_t> framebuffer_updates_{0};
101
107
  // Tracks whether the session is still alive while Connect() waits for the
102
108
  // first frame, so a drop during that window wakes the wait immediately
103
109
  // instead of stalling until the timeout.
@@ -114,7 +120,9 @@ class FreeRdpSessionTransport final : public SessionTransport {
114
120
 
115
121
  void ResetStateLocked();
116
122
  void ClearSessionErrorLocked();
117
- void SetSessionError(std::string message, std::string code);
123
+ bool RecordEventLoopFailureIfActive(bool operation_failed,
124
+ bool disconnect_requested,
125
+ std::string message);
118
126
  std::string LastFreeRdpErrorLocked() const;
119
127
  void StopInstance(bool preserve_session_error);
120
128
  void EventLoop();
@@ -0,0 +1,41 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <string>
5
+ #include <string_view>
6
+
7
+ namespace midscene::rdp {
8
+
9
+ constexpr bool ShouldReportEventLoopFailure(bool operation_failed,
10
+ bool disconnect_requested,
11
+ bool session_running,
12
+ bool session_connected) noexcept {
13
+ return (operation_failed || disconnect_requested) && session_running &&
14
+ session_connected;
15
+ }
16
+
17
+ inline std::string BuildFirstFrameTimeoutMessage(
18
+ uint64_t framebuffer_updates,
19
+ std::string_view failure_reason) {
20
+ std::string message;
21
+ if (framebuffer_updates == 0) {
22
+ message =
23
+ "Connected to the RDP server but received no desktop frame within "
24
+ "timeout; the remote desktop may be blank or locked";
25
+ } else {
26
+ message = "Connected to the RDP server and received " +
27
+ std::to_string(framebuffer_updates) + " framebuffer update";
28
+ if (framebuffer_updates != 1) {
29
+ message += "s";
30
+ }
31
+ message +=
32
+ ", but none passed the informative-frame check within timeout; the "
33
+ "remote desktop may be blank, locked, or still loading";
34
+ }
35
+ if (!failure_reason.empty()) {
36
+ message += " (" + std::string(failure_reason) + ")";
37
+ }
38
+ return message;
39
+ }
40
+
41
+ } // namespace midscene::rdp
@@ -37,6 +37,7 @@
37
37
  #include <winpr/synch.h>
38
38
 
39
39
  #include "rdp_helper_connection_policy.hpp"
40
+ #include "rdp_helper_session_policy.hpp"
40
41
 
41
42
  namespace midscene::rdp {
42
43
 
@@ -512,6 +513,7 @@ BOOL MidsceneEndPaint(rdpContext* context) {
512
513
  ok = typed_context->owner->CallOriginalEndPaint(context);
513
514
  const bool already_painted = typed_context->owner->HasFramePainted();
514
515
  if (ok && framebuffer_invalidated) {
516
+ typed_context->owner->MarkFramebufferUpdated();
515
517
  if (already_painted) {
516
518
  typed_context->owner->MarkFramePainted();
517
519
  } else if (auto first_frame = CaptureInformativeFramebuffer(context);
@@ -978,6 +980,7 @@ ConnectionInfo FreeRdpSessionTransport::Connect(const ConnectionConfig& config)
978
980
  mouse_x_ = 0;
979
981
  mouse_y_ = 0;
980
982
  frames_painted_.store(0, std::memory_order_relaxed);
983
+ framebuffer_updates_.store(0, std::memory_order_relaxed);
981
984
  {
982
985
  std::lock_guard<std::mutex> frame_lock(frame_mutex_);
983
986
  first_frame_.reset();
@@ -1061,18 +1064,15 @@ ConnectionInfo FreeRdpSessionTransport::Connect(const ConnectionConfig& config)
1061
1064
 
1062
1065
  if (!painted) {
1063
1066
  std::string reason;
1067
+ const uint64_t framebuffer_updates =
1068
+ framebuffer_updates_.load(std::memory_order_relaxed);
1064
1069
  {
1065
1070
  std::lock_guard<std::mutex> lock(mutex_);
1066
1071
  reason = connected_ ? std::string() : LastFreeRdpErrorLocked();
1067
1072
  }
1068
1073
  StopInstance(false);
1069
- std::string message =
1070
- "Connected to the RDP server but received no desktop frame within "
1071
- "timeout; the remote desktop may be blank or locked";
1072
- if (!reason.empty()) {
1073
- message += " (" + reason + ")";
1074
- }
1075
- throw std::runtime_error(message);
1074
+ throw std::runtime_error(
1075
+ BuildFirstFrameTimeoutMessage(framebuffer_updates, reason));
1076
1076
  }
1077
1077
 
1078
1078
  return info;
@@ -1357,6 +1357,7 @@ void FreeRdpSessionTransport::ResetStateLocked() {
1357
1357
  connected_ = false;
1358
1358
  gdi_initialized_ = false;
1359
1359
  frames_painted_.store(0, std::memory_order_relaxed);
1360
+ framebuffer_updates_.store(0, std::memory_order_relaxed);
1360
1361
  {
1361
1362
  std::lock_guard<std::mutex> frame_lock(frame_mutex_);
1362
1363
  first_frame_.reset();
@@ -1402,6 +1403,10 @@ void FreeRdpSessionTransport::MarkFramePainted(
1402
1403
  frame_cv_.notify_all();
1403
1404
  }
1404
1405
 
1406
+ void FreeRdpSessionTransport::MarkFramebufferUpdated() {
1407
+ framebuffer_updates_.fetch_add(1, std::memory_order_relaxed);
1408
+ }
1409
+
1405
1410
  bool FreeRdpSessionTransport::HasFramePainted() const {
1406
1411
  return frames_painted_.load(std::memory_order_relaxed) > 0;
1407
1412
  }
@@ -1418,12 +1423,22 @@ void FreeRdpSessionTransport::ClearSessionErrorLocked() {
1418
1423
  last_error_.reset();
1419
1424
  }
1420
1425
 
1421
- void FreeRdpSessionTransport::SetSessionError(std::string message, std::string code) {
1426
+ bool FreeRdpSessionTransport::RecordEventLoopFailureIfActive(
1427
+ bool operation_failed,
1428
+ bool disconnect_requested,
1429
+ std::string message) {
1422
1430
  std::lock_guard<std::mutex> lock(mutex_);
1431
+ if (!ShouldReportEventLoopFailure(operation_failed, disconnect_requested,
1432
+ running_, connected_)) {
1433
+ return false;
1434
+ }
1423
1435
  last_error_ = ErrorPayload{
1424
- std::move(code),
1436
+ "session_lost",
1425
1437
  message.empty() ? "RDP session was lost" : std::move(message),
1426
1438
  };
1439
+ connected_ = false;
1440
+ running_ = false;
1441
+ return true;
1427
1442
  }
1428
1443
 
1429
1444
  std::string FreeRdpSessionTransport::LastFreeRdpErrorLocked() const {
@@ -1522,11 +1537,9 @@ void FreeRdpSessionTransport::EventLoop() {
1522
1537
  const DWORD count = freerdp_get_event_handles(
1523
1538
  instance->context, handles, static_cast<DWORD>(std::size(handles)));
1524
1539
  if (count == 0) {
1525
- SetSessionError("freerdp_get_event_handles returned no handles", "session_lost");
1526
- {
1527
- std::lock_guard<std::mutex> lock(mutex_);
1528
- connected_ = false;
1529
- running_ = false;
1540
+ if (!RecordEventLoopFailureIfActive(
1541
+ true, false, "freerdp_get_event_handles returned no handles")) {
1542
+ return;
1530
1543
  }
1531
1544
  std::fprintf(stderr,
1532
1545
  "RDP session event loop failed: freerdp_get_event_handles returned no handles\n");
@@ -1537,12 +1550,10 @@ void FreeRdpSessionTransport::EventLoop() {
1537
1550
 
1538
1551
  const DWORD status = WaitForMultipleObjects(count, handles, FALSE, 100);
1539
1552
  if (status == WAIT_FAILED) {
1540
- SetSessionError("WaitForMultipleObjects failed in the RDP event loop",
1541
- "session_lost");
1542
- {
1543
- std::lock_guard<std::mutex> lock(mutex_);
1544
- connected_ = false;
1545
- running_ = false;
1553
+ if (!RecordEventLoopFailureIfActive(
1554
+ true, false,
1555
+ "WaitForMultipleObjects failed in the RDP event loop")) {
1556
+ return;
1546
1557
  }
1547
1558
  std::fprintf(stderr,
1548
1559
  "RDP session event loop failed: WaitForMultipleObjects failed\n");
@@ -1568,11 +1579,9 @@ void FreeRdpSessionTransport::EventLoop() {
1568
1579
  }
1569
1580
 
1570
1581
  if (!ok || should_disconnect) {
1571
- SetSessionError(failure_reason, "session_lost");
1572
- {
1573
- std::lock_guard<std::mutex> lock(mutex_);
1574
- connected_ = false;
1575
- running_ = false;
1582
+ if (!RecordEventLoopFailureIfActive(!ok, should_disconnect,
1583
+ failure_reason)) {
1584
+ return;
1576
1585
  }
1577
1586
  std::fprintf(stderr, "RDP session event loop failed: %s\n",
1578
1587
  failure_reason.c_str());
@@ -0,0 +1,77 @@
1
+ #include <iostream>
2
+ #include <string>
3
+ #include <string_view>
4
+
5
+ #include "rdp_helper_session_policy.hpp"
6
+
7
+ namespace {
8
+
9
+ bool ExpectBool(std::string_view name, bool actual, bool expected) {
10
+ if (actual == expected) {
11
+ return true;
12
+ }
13
+
14
+ std::cerr << name << ": expected " << expected << ", actual=" << actual
15
+ << '\n';
16
+ return false;
17
+ }
18
+
19
+ bool ExpectMessage(std::string_view name,
20
+ const std::string& actual,
21
+ std::string_view expected) {
22
+ if (actual == expected) {
23
+ return true;
24
+ }
25
+
26
+ std::cerr << name << ": expected \"" << expected << "\", actual=\""
27
+ << actual << "\"\n";
28
+ return false;
29
+ }
30
+
31
+ } // namespace
32
+
33
+ int main() {
34
+ bool passed = true;
35
+ passed &= ExpectBool(
36
+ "active transport failure",
37
+ midscene::rdp::ShouldReportEventLoopFailure(true, false, true, true),
38
+ true);
39
+ passed &= ExpectBool(
40
+ "intentional shutdown transport failure",
41
+ midscene::rdp::ShouldReportEventLoopFailure(true, false, false, false),
42
+ false);
43
+ passed &= ExpectBool(
44
+ "active server disconnect",
45
+ midscene::rdp::ShouldReportEventLoopFailure(false, true, true, true),
46
+ true);
47
+ passed &= ExpectBool(
48
+ "healthy active session",
49
+ midscene::rdp::ShouldReportEventLoopFailure(false, false, true, true),
50
+ false);
51
+
52
+ passed &= ExpectMessage(
53
+ "no framebuffer updates",
54
+ midscene::rdp::BuildFirstFrameTimeoutMessage(0, ""),
55
+ "Connected to the RDP server but received no desktop frame within "
56
+ "timeout; the remote desktop may be blank or locked");
57
+ passed &= ExpectMessage(
58
+ "non-informative framebuffer updates",
59
+ midscene::rdp::BuildFirstFrameTimeoutMessage(3, ""),
60
+ "Connected to the RDP server and received 3 framebuffer updates, but "
61
+ "none passed the informative-frame check within timeout; the remote "
62
+ "desktop may be blank, locked, or still loading");
63
+ passed &= ExpectMessage(
64
+ "single non-informative framebuffer update",
65
+ midscene::rdp::BuildFirstFrameTimeoutMessage(1, ""),
66
+ "Connected to the RDP server and received 1 framebuffer update, but "
67
+ "none passed the informative-frame check within timeout; the remote "
68
+ "desktop may be blank, locked, or still loading");
69
+ passed &= ExpectMessage(
70
+ "session loss reason",
71
+ midscene::rdp::BuildFirstFrameTimeoutMessage(
72
+ 0, "ERRCONNECT_CONNECT_TRANSPORT_FAILED"),
73
+ "Connected to the RDP server but received no desktop frame within "
74
+ "timeout; the remote desktop may be blank or locked "
75
+ "(ERRCONNECT_CONNECT_TRANSPORT_FAILED)");
76
+ return passed ? 0 : 1;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/computer",
3
- "version": "1.10.7",
3
+ "version": "1.10.8-beta-20260723091821.0",
4
4
  "description": "Midscene.js Computer Desktop Automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "@computer-use/libnut": "^4.2.0",
34
34
  "clipboardy": "^4.0.0",
35
35
  "screenshot-desktop": "^1.15.3",
36
- "@midscene/core": "1.10.7",
37
- "@midscene/shared": "1.10.7"
36
+ "@midscene/core": "1.10.8-beta-20260723091821.0",
37
+ "@midscene/shared": "1.10.8-beta-20260723091821.0"
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "node-mac-permissions": "2.5.0"