@camstack/addon-pipeline 1.2.360 → 1.2.361

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 (34) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-K9Hx82GZ.js → custom-model-catalog-BsuqZLaN.js} +2 -2
  4. package/dist/{custom-model-catalog-ihWAlDr4.mjs → custom-model-catalog-DWFL7iBE.mjs} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BYCrV0VR.mjs → dist-73R05PFN.mjs} +26 -19
  8. package/dist/{dist-CYakbWA8.js → dist-DFGiI5lr.js} +25 -18
  9. package/dist/{model-catalogs-C9cyLHdH.js → model-catalogs-BefhGOAc.js} +1 -1
  10. package/dist/{model-catalogs-CX9SJ9in.mjs → model-catalogs-OLyh3TdB.mjs} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-C_qDNN9f.mjs → node-C0-Ik_JH.mjs} +3 -2
  14. package/dist/{node-CTCm8Vtw.js → node-CdOP9ELN.js} +4 -2
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-B3lkW9H-.mjs → process-memory-DKKVQV5j.mjs} +1 -1
  18. package/dist/{process-memory-Ds5S3L0F.js → process-memory-DO-ZBWdK.js} +1 -1
  19. package/dist/recorder/index.js +1675 -2
  20. package/dist/recorder/index.mjs +1675 -2
  21. package/dist/{segment-demux-js-W5FIgijf.mjs → segment-demux-js-C_27CmCq.mjs} +1 -1
  22. package/dist/{segment-demux-js-BgDpp15L.js → segment-demux-js-Dr3e9r4m.js} +1 -1
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-v95KZ2Rt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DJeFG_hB.mjs} +1 -1
  24. package/dist/stream-broker/demux-worker-child.js +1 -1
  25. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  26. package/dist/stream-broker/{hostInit-CDaDWDva.mjs → hostInit-Buho1uzb.mjs} +1 -1
  27. package/dist/stream-broker/index.js +3 -3
  28. package/dist/stream-broker/index.mjs +3 -3
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/package.json +1 -1
  31. package/python/inference_pool.py +12 -13
  32. package/python/pool_platform.py +214 -0
  33. package/python/test_inference_pool_windows.py +86 -0
  34. package/python/test_pool_platform.py +141 -0
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { jn as errMsg } from "./dist-BYCrV0VR.mjs";
2
+ import { On as errMsg } from "./dist-73R05PFN.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-CYakbWA8.js");
2
+ const require_dist = require("./dist-DFGiI5lr.js");
3
3
  let node_child_process = require("node:child_process");
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.355",
21
+ version: "1.2.356",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -1,4 +1,4 @@
1
- const require_segment_demux_js = require("../segment-demux-js-BgDpp15L.js");
1
+ const require_segment_demux_js = require("../segment-demux-js-Dr3e9r4m.js");
2
2
  let node_worker_threads = require("node:worker_threads");
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as demuxSegmentFullJs } from "../segment-demux-js-W5FIgijf.mjs";
1
+ import { t as demuxSegmentFullJs } from "../segment-demux-js-C_27CmCq.mjs";
2
2
  import { parentPort } from "node:worker_threads";
3
3
  //#region src/stream-broker/recorded/demux-worker-child.ts
4
4
  /**
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.355",
39
+ version: "1.2.356",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CYakbWA8.js");
7
- const require_node = require("../node-CTCm8Vtw.js");
6
+ const require_dist = require("../dist-DFGiI5lr.js");
7
+ const require_node = require("../node-CdOP9ELN.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-BgDpp15L.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-Dr3e9r4m.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $n as discriminatedUnion, $t as rankStreams, An as DeviceType, At as gifFromMp4Args, Bn as expandAudioChunkToF32le, D as G711_SCALE_CORRECTION_DB, Dn as invocationFromEncodeProfile, Dt as ffmpegRate, Fn as CamProfileSchema, Gn as maskUrlCredentials, Hn as isEvent, It as loadContributionCapability, K as RingBuffer, Kn as nodePin, Ln as createEvent, Lt as logChannelsCapability, Mn as BOOT_RECOVERY_BACKOFF_MS, Nn as BaseAddon, On as isSoftwareDecode, Pn as CAM_PROFILE_ORDER, Qn as boolean, R as RATE_CONTROL_RELAXED, Sn as AUDIO_PRESETS, Tn as buildFfmpegArgs, Un as makeProfileBrokerId, Vt as memoizePinnedFfmpeg, Wn as makeSourceBrokerId, X as addonWidgetsSourceCapability, Xn as _enum, Zn as array, _n as streamBrokerCapability, ar as record, at as cameraStreamsCapability, bt as egressTransportFromRequest, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, dt as createLogChannelsProvider, ft as declareLogChannel, jn as errMsg, kn as DeviceFeature, kt as gifDeriveSpeed, mt as deriveBatteryPresence, nr as number, nt as batteryCapability, o as BatteryStatusSchema, on as resolveEgressDecodeHwAccel, or as string, qn as parseProfileBrokerId, rr as object, s as CLIP_BROKER_PACED_RATES, sr as union, st as clipExportFailureOf, tr as literal, ut as createHwAccelCache, w as EncodeProfileSchema, xn as webrtcSessionCapability, yt as egressTranscodeSharingKey, z as RATE_CONTROL_TIGHT } from "../dist-BYCrV0VR.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C_qDNN9f.mjs";
2
+ import { $n as discriminatedUnion, An as BaseAddon, D as G711_SCALE_CORRECTION_DB, Dn as DeviceType, Dt as gifDeriveSpeed, En as DeviceFeature, Gn as maskUrlCredentials, Hn as isEvent, I as RATE_CONTROL_RELAXED, In as createEvent, J as addonWidgetsSourceCapability, Kn as nodePin, L as RATE_CONTROL_TIGHT, Mn as CamProfileSchema, Nt as loadContributionCapability, On as errMsg, Ot as gifFromMp4Args, Pt as logChannelsCapability, Qn as boolean, Rt as memoizePinnedFfmpeg, Sn as buildFfmpegArgs, Tn as isSoftwareDecode, Tt as ffmpegRate, Un as makeProfileBrokerId, W as RingBuffer, Wn as makeSourceBrokerId, Xn as _enum, Xt as rankStreams, Zn as array, _t as egressTranscodeSharingKey, ar as record, at as clipExportFailureOf, c as CLIP_STILL_REASON_CODE_HEADER, cr as EventCategory, ct as createHwAccelCache, et as batteryCapability, ft as deriveBatteryPresence, jn as CAM_PROFILE_ORDER, kn as BOOT_RECOVERY_BACKOFF_MS, lt as createLogChannelsProvider, mn as streamBrokerCapability, nr as number, o as BatteryStatusSchema, or as string, qn as parseProfileBrokerId, rn as resolveEgressDecodeHwAccel, rr as object, rt as cameraStreamsCapability, s as CLIP_BROKER_PACED_RATES, sr as union, tr as literal, ut as declareLogChannel, vn as webrtcSessionCapability, vt as egressTransportFromRequest, w as EncodeProfileSchema, wn as invocationFromEncodeProfile, yn as AUDIO_PRESETS, zn as expandAudioChunkToF32le } from "../dist-73R05PFN.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C0-Ik_JH.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-W5FIgijf.mjs";
7
+ import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-C_27CmCq.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os$1 from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-v95KZ2Rt.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DJeFG_hB.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.360",
3
+ "version": "1.2.361",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -61,7 +61,6 @@ from __future__ import annotations
61
61
  import asyncio
62
62
  import concurrent.futures
63
63
  import contextlib
64
- import fcntl
65
64
  import gc
66
65
  import io
67
66
  import json
@@ -80,6 +79,7 @@ from typing import Any, Awaitable, Callable, Optional
80
79
  import numpy as np
81
80
  from PIL import Image
82
81
 
82
+ import pool_platform
83
83
  from postprocessors import POSTPROCESSORS
84
84
 
85
85
  # ---------------------------------------------------------------------------
@@ -574,9 +574,12 @@ def compile_cache_lock(
574
574
  deadline = time.monotonic() + max(0.0, timeout_sec)
575
575
  while True:
576
576
  try:
577
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
578
- held = True
579
- break
577
+ if pool_platform.try_lock_exclusive(handle.fileno()):
578
+ held = True
579
+ break
580
+ if time.monotonic() >= deadline:
581
+ break
582
+ time.sleep(_COMPILE_CACHE_LOCK_POLL_SEC)
580
583
  except OSError:
581
584
  if time.monotonic() >= deadline:
582
585
  break
@@ -589,7 +592,7 @@ def compile_cache_lock(
589
592
  if handle is not None:
590
593
  try:
591
594
  if held:
592
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
595
+ pool_platform.unlock(handle.fileno())
593
596
  except OSError:
594
597
  pass
595
598
  try:
@@ -2344,6 +2347,9 @@ def _read_self_memory() -> dict:
2344
2347
  Partial results are fine — an empty dict means the read failed entirely.
2345
2348
  """
2346
2349
  out: dict = {}
2350
+ if sys.platform == "win32":
2351
+ # No `resource` and no /proc on Windows: psapi has the same numbers.
2352
+ return pool_platform.read_windows_memory()
2347
2353
  try:
2348
2354
  import resource
2349
2355
 
@@ -3000,14 +3006,7 @@ class CommandRouter:
3000
3006
 
3001
3007
  async def _run() -> None:
3002
3008
  loop = asyncio.get_event_loop()
3003
- reader = asyncio.StreamReader(loop=loop)
3004
- protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
3005
- await loop.connect_read_pipe(lambda: protocol, sys.stdin.buffer)
3006
-
3007
- transport, stdout_protocol = await loop.connect_write_pipe(
3008
- asyncio.streams.FlowControlMixin, sys.stdout.buffer,
3009
- )
3010
- stream_writer = asyncio.StreamWriter(transport, stdout_protocol, None, loop)
3009
+ reader, stream_writer = await pool_platform.open_stdio(loop)
3011
3010
  writer = ResponseWriter(stream_writer)
3012
3011
 
3013
3012
  t_start = time.perf_counter()
@@ -0,0 +1,214 @@
1
+ """The parts of the inference pool that differ between POSIX and Windows.
2
+
3
+ Everything here is a seam with ONE job: keep `inference_pool.py` free of
4
+ platform branches. On Linux/macOS each function delegates to the primitive the
5
+ pool always used (`fcntl.flock`, asyncio's pipe transports, `resource`); on
6
+ Windows it uses the closest equivalent that exists there. Nothing in this module
7
+ changes what Linux or macOS do — the Windows branches are only reachable when
8
+ `sys.platform == "win32"`, and each takes its platform as a parameter so a
9
+ POSIX machine can test both.
10
+
11
+ Why stdio is here too: the pool talks to its host over stdin/stdout, and
12
+ asyncio's `connect_read_pipe` / `connect_write_pipe` cannot adopt the child end
13
+ of a Node `child_process` pipe on Windows (the ProactorEventLoop needs an
14
+ overlapped handle; that end is a plain synchronous one). The Windows branch
15
+ reads stdin on a thread and writes stdout through an executor instead.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import concurrent.futures
21
+ import os
22
+ import sys
23
+ from typing import Any, BinaryIO, Optional
24
+
25
+ _READ_CHUNK = 1 << 16
26
+
27
+
28
+ def is_windows(platform: Optional[str] = None) -> bool:
29
+ return (platform if platform is not None else sys.platform) == "win32"
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Advisory file lock (the per-model compile-cache lock)
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def try_lock_exclusive(fd: int, platform: Optional[str] = None) -> bool:
38
+ """Take a NON-BLOCKING exclusive lock on `fd`; False when someone holds it.
39
+
40
+ POSIX: `flock(LOCK_EX | LOCK_NB)`. Windows: `msvcrt.locking(LK_NBLCK)` on the
41
+ first byte — byte-range locks are the only advisory primitive the C runtime
42
+ offers, and one byte is enough because the lock file is only ever a mutex.
43
+ Raises OSError for "unusable lock", exactly as `flock` did, so the caller's
44
+ "compile without the cache" path is unchanged.
45
+ """
46
+ if is_windows(platform):
47
+ import msvcrt
48
+
49
+ os.lseek(fd, 0, os.SEEK_SET)
50
+ try:
51
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
52
+ except OSError:
53
+ return False
54
+ return True
55
+ import fcntl
56
+
57
+ try:
58
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
59
+ except BlockingIOError:
60
+ return False
61
+ return True
62
+
63
+
64
+ def unlock(fd: int, platform: Optional[str] = None) -> None:
65
+ if is_windows(platform):
66
+ import msvcrt
67
+
68
+ os.lseek(fd, 0, os.SEEK_SET)
69
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
70
+ return
71
+ import fcntl
72
+
73
+ fcntl.flock(fd, fcntl.LOCK_UN)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Own-process memory (mem_stats)
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ def read_windows_memory() -> dict:
82
+ """Working set / peak working set / thread-less view of THIS process, in kB.
83
+
84
+ `GetProcessMemoryInfo` (psapi). Partial or empty on any failure — this rides
85
+ the mem_stats reply and must never raise.
86
+ """
87
+ out: dict = {}
88
+ try:
89
+ import ctypes
90
+ from ctypes import wintypes
91
+
92
+ class _Counters(ctypes.Structure):
93
+ _fields_ = [
94
+ ("cb", wintypes.DWORD),
95
+ ("PageFaultCount", wintypes.DWORD),
96
+ ("PeakWorkingSetSize", ctypes.c_size_t),
97
+ ("WorkingSetSize", ctypes.c_size_t),
98
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
99
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
100
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
101
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
102
+ ("PagefileUsage", ctypes.c_size_t),
103
+ ("PeakPagefileUsage", ctypes.c_size_t),
104
+ ]
105
+
106
+ counters = _Counters()
107
+ counters.cb = ctypes.sizeof(_Counters)
108
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
109
+ psapi = ctypes.WinDLL("psapi", use_last_error=True) # type: ignore[attr-defined]
110
+ kernel.GetCurrentProcess.restype = wintypes.HANDLE
111
+ psapi.GetProcessMemoryInfo.argtypes = [
112
+ wintypes.HANDLE,
113
+ ctypes.POINTER(_Counters),
114
+ wintypes.DWORD,
115
+ ]
116
+ if psapi.GetProcessMemoryInfo(
117
+ kernel.GetCurrentProcess(), ctypes.byref(counters), counters.cb
118
+ ):
119
+ out["rssKb"] = int(counters.WorkingSetSize // 1024)
120
+ out["hwmKb"] = int(counters.PeakWorkingSetSize // 1024)
121
+ out["maxRssKb"] = int(counters.PeakWorkingSetSize // 1024)
122
+ out["vmKb"] = int(counters.PagefileUsage // 1024)
123
+ except Exception:
124
+ pass
125
+ return out
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # stdin / stdout transport
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ class ThreadedStdoutWriter:
134
+ """The slice of `asyncio.StreamWriter` the pool uses: `write` + `drain`.
135
+
136
+ `write` only buffers; `drain` hands the buffer to ONE worker thread, which
137
+ writes and flushes it, so a slow host stalls the awaiting coroutine and
138
+ never the event loop. A single worker keeps bytes in the order written.
139
+ """
140
+
141
+ def __init__(self, out: BinaryIO, loop: asyncio.AbstractEventLoop) -> None:
142
+ self._out = out
143
+ self._loop = loop
144
+ self._pending = bytearray()
145
+ self._executor = concurrent.futures.ThreadPoolExecutor(
146
+ max_workers=1, thread_name_prefix="pool-stdout"
147
+ )
148
+
149
+ def write(self, data: bytes) -> None:
150
+ self._pending += data
151
+
152
+ def _flush(self, data: bytes) -> None:
153
+ self._out.write(data)
154
+ self._out.flush()
155
+
156
+ async def drain(self) -> None:
157
+ if not self._pending:
158
+ return
159
+ data = bytes(self._pending)
160
+ self._pending.clear()
161
+ await self._loop.run_in_executor(self._executor, self._flush, data)
162
+
163
+
164
+ def _pump_stdin(
165
+ fd: int, reader: asyncio.StreamReader, loop: asyncio.AbstractEventLoop
166
+ ) -> None:
167
+ """Thread body: stdin bytes -> the reader, EOF on close or error."""
168
+ try:
169
+ while True:
170
+ chunk = os.read(fd, _READ_CHUNK)
171
+ if not chunk:
172
+ break
173
+ loop.call_soon_threadsafe(reader.feed_data, chunk)
174
+ except OSError:
175
+ pass
176
+ finally:
177
+ try:
178
+ loop.call_soon_threadsafe(reader.feed_eof)
179
+ except RuntimeError:
180
+ pass # the loop is already closed — nothing is waiting
181
+
182
+
183
+ def open_threaded_stdio(
184
+ loop: asyncio.AbstractEventLoop, in_fd: int, out: BinaryIO
185
+ ) -> tuple[asyncio.StreamReader, Any]:
186
+ import threading
187
+
188
+ reader = asyncio.StreamReader(loop=loop)
189
+ threading.Thread(
190
+ target=_pump_stdin, args=(in_fd, reader, loop), name="pool-stdin", daemon=True
191
+ ).start()
192
+ return reader, ThreadedStdoutWriter(out, loop)
193
+
194
+
195
+ async def open_stdio(
196
+ loop: asyncio.AbstractEventLoop, platform: Optional[str] = None
197
+ ) -> tuple[asyncio.StreamReader, Any]:
198
+ """(reader, writer) over this process's stdin/stdout, binary, on any OS."""
199
+ if is_windows(platform):
200
+ import msvcrt
201
+
202
+ # Belt and braces: Python already opens the std handles binary, but a
203
+ # CRLF translation on a length-prefixed protocol corrupts every frame.
204
+ msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
205
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
206
+ return open_threaded_stdio(loop, sys.stdin.fileno(), sys.stdout.buffer)
207
+
208
+ reader = asyncio.StreamReader(loop=loop)
209
+ protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
210
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin.buffer)
211
+ transport, stdout_protocol = await loop.connect_write_pipe(
212
+ asyncio.streams.FlowControlMixin, sys.stdout.buffer
213
+ )
214
+ return reader, asyncio.StreamWriter(transport, stdout_protocol, None, loop)
@@ -0,0 +1,86 @@
1
+ """inference_pool.py must import and serve its seams on a machine with no
2
+ `fcntl` and no `resource` — which is what Windows is.
3
+
4
+ The first test is the gate: it removes both modules from the importer's world
5
+ (`sys.modules[name] = None` makes `import name` raise ImportError) and imports a
6
+ fresh copy of the pool. A top-level `import fcntl` fails it, exactly as it
7
+ failed the first Windows boot.
8
+
9
+ Run: python3 -m unittest test_inference_pool_windows -v
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import importlib
14
+ import os
15
+ import sys
16
+ import tempfile
17
+ import types
18
+ import unittest
19
+ from unittest import mock
20
+
21
+ try:
22
+ import numpy # noqa: F401
23
+ except ImportError:
24
+ _np = types.ModuleType("numpy")
25
+ _np.float32 = float
26
+ _np.array = lambda values, dtype=None: values # noqa: E731
27
+ sys.modules["numpy"] = _np
28
+
29
+ try:
30
+ import PIL.Image # noqa: F401
31
+ except ImportError:
32
+ _pil = types.ModuleType("PIL")
33
+ _pil_image = types.ModuleType("PIL.Image")
34
+ _pil.Image = _pil_image
35
+ sys.modules["PIL"] = _pil
36
+ sys.modules["PIL.Image"] = _pil_image
37
+
38
+ if "postprocessors" not in sys.modules:
39
+ _pp = types.ModuleType("postprocessors")
40
+ _pp.POSTPROCESSORS = {}
41
+ sys.modules["postprocessors"] = _pp
42
+
43
+ from test_pool_platform import FakeMsvcrt # noqa: E402
44
+
45
+
46
+ def import_pool_without_posix_modules() -> types.ModuleType:
47
+ sys.modules.pop("inference_pool", None)
48
+ with mock.patch.dict(sys.modules, {"fcntl": None, "resource": None}):
49
+ return importlib.import_module("inference_pool")
50
+
51
+
52
+ class WindowsImportTest(unittest.TestCase):
53
+ def test_imports_with_no_fcntl_and_no_resource(self) -> None:
54
+ pool = import_pool_without_posix_modules()
55
+ self.assertTrue(callable(pool.compile_cache_lock))
56
+
57
+ def test_compile_lock_uses_the_windows_range_lock(self) -> None:
58
+ pool = import_pool_without_posix_modules()
59
+ fake = FakeMsvcrt()
60
+ with tempfile.TemporaryDirectory() as tmp, mock.patch.dict(
61
+ sys.modules, {"msvcrt": fake}
62
+ ), mock.patch.object(sys, "platform", "win32"):
63
+ with pool.compile_cache_lock(os.path.join(tmp, "cache"), "model") as held:
64
+ self.assertTrue(held)
65
+ self.assertEqual([c[1:] for c in fake.calls], [(2, 1), (0, 1)])
66
+
67
+ def test_compile_lock_gives_up_after_the_timeout_when_it_is_held(self) -> None:
68
+ pool = import_pool_without_posix_modules()
69
+ fake = FakeMsvcrt(busy=True)
70
+ with tempfile.TemporaryDirectory() as tmp, mock.patch.dict(
71
+ sys.modules, {"msvcrt": fake}
72
+ ), mock.patch.object(sys, "platform", "win32"):
73
+ with pool.compile_cache_lock(tmp, "model", timeout_sec=0.1) as held:
74
+ self.assertFalse(held)
75
+
76
+ def test_memory_read_on_windows_comes_from_psapi_and_skips_proc(self) -> None:
77
+ pool = import_pool_without_posix_modules()
78
+ sample = {"rssKb": 1024, "maxRssKb": 2048}
79
+ with mock.patch.object(sys, "platform", "win32"), mock.patch.object(
80
+ pool.pool_platform, "read_windows_memory", return_value=sample
81
+ ), mock.patch("builtins.open", side_effect=AssertionError("must not read /proc")):
82
+ self.assertEqual(pool._read_self_memory(), sample)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ unittest.main()
@@ -0,0 +1,141 @@
1
+ """The POSIX/Windows seams of the inference pool (`pool_platform.py`).
2
+
3
+ Both branches are exercised on whatever machine runs this: the Windows branch
4
+ takes its platform as a parameter and a fake `msvcrt` module, so a Mac or a
5
+ Linux box proves the dispatch, the byte-range lock calls and the threaded
6
+ stdin/stdout transport. What it cannot prove is that `msvcrt.locking` and
7
+ psapi behave as modelled on real Windows — that is the one thing still waiting
8
+ for a Windows host.
9
+
10
+ Run: python3 -m unittest test_pool_platform -v
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ import types
19
+ import unittest
20
+ from unittest import mock
21
+
22
+ import pool_platform
23
+
24
+
25
+ class FakeMsvcrt(types.ModuleType):
26
+ LK_NBLCK = 2
27
+ LK_UNLCK = 0
28
+ O_BINARY = 0x8000
29
+
30
+ def __init__(self, busy: bool = False) -> None:
31
+ super().__init__("msvcrt")
32
+ self.busy = busy
33
+ self.calls: list[tuple[int, int, int]] = []
34
+ self.modes: list[tuple[int, int]] = []
35
+
36
+ def locking(self, fd: int, mode: int, nbytes: int) -> None:
37
+ self.calls.append((fd, mode, nbytes))
38
+ if self.busy and mode == self.LK_NBLCK:
39
+ raise OSError(13, "Permission denied")
40
+
41
+ def setmode(self, fd: int, flags: int) -> int:
42
+ self.modes.append((fd, flags))
43
+ return 0
44
+
45
+
46
+ class IsWindowsTest(unittest.TestCase):
47
+ def test_follows_the_explicit_platform_then_sys_platform(self) -> None:
48
+ self.assertTrue(pool_platform.is_windows("win32"))
49
+ self.assertFalse(pool_platform.is_windows("linux"))
50
+ self.assertFalse(pool_platform.is_windows("darwin"))
51
+ with mock.patch.object(sys, "platform", "win32"):
52
+ self.assertTrue(pool_platform.is_windows())
53
+
54
+
55
+ @unittest.skipIf(sys.platform == "win32", "flock is the POSIX branch")
56
+ class PosixLockTest(unittest.TestCase):
57
+ def test_second_holder_is_refused_until_the_first_unlocks(self) -> None:
58
+ with tempfile.TemporaryDirectory() as tmp:
59
+ path = os.path.join(tmp, "m.lock")
60
+ with open(path, "a+") as first, open(path, "a+") as second:
61
+ self.assertTrue(pool_platform.try_lock_exclusive(first.fileno()))
62
+ self.assertFalse(pool_platform.try_lock_exclusive(second.fileno()))
63
+ pool_platform.unlock(first.fileno())
64
+ self.assertTrue(pool_platform.try_lock_exclusive(second.fileno()))
65
+ pool_platform.unlock(second.fileno())
66
+
67
+
68
+ class WindowsLockTest(unittest.TestCase):
69
+ def test_takes_a_one_byte_non_blocking_range_lock(self) -> None:
70
+ fake = FakeMsvcrt()
71
+ with tempfile.TemporaryFile() as handle, mock.patch.dict(sys.modules, {"msvcrt": fake}):
72
+ self.assertTrue(pool_platform.try_lock_exclusive(handle.fileno(), "win32"))
73
+ pool_platform.unlock(handle.fileno(), "win32")
74
+ self.assertEqual([c[1:] for c in fake.calls], [(2, 1), (0, 1)])
75
+
76
+ def test_a_held_lock_is_false_not_an_exception(self) -> None:
77
+ fake = FakeMsvcrt(busy=True)
78
+ with tempfile.TemporaryFile() as handle, mock.patch.dict(sys.modules, {"msvcrt": fake}):
79
+ self.assertFalse(pool_platform.try_lock_exclusive(handle.fileno(), "win32"))
80
+
81
+
82
+ class ThreadedStdioTest(unittest.IsolatedAsyncioTestCase):
83
+ async def test_reader_gets_the_bytes_then_eof(self) -> None:
84
+ loop = asyncio.get_running_loop()
85
+ in_read, in_write = os.pipe()
86
+ out_read, out_write = os.pipe()
87
+ with os.fdopen(out_write, "wb") as out_file:
88
+ reader, writer = pool_platform.open_threaded_stdio(loop, in_read, out_file)
89
+ os.write(in_write, b"hello ")
90
+ os.write(in_write, b"world")
91
+ self.assertEqual(await asyncio.wait_for(reader.readexactly(11), 5), b"hello world")
92
+ os.close(in_write)
93
+ self.assertEqual(await asyncio.wait_for(reader.read(), 5), b"")
94
+ self.assertTrue(reader.at_eof())
95
+
96
+ writer.write(b"abc")
97
+ writer.write(b"def")
98
+ await writer.drain()
99
+ self.assertEqual(os.read(out_read, 6), b"abcdef")
100
+ os.close(out_read)
101
+ os.close(in_read)
102
+
103
+ async def test_writes_keep_their_order_across_drains(self) -> None:
104
+ loop = asyncio.get_running_loop()
105
+ in_read, in_write = os.pipe()
106
+ out_read, out_write = os.pipe()
107
+ with os.fdopen(out_write, "wb") as out_file:
108
+ _, writer = pool_platform.open_threaded_stdio(loop, in_read, out_file)
109
+ for chunk in (b"1", b"2", b"3"):
110
+ writer.write(chunk)
111
+ await writer.drain()
112
+ self.assertEqual(os.read(out_read, 3), b"123")
113
+ for fd in (in_write, in_read, out_read):
114
+ os.close(fd)
115
+
116
+ async def test_open_stdio_on_windows_sets_binary_mode_and_uses_the_threads(self) -> None:
117
+ loop = asyncio.get_running_loop()
118
+ fake = FakeMsvcrt()
119
+ in_read, in_write = os.pipe()
120
+ out_read, out_write = os.pipe()
121
+ with os.fdopen(out_write, "wb") as out_file:
122
+ fake_sys = types.SimpleNamespace(
123
+ stdin=types.SimpleNamespace(fileno=lambda: in_read),
124
+ stdout=types.SimpleNamespace(fileno=lambda: out_write, buffer=out_file),
125
+ )
126
+ with mock.patch.dict(sys.modules, {"msvcrt": fake}), mock.patch.object(
127
+ pool_platform, "sys", fake_sys
128
+ ), mock.patch.object(os, "O_BINARY", 0x8000, create=True):
129
+ reader, writer = await pool_platform.open_stdio(loop, "win32")
130
+ os.write(in_write, b"x")
131
+ self.assertEqual(await asyncio.wait_for(reader.readexactly(1), 5), b"x")
132
+ writer.write(b"y")
133
+ await writer.drain()
134
+ self.assertEqual(os.read(out_read, 1), b"y")
135
+ self.assertEqual([m[0] for m in fake.modes], [in_read, out_write])
136
+ for fd in (in_write, in_read, out_read):
137
+ os.close(fd)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ unittest.main()