feat: release 2.2.0 with PC-only tests and execution reliability fixes

Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
This commit is contained in:
chenmingxuan committed 2026-10-27 01:13:41 +08:00
1 parent fac4ccb760
commit be6e8c2abd
79 files changed
+2263 -277

No files matched your search

+25 -31
View File
@@ -89,9 +89,8 @@ class DeviceLockOwner:
class DeviceExecutionLock:
"""A PID-aware FIFO lease supporting per-device mutexes and global concurrency limits.
Artemis enforces a single active task per mobile device at any time.
Different devices can execute tasks concurrently unless a global concurrency
limit (e.g. concurrency_mode="global" or ARTEMIS_MAX_CONCURRENT_TASKS=1) is configured.
Artemis 2.2 serializes tasks across Android and computer targets. Legacy
concurrency options remain accepted but cannot enable parallel execution.
Atomic file creation provides exclusion per device serial. PID creation time
prevents stale owners or queue tickets from blocking execution after a process
@@ -102,6 +101,7 @@ class DeviceExecutionLock:
_DEFAULT_POLL_INTERVAL_SECONDS = 0.1
QUEUE_TICKET_ENV = "ARTEMIS_DEVICE_QUEUE_TICKET"
LOCK_SCOPE_ENV = "ARTEMIS_ADB_ENDPOINT_ID"
EXECUTION_RESOURCE = "artemis:execution"
@staticmethod
def _normalize_device_id(device_id: str | None) -> str:
@@ -160,7 +160,12 @@ class DeviceExecutionLock:
self._queue_path: Path | None = None
self._acquired = False
self.resource_ids = tuple(
sorted(set(r.strip().casefold() for r in resource_ids if r.strip()))
sorted(
{
self.EXECUTION_RESOURCE,
*(r.strip().casefold() for r in resource_ids if r.strip()),
}
)
)
self._resource_paths: list[Path] = []
@@ -613,7 +618,15 @@ class DeviceExecutionLock:
path = self.path.parent / f"artemis-device-resource-{digest}.lock"
# Same resource identity applies across plugins and ADB server endpoints.
owner = self._get_active_owner_for_path(path)
if owner is not None and owner.token == self.token:
if owner is not None and (
owner.token == self.token
or (
resource == self.EXECUTION_RESOURCE
and owner.pid == os.getpid()
and self.session_id
and owner.session_id == self.session_id
)
):
continue
try:
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
@@ -675,37 +688,17 @@ class DeviceExecutionLock:
@property
def effective_concurrency(self) -> int:
"""Resolve effective concurrency limit:
0: per-device concurrency (multiple devices run concurrently, FIFO per device)
1: global concurrency (strict single-task serialization across all devices)
>1: global concurrency capped at N tasks across all devices
"""
if self.concurrency_mode is not None:
mode = self.concurrency_mode
if mode in ("global", "serial", "1"):
return 1
if mode in ("per_device", "device", "parallel", "0"):
return 0
if self.max_concurrency is not None:
return self.max_concurrency
"""All task entrypoints share the current single-task execution policy."""
return self.resolve_env_concurrency()
@classmethod
def resolve_env_concurrency(cls) -> int:
"""Resolve the concurrency limit from the environment.
"""Compatibility entrypoint shared with the scheduler: execution is serial.
Shared with the admin scheduler. Returns 0 for per-device concurrency,
or a positive global limit.
Legacy environment/SDK concurrency values cannot opt out in 2.2.0.
The shared execution resource also makes admission atomic across processes.
"""
env_mode = os.environ.get("ARTEMIS_CONCURRENCY_MODE", "").strip().lower()
if env_mode in ("global", "serial", "1"):
return 1
if env_mode in ("per_device", "device", "parallel", "0"):
return 0
try:
return int(os.environ.get("ARTEMIS_MAX_CONCURRENT_TASKS", 0))
except (ValueError, TypeError):
return 0
return 1
def acquire(
self,
@@ -768,6 +761,7 @@ class DeviceExecutionLock:
# 2. Determine FIFO eligibility for this specific device
all_wait_files = sorted(self.queue_dir.glob("*.wait"))
is_reentrant = False
if effective_concurrency == 1:
# In strict serial mode, global FIFO applies; re-entrant parent processes are always eligible
active_owners = self.get_active_owners()
@@ -794,7 +788,7 @@ class DeviceExecutionLock:
)
is_eligible = bool(device_queue and device_queue[0] == self._queue_path)
if is_eligible and self.resource_ids:
if is_eligible and self.resource_ids and not is_reentrant:
for earlier in all_wait_files:
if earlier == self._queue_path:
break