@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.
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-K9Hx82GZ.js → custom-model-catalog-BsuqZLaN.js} +2 -2
- package/dist/{custom-model-catalog-ihWAlDr4.mjs → custom-model-catalog-DWFL7iBE.mjs} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BYCrV0VR.mjs → dist-73R05PFN.mjs} +26 -19
- package/dist/{dist-CYakbWA8.js → dist-DFGiI5lr.js} +25 -18
- package/dist/{model-catalogs-C9cyLHdH.js → model-catalogs-BefhGOAc.js} +1 -1
- package/dist/{model-catalogs-CX9SJ9in.mjs → model-catalogs-OLyh3TdB.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-C_qDNN9f.mjs → node-C0-Ik_JH.mjs} +3 -2
- package/dist/{node-CTCm8Vtw.js → node-CdOP9ELN.js} +4 -2
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-B3lkW9H-.mjs → process-memory-DKKVQV5j.mjs} +1 -1
- package/dist/{process-memory-Ds5S3L0F.js → process-memory-DO-ZBWdK.js} +1 -1
- package/dist/recorder/index.js +1675 -2
- package/dist/recorder/index.mjs +1675 -2
- package/dist/{segment-demux-js-W5FIgijf.mjs → segment-demux-js-C_27CmCq.mjs} +1 -1
- package/dist/{segment-demux-js-BgDpp15L.js → segment-demux-js-Dr3e9r4m.js} +1 -1
- 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
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-CDaDWDva.mjs → hostInit-Buho1uzb.mjs} +1 -1
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +12 -13
- package/python/pool_platform.py +214 -0
- package/python/test_inference_pool_windows.py +86 -0
- 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 {
|
|
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-
|
|
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
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_segment_demux_js = require("../segment-demux-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-
|
|
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
|
/**
|
|
@@ -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-
|
|
7
|
-
const require_node = require("../node-
|
|
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-
|
|
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,
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
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-
|
|
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-
|
|
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.
|
|
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",
|
package/python/inference_pool.py
CHANGED
|
@@ -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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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 =
|
|
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()
|