@scriptc/runtime 0.0.0 → 0.0.2

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 (416) hide show
  1. package/LICENSE +202 -0
  2. package/package.json +16 -6
  3. package/src/scr_array.c +488 -0
  4. package/src/scr_assert.c +1393 -0
  5. package/src/scr_async.c +2593 -0
  6. package/src/scr_async_dyn.c +607 -0
  7. package/src/scr_bytes.c +1425 -0
  8. package/src/scr_bytes_io.c +161 -0
  9. package/src/scr_child.c +3587 -0
  10. package/src/scr_closure.c +214 -0
  11. package/src/scr_console.c +143 -0
  12. package/src/scr_cycle.c +218 -0
  13. package/src/scr_dc.c +805 -0
  14. package/src/scr_dgram.c +1007 -0
  15. package/src/scr_dyn_handle.c +154 -0
  16. package/src/scr_dyn_invoke.c +644 -0
  17. package/src/scr_error.c +332 -0
  18. package/src/scr_events.c +714 -0
  19. package/src/scr_events_emitter.c +699 -0
  20. package/src/scr_exception.c +242 -0
  21. package/src/scr_fetch.c +1130 -0
  22. package/src/scr_fetch_curl.c +654 -0
  23. package/src/scr_http.c +3482 -0
  24. package/src/scr_http2.c +3030 -0
  25. package/src/scr_inspect.c +803 -0
  26. package/src/scr_inspect_island.c +51 -0
  27. package/src/scr_island.c +9363 -0
  28. package/src/scr_json.c +2151 -0
  29. package/src/scr_lib.c +3612 -0
  30. package/src/scr_loop_epoll.c +241 -0
  31. package/src/scr_loop_kqueue.c +121 -0
  32. package/src/scr_loop_wsapoll.c +208 -0
  33. package/src/scr_map.c +591 -0
  34. package/src/scr_net.c +3017 -0
  35. package/src/scr_net_island.c +458 -0
  36. package/src/scr_number.c +115 -0
  37. package/src/scr_object.c +25 -0
  38. package/src/scr_path.c +1266 -0
  39. package/src/scr_platform.h +119 -0
  40. package/src/scr_readline.c +287 -0
  41. package/src/scr_regex.c +1080 -0
  42. package/src/scr_runtime.h +5046 -0
  43. package/src/scr_stream.c +2877 -0
  44. package/src/scr_string.c +1264 -0
  45. package/src/scr_symbol.c +132 -0
  46. package/src/scr_test.c +662 -0
  47. package/src/scr_tls.c +1520 -0
  48. package/src/scr_union.c +105 -0
  49. package/src/scr_url.c +911 -0
  50. package/src/scr_url_internal.h +29 -0
  51. package/src/scr_url_params.c +561 -0
  52. package/src/scr_watch.c +568 -0
  53. package/src/scr_web.c +1778 -0
  54. package/src/scr_win.c +76 -0
  55. package/src/scr_zlib.c +197 -0
  56. package/src/scr_zlib_island.c +15 -0
  57. package/vendor/README.md +52 -0
  58. package/vendor/curl/COPYRIGHT +534 -0
  59. package/vendor/curl/include/curl/curl.h +3214 -0
  60. package/vendor/curl/include/curl/curlver.h +79 -0
  61. package/vendor/curl/include/curl/easy.h +125 -0
  62. package/vendor/curl/include/curl/header.h +74 -0
  63. package/vendor/curl/include/curl/mprintf.h +52 -0
  64. package/vendor/curl/include/curl/multi.h +460 -0
  65. package/vendor/curl/include/curl/options.h +70 -0
  66. package/vendor/curl/include/curl/stdcheaders.h +35 -0
  67. package/vendor/curl/include/curl/system.h +508 -0
  68. package/vendor/curl/include/curl/typecheck-gcc.h +716 -0
  69. package/vendor/curl/include/curl/urlapi.h +149 -0
  70. package/vendor/curl/include/curl/websockets.h +84 -0
  71. package/vendor/mbedtls/LICENSE +553 -0
  72. package/vendor/mbedtls/include/CMakeLists.txt +22 -0
  73. package/vendor/mbedtls/include/mbedtls/aes.h +631 -0
  74. package/vendor/mbedtls/include/mbedtls/aria.h +343 -0
  75. package/vendor/mbedtls/include/mbedtls/asn1.h +642 -0
  76. package/vendor/mbedtls/include/mbedtls/asn1write.h +390 -0
  77. package/vendor/mbedtls/include/mbedtls/base64.h +82 -0
  78. package/vendor/mbedtls/include/mbedtls/bignum.h +1088 -0
  79. package/vendor/mbedtls/include/mbedtls/block_cipher.h +76 -0
  80. package/vendor/mbedtls/include/mbedtls/build_info.h +194 -0
  81. package/vendor/mbedtls/include/mbedtls/camellia.h +305 -0
  82. package/vendor/mbedtls/include/mbedtls/ccm.h +526 -0
  83. package/vendor/mbedtls/include/mbedtls/chacha20.h +209 -0
  84. package/vendor/mbedtls/include/mbedtls/chachapoly.h +351 -0
  85. package/vendor/mbedtls/include/mbedtls/check_config.h +1149 -0
  86. package/vendor/mbedtls/include/mbedtls/cipher.h +1250 -0
  87. package/vendor/mbedtls/include/mbedtls/cmac.h +246 -0
  88. package/vendor/mbedtls/include/mbedtls/compat-2.x.h +46 -0
  89. package/vendor/mbedtls/include/mbedtls/config_adjust_legacy_crypto.h +578 -0
  90. package/vendor/mbedtls/include/mbedtls/config_adjust_legacy_from_psa.h +873 -0
  91. package/vendor/mbedtls/include/mbedtls/config_adjust_psa_from_legacy.h +359 -0
  92. package/vendor/mbedtls/include/mbedtls/config_adjust_psa_superset_legacy.h +145 -0
  93. package/vendor/mbedtls/include/mbedtls/config_adjust_ssl.h +91 -0
  94. package/vendor/mbedtls/include/mbedtls/config_adjust_x509.h +35 -0
  95. package/vendor/mbedtls/include/mbedtls/config_psa.h +61 -0
  96. package/vendor/mbedtls/include/mbedtls/constant_time.h +36 -0
  97. package/vendor/mbedtls/include/mbedtls/ctr_drbg.h +596 -0
  98. package/vendor/mbedtls/include/mbedtls/debug.h +156 -0
  99. package/vendor/mbedtls/include/mbedtls/des.h +385 -0
  100. package/vendor/mbedtls/include/mbedtls/dhm.h +972 -0
  101. package/vendor/mbedtls/include/mbedtls/ecdh.h +455 -0
  102. package/vendor/mbedtls/include/mbedtls/ecdsa.h +674 -0
  103. package/vendor/mbedtls/include/mbedtls/ecjpake.h +298 -0
  104. package/vendor/mbedtls/include/mbedtls/ecp.h +1517 -0
  105. package/vendor/mbedtls/include/mbedtls/entropy.h +274 -0
  106. package/vendor/mbedtls/include/mbedtls/error.h +201 -0
  107. package/vendor/mbedtls/include/mbedtls/gcm.h +387 -0
  108. package/vendor/mbedtls/include/mbedtls/hkdf.h +124 -0
  109. package/vendor/mbedtls/include/mbedtls/hmac_drbg.h +434 -0
  110. package/vendor/mbedtls/include/mbedtls/lms.h +440 -0
  111. package/vendor/mbedtls/include/mbedtls/mbedtls_config.h +4446 -0
  112. package/vendor/mbedtls/include/mbedtls/md.h +526 -0
  113. package/vendor/mbedtls/include/mbedtls/md5.h +190 -0
  114. package/vendor/mbedtls/include/mbedtls/memory_buffer_alloc.h +142 -0
  115. package/vendor/mbedtls/include/mbedtls/net_sockets.h +299 -0
  116. package/vendor/mbedtls/include/mbedtls/nist_kw.h +166 -0
  117. package/vendor/mbedtls/include/mbedtls/oid.h +727 -0
  118. package/vendor/mbedtls/include/mbedtls/pem.h +160 -0
  119. package/vendor/mbedtls/include/mbedtls/pk.h +1303 -0
  120. package/vendor/mbedtls/include/mbedtls/pkcs12.h +186 -0
  121. package/vendor/mbedtls/include/mbedtls/pkcs5.h +198 -0
  122. package/vendor/mbedtls/include/mbedtls/pkcs7.h +252 -0
  123. package/vendor/mbedtls/include/mbedtls/platform.h +516 -0
  124. package/vendor/mbedtls/include/mbedtls/platform_time.h +79 -0
  125. package/vendor/mbedtls/include/mbedtls/platform_util.h +247 -0
  126. package/vendor/mbedtls/include/mbedtls/poly1305.h +168 -0
  127. package/vendor/mbedtls/include/mbedtls/private_access.h +20 -0
  128. package/vendor/mbedtls/include/mbedtls/psa_util.h +207 -0
  129. package/vendor/mbedtls/include/mbedtls/ripemd160.h +136 -0
  130. package/vendor/mbedtls/include/mbedtls/rsa.h +1170 -0
  131. package/vendor/mbedtls/include/mbedtls/sha1.h +219 -0
  132. package/vendor/mbedtls/include/mbedtls/sha256.h +200 -0
  133. package/vendor/mbedtls/include/mbedtls/sha3.h +172 -0
  134. package/vendor/mbedtls/include/mbedtls/sha512.h +208 -0
  135. package/vendor/mbedtls/include/mbedtls/ssl.h +5887 -0
  136. package/vendor/mbedtls/include/mbedtls/ssl_cache.h +187 -0
  137. package/vendor/mbedtls/include/mbedtls/ssl_ciphersuites.h +482 -0
  138. package/vendor/mbedtls/include/mbedtls/ssl_cookie.h +106 -0
  139. package/vendor/mbedtls/include/mbedtls/ssl_ticket.h +199 -0
  140. package/vendor/mbedtls/include/mbedtls/threading.h +167 -0
  141. package/vendor/mbedtls/include/mbedtls/timing.h +94 -0
  142. package/vendor/mbedtls/include/mbedtls/version.h +78 -0
  143. package/vendor/mbedtls/include/mbedtls/x509.h +500 -0
  144. package/vendor/mbedtls/include/mbedtls/x509_crl.h +184 -0
  145. package/vendor/mbedtls/include/mbedtls/x509_crt.h +1208 -0
  146. package/vendor/mbedtls/include/mbedtls/x509_csr.h +382 -0
  147. package/vendor/mbedtls/include/psa/build_info.h +20 -0
  148. package/vendor/mbedtls/include/psa/crypto.h +4998 -0
  149. package/vendor/mbedtls/include/psa/crypto_adjust_auto_enabled.h +31 -0
  150. package/vendor/mbedtls/include/psa/crypto_adjust_config_dependencies.h +51 -0
  151. package/vendor/mbedtls/include/psa/crypto_adjust_config_key_pair_types.h +101 -0
  152. package/vendor/mbedtls/include/psa/crypto_adjust_config_synonyms.h +49 -0
  153. package/vendor/mbedtls/include/psa/crypto_builtin_composites.h +214 -0
  154. package/vendor/mbedtls/include/psa/crypto_builtin_key_derivation.h +118 -0
  155. package/vendor/mbedtls/include/psa/crypto_builtin_primitives.h +114 -0
  156. package/vendor/mbedtls/include/psa/crypto_compat.h +230 -0
  157. package/vendor/mbedtls/include/psa/crypto_config.h +145 -0
  158. package/vendor/mbedtls/include/psa/crypto_driver_common.h +44 -0
  159. package/vendor/mbedtls/include/psa/crypto_driver_contexts_composites.h +151 -0
  160. package/vendor/mbedtls/include/psa/crypto_driver_contexts_key_derivation.h +52 -0
  161. package/vendor/mbedtls/include/psa/crypto_driver_contexts_primitives.h +105 -0
  162. package/vendor/mbedtls/include/psa/crypto_extra.h +2145 -0
  163. package/vendor/mbedtls/include/psa/crypto_legacy.h +88 -0
  164. package/vendor/mbedtls/include/psa/crypto_platform.h +102 -0
  165. package/vendor/mbedtls/include/psa/crypto_se_driver.h +1383 -0
  166. package/vendor/mbedtls/include/psa/crypto_sizes.h +1319 -0
  167. package/vendor/mbedtls/include/psa/crypto_struct.h +527 -0
  168. package/vendor/mbedtls/include/psa/crypto_types.h +508 -0
  169. package/vendor/mbedtls/include/psa/crypto_values.h +2782 -0
  170. package/vendor/mbedtls/library/aes.c +2294 -0
  171. package/vendor/mbedtls/library/aesce.c +624 -0
  172. package/vendor/mbedtls/library/aesce.h +136 -0
  173. package/vendor/mbedtls/library/aesni.c +846 -0
  174. package/vendor/mbedtls/library/aesni.h +162 -0
  175. package/vendor/mbedtls/library/alignment.h +704 -0
  176. package/vendor/mbedtls/library/aria.c +969 -0
  177. package/vendor/mbedtls/library/asn1parse.c +468 -0
  178. package/vendor/mbedtls/library/asn1write.c +440 -0
  179. package/vendor/mbedtls/library/base64.c +322 -0
  180. package/vendor/mbedtls/library/base64_internal.h +45 -0
  181. package/vendor/mbedtls/library/bignum.c +2583 -0
  182. package/vendor/mbedtls/library/bignum_core.c +1240 -0
  183. package/vendor/mbedtls/library/bignum_core.h +872 -0
  184. package/vendor/mbedtls/library/bignum_core_invasive.h +38 -0
  185. package/vendor/mbedtls/library/bignum_internal.h +122 -0
  186. package/vendor/mbedtls/library/bignum_mod.c +394 -0
  187. package/vendor/mbedtls/library/bignum_mod.h +452 -0
  188. package/vendor/mbedtls/library/bignum_mod_raw.c +276 -0
  189. package/vendor/mbedtls/library/bignum_mod_raw.h +416 -0
  190. package/vendor/mbedtls/library/bignum_mod_raw_invasive.h +34 -0
  191. package/vendor/mbedtls/library/block_cipher.c +207 -0
  192. package/vendor/mbedtls/library/block_cipher_internal.h +99 -0
  193. package/vendor/mbedtls/library/bn_mul.h +1094 -0
  194. package/vendor/mbedtls/library/camellia.c +1058 -0
  195. package/vendor/mbedtls/library/ccm.c +777 -0
  196. package/vendor/mbedtls/library/chacha20.c +563 -0
  197. package/vendor/mbedtls/library/chacha20_internal.h +23 -0
  198. package/vendor/mbedtls/library/chachapoly.c +487 -0
  199. package/vendor/mbedtls/library/check_crypto_config.h +136 -0
  200. package/vendor/mbedtls/library/cipher.c +1712 -0
  201. package/vendor/mbedtls/library/cipher_invasive.h +28 -0
  202. package/vendor/mbedtls/library/cipher_wrap.c +2482 -0
  203. package/vendor/mbedtls/library/cipher_wrap.h +178 -0
  204. package/vendor/mbedtls/library/cmac.c +1067 -0
  205. package/vendor/mbedtls/library/common.h +487 -0
  206. package/vendor/mbedtls/library/constant_time.c +247 -0
  207. package/vendor/mbedtls/library/constant_time_impl.h +541 -0
  208. package/vendor/mbedtls/library/constant_time_internal.h +579 -0
  209. package/vendor/mbedtls/library/ctr.h +35 -0
  210. package/vendor/mbedtls/library/ctr_drbg.c +1016 -0
  211. package/vendor/mbedtls/library/debug.c +475 -0
  212. package/vendor/mbedtls/library/debug_internal.h +185 -0
  213. package/vendor/mbedtls/library/des.c +1042 -0
  214. package/vendor/mbedtls/library/dhm.c +700 -0
  215. package/vendor/mbedtls/library/ecdh.c +696 -0
  216. package/vendor/mbedtls/library/ecdsa.c +858 -0
  217. package/vendor/mbedtls/library/ecjpake.c +1216 -0
  218. package/vendor/mbedtls/library/ecp.c +3674 -0
  219. package/vendor/mbedtls/library/ecp_curves.c +6125 -0
  220. package/vendor/mbedtls/library/ecp_curves_new.c +9 -0
  221. package/vendor/mbedtls/library/ecp_internal_alt.h +287 -0
  222. package/vendor/mbedtls/library/ecp_invasive.h +305 -0
  223. package/vendor/mbedtls/library/entropy.c +680 -0
  224. package/vendor/mbedtls/library/entropy_poll.c +233 -0
  225. package/vendor/mbedtls/library/entropy_poll.h +64 -0
  226. package/vendor/mbedtls/library/error.c +878 -0
  227. package/vendor/mbedtls/library/gcm.c +1330 -0
  228. package/vendor/mbedtls/library/hkdf.c +161 -0
  229. package/vendor/mbedtls/library/hmac_drbg.c +633 -0
  230. package/vendor/mbedtls/library/lmots.c +789 -0
  231. package/vendor/mbedtls/library/lmots.h +288 -0
  232. package/vendor/mbedtls/library/lms.c +778 -0
  233. package/vendor/mbedtls/library/md.c +1108 -0
  234. package/vendor/mbedtls/library/md5.c +426 -0
  235. package/vendor/mbedtls/library/md_psa.h +26 -0
  236. package/vendor/mbedtls/library/md_wrap.h +46 -0
  237. package/vendor/mbedtls/library/memory_buffer_alloc.c +751 -0
  238. package/vendor/mbedtls/library/mps_common.h +181 -0
  239. package/vendor/mbedtls/library/mps_error.h +89 -0
  240. package/vendor/mbedtls/library/mps_reader.c +538 -0
  241. package/vendor/mbedtls/library/mps_reader.h +366 -0
  242. package/vendor/mbedtls/library/mps_trace.c +112 -0
  243. package/vendor/mbedtls/library/mps_trace.h +154 -0
  244. package/vendor/mbedtls/library/net_sockets.c +694 -0
  245. package/vendor/mbedtls/library/nist_kw.c +729 -0
  246. package/vendor/mbedtls/library/oid.c +1166 -0
  247. package/vendor/mbedtls/library/padlock.c +157 -0
  248. package/vendor/mbedtls/library/padlock.h +111 -0
  249. package/vendor/mbedtls/library/pem.c +554 -0
  250. package/vendor/mbedtls/library/pk.c +1602 -0
  251. package/vendor/mbedtls/library/pk_ecc.c +261 -0
  252. package/vendor/mbedtls/library/pk_internal.h +241 -0
  253. package/vendor/mbedtls/library/pk_wrap.c +1618 -0
  254. package/vendor/mbedtls/library/pk_wrap.h +138 -0
  255. package/vendor/mbedtls/library/pkcs12.c +437 -0
  256. package/vendor/mbedtls/library/pkcs5.c +500 -0
  257. package/vendor/mbedtls/library/pkcs7.c +787 -0
  258. package/vendor/mbedtls/library/pkparse.c +1392 -0
  259. package/vendor/mbedtls/library/pkwrite.c +631 -0
  260. package/vendor/mbedtls/library/pkwrite.h +121 -0
  261. package/vendor/mbedtls/library/platform.c +402 -0
  262. package/vendor/mbedtls/library/platform_util.c +258 -0
  263. package/vendor/mbedtls/library/poly1305.c +492 -0
  264. package/vendor/mbedtls/library/psa_crypto.c +9532 -0
  265. package/vendor/mbedtls/library/psa_crypto_aead.c +646 -0
  266. package/vendor/mbedtls/library/psa_crypto_aead.h +499 -0
  267. package/vendor/mbedtls/library/psa_crypto_cipher.c +747 -0
  268. package/vendor/mbedtls/library/psa_crypto_cipher.h +316 -0
  269. package/vendor/mbedtls/library/psa_crypto_client.c +22 -0
  270. package/vendor/mbedtls/library/psa_crypto_core.h +983 -0
  271. package/vendor/mbedtls/library/psa_crypto_core_common.h +52 -0
  272. package/vendor/mbedtls/library/psa_crypto_driver_wrappers.h +2896 -0
  273. package/vendor/mbedtls/library/psa_crypto_driver_wrappers_no_static.c +256 -0
  274. package/vendor/mbedtls/library/psa_crypto_driver_wrappers_no_static.h +31 -0
  275. package/vendor/mbedtls/library/psa_crypto_ecp.c +594 -0
  276. package/vendor/mbedtls/library/psa_crypto_ecp.h +267 -0
  277. package/vendor/mbedtls/library/psa_crypto_ffdh.c +361 -0
  278. package/vendor/mbedtls/library/psa_crypto_ffdh.h +131 -0
  279. package/vendor/mbedtls/library/psa_crypto_hash.c +470 -0
  280. package/vendor/mbedtls/library/psa_crypto_hash.h +211 -0
  281. package/vendor/mbedtls/library/psa_crypto_invasive.h +92 -0
  282. package/vendor/mbedtls/library/psa_crypto_its.h +131 -0
  283. package/vendor/mbedtls/library/psa_crypto_mac.c +505 -0
  284. package/vendor/mbedtls/library/psa_crypto_mac.h +264 -0
  285. package/vendor/mbedtls/library/psa_crypto_pake.c +571 -0
  286. package/vendor/mbedtls/library/psa_crypto_pake.h +159 -0
  287. package/vendor/mbedtls/library/psa_crypto_random.c +181 -0
  288. package/vendor/mbedtls/library/psa_crypto_random.h +72 -0
  289. package/vendor/mbedtls/library/psa_crypto_random_impl.h +200 -0
  290. package/vendor/mbedtls/library/psa_crypto_rsa.c +714 -0
  291. package/vendor/mbedtls/library/psa_crypto_rsa.h +321 -0
  292. package/vendor/mbedtls/library/psa_crypto_se.c +373 -0
  293. package/vendor/mbedtls/library/psa_crypto_se.h +192 -0
  294. package/vendor/mbedtls/library/psa_crypto_slot_management.c +1137 -0
  295. package/vendor/mbedtls/library/psa_crypto_slot_management.h +344 -0
  296. package/vendor/mbedtls/library/psa_crypto_storage.c +481 -0
  297. package/vendor/mbedtls/library/psa_crypto_storage.h +392 -0
  298. package/vendor/mbedtls/library/psa_its_file.c +254 -0
  299. package/vendor/mbedtls/library/psa_util.c +614 -0
  300. package/vendor/mbedtls/library/psa_util_internal.h +100 -0
  301. package/vendor/mbedtls/library/ripemd160.c +490 -0
  302. package/vendor/mbedtls/library/rsa.c +3121 -0
  303. package/vendor/mbedtls/library/rsa_alt_helpers.c +455 -0
  304. package/vendor/mbedtls/library/rsa_alt_helpers.h +209 -0
  305. package/vendor/mbedtls/library/rsa_internal.h +188 -0
  306. package/vendor/mbedtls/library/sha1.c +480 -0
  307. package/vendor/mbedtls/library/sha256.c +983 -0
  308. package/vendor/mbedtls/library/sha3.c +721 -0
  309. package/vendor/mbedtls/library/sha512.c +1115 -0
  310. package/vendor/mbedtls/library/ssl_cache.c +410 -0
  311. package/vendor/mbedtls/library/ssl_ciphersuites.c +2050 -0
  312. package/vendor/mbedtls/library/ssl_ciphersuites_internal.h +154 -0
  313. package/vendor/mbedtls/library/ssl_client.c +1023 -0
  314. package/vendor/mbedtls/library/ssl_client.h +22 -0
  315. package/vendor/mbedtls/library/ssl_cookie.c +384 -0
  316. package/vendor/mbedtls/library/ssl_debug_helpers.h +85 -0
  317. package/vendor/mbedtls/library/ssl_debug_helpers_generated.c +251 -0
  318. package/vendor/mbedtls/library/ssl_misc.h +3198 -0
  319. package/vendor/mbedtls/library/ssl_msg.c +6619 -0
  320. package/vendor/mbedtls/library/ssl_ticket.c +556 -0
  321. package/vendor/mbedtls/library/ssl_tls.c +10234 -0
  322. package/vendor/mbedtls/library/ssl_tls12_client.c +3688 -0
  323. package/vendor/mbedtls/library/ssl_tls12_server.c +4311 -0
  324. package/vendor/mbedtls/library/ssl_tls13_client.c +3208 -0
  325. package/vendor/mbedtls/library/ssl_tls13_generic.c +1793 -0
  326. package/vendor/mbedtls/library/ssl_tls13_invasive.h +23 -0
  327. package/vendor/mbedtls/library/ssl_tls13_keys.c +1918 -0
  328. package/vendor/mbedtls/library/ssl_tls13_keys.h +668 -0
  329. package/vendor/mbedtls/library/ssl_tls13_server.c +3624 -0
  330. package/vendor/mbedtls/library/threading.c +193 -0
  331. package/vendor/mbedtls/library/threading_internal.h +28 -0
  332. package/vendor/mbedtls/library/timing.c +152 -0
  333. package/vendor/mbedtls/library/version.c +32 -0
  334. package/vendor/mbedtls/library/version_features.c +856 -0
  335. package/vendor/mbedtls/library/x509.c +1776 -0
  336. package/vendor/mbedtls/library/x509_create.c +570 -0
  337. package/vendor/mbedtls/library/x509_crl.c +708 -0
  338. package/vendor/mbedtls/library/x509_crt.c +3311 -0
  339. package/vendor/mbedtls/library/x509_csr.c +648 -0
  340. package/vendor/mbedtls/library/x509_internal.h +101 -0
  341. package/vendor/mbedtls/library/x509write.c +174 -0
  342. package/vendor/mbedtls/library/x509write_crt.c +688 -0
  343. package/vendor/mbedtls/library/x509write_csr.c +336 -0
  344. package/vendor/quickjs-ng/CMakeLists.txt +566 -0
  345. package/vendor/quickjs-ng/LICENSE +24 -0
  346. package/vendor/quickjs-ng/README.md +24 -0
  347. package/vendor/quickjs-ng/api-test.c +1195 -0
  348. package/vendor/quickjs-ng/builtin-array-fromasync.h +119 -0
  349. package/vendor/quickjs-ng/builtin-iterator-zip-keyed.h +332 -0
  350. package/vendor/quickjs-ng/builtin-iterator-zip.h +337 -0
  351. package/vendor/quickjs-ng/cutils.h +1998 -0
  352. package/vendor/quickjs-ng/dtoa.c +1619 -0
  353. package/vendor/quickjs-ng/dtoa.h +87 -0
  354. package/vendor/quickjs-ng/gen/function_source.c +81 -0
  355. package/vendor/quickjs-ng/gen/hello.c +53 -0
  356. package/vendor/quickjs-ng/gen/hello_module.c +106 -0
  357. package/vendor/quickjs-ng/gen/repl.c +3036 -0
  358. package/vendor/quickjs-ng/gen/standalone.c +323 -0
  359. package/vendor/quickjs-ng/gen/test_fib.c +81 -0
  360. package/vendor/quickjs-ng/libregexp-opcode.h +73 -0
  361. package/vendor/quickjs-ng/libregexp.c +3474 -0
  362. package/vendor/quickjs-ng/libregexp.h +101 -0
  363. package/vendor/quickjs-ng/libunicode-table.h +5173 -0
  364. package/vendor/quickjs-ng/libunicode.c +2069 -0
  365. package/vendor/quickjs-ng/libunicode.h +172 -0
  366. package/vendor/quickjs-ng/list.h +107 -0
  367. package/vendor/quickjs-ng/lre-test.c +52 -0
  368. package/vendor/quickjs-ng/qjs.c +762 -0
  369. package/vendor/quickjs-ng/qjsc.c +673 -0
  370. package/vendor/quickjs-ng/quickjs-atom.h +280 -0
  371. package/vendor/quickjs-ng/quickjs-c-atomics.h +54 -0
  372. package/vendor/quickjs-ng/quickjs-libc.c +5037 -0
  373. package/vendor/quickjs-ng/quickjs-libc.h +87 -0
  374. package/vendor/quickjs-ng/quickjs-opcode.h +377 -0
  375. package/vendor/quickjs-ng/quickjs.c +64041 -0
  376. package/vendor/quickjs-ng/quickjs.h +1447 -0
  377. package/vendor/quickjs-ng/run-test262.c +2374 -0
  378. package/vendor/quickjs-ng/unicode_gen.c +3121 -0
  379. package/vendor/quickjs-ng/unicode_gen_def.h +310 -0
  380. package/vendor/ryu/LICENSE-Boost +23 -0
  381. package/vendor/ryu/common.h +114 -0
  382. package/vendor/ryu/d2s.c +509 -0
  383. package/vendor/ryu/d2s_full_table.h +367 -0
  384. package/vendor/ryu/d2s_intrinsics.h +357 -0
  385. package/vendor/ryu/d2s_small_table.h +186 -0
  386. package/vendor/ryu/digit_table.h +35 -0
  387. package/vendor/ryu/ryu.h +46 -0
  388. package/vendor/zlib/LICENSE +22 -0
  389. package/vendor/zlib/adler32.c +164 -0
  390. package/vendor/zlib/compress.c +75 -0
  391. package/vendor/zlib/crc32.c +1049 -0
  392. package/vendor/zlib/crc32.h +9446 -0
  393. package/vendor/zlib/deflate.c +2139 -0
  394. package/vendor/zlib/deflate.h +377 -0
  395. package/vendor/zlib/gzclose.c +23 -0
  396. package/vendor/zlib/gzguts.h +214 -0
  397. package/vendor/zlib/gzlib.c +582 -0
  398. package/vendor/zlib/gzread.c +602 -0
  399. package/vendor/zlib/gzwrite.c +631 -0
  400. package/vendor/zlib/infback.c +628 -0
  401. package/vendor/zlib/inffast.c +320 -0
  402. package/vendor/zlib/inffast.h +11 -0
  403. package/vendor/zlib/inffixed.h +94 -0
  404. package/vendor/zlib/inflate.c +1526 -0
  405. package/vendor/zlib/inflate.h +126 -0
  406. package/vendor/zlib/inftrees.c +299 -0
  407. package/vendor/zlib/inftrees.h +62 -0
  408. package/vendor/zlib/trees.c +1117 -0
  409. package/vendor/zlib/trees.h +128 -0
  410. package/vendor/zlib/uncompr.c +85 -0
  411. package/vendor/zlib/zconf.h +543 -0
  412. package/vendor/zlib/zlib.h +1938 -0
  413. package/vendor/zlib/zutil.c +299 -0
  414. package/vendor/zlib/zutil.h +254 -0
  415. package/README.md +0 -3
  416. package/index.js +0 -2
@@ -0,0 +1,3587 @@
1
+ /* child_process — the synchronous slice: spawnSync as posix_spawnp +
2
+ * piped utf8 capture + waitpid.
3
+ *
4
+ * Node parity notes (SEMANTICS.md has the user-facing story):
5
+ * - The command PATH-searches like Node's (posix_spawnp; a command
6
+ * containing '/' spawns directly). No shell, ever.
7
+ * - stdin is /dev/null (Node's spawnSync default reads nothing without
8
+ * `input`); stdout/stderr are captured to strings, utf8 assumed — the
9
+ * compiler fences every other encoding.
10
+ * - `status` is the exit code; a child killed by a signal has NO status
11
+ * (has_status = false → the compiler's `number | null` union takes the
12
+ * null arm), exactly Node's status: null there.
13
+ * - Spawn FAILURE (nonexistent binary, EACCES) never throws: Node reports
14
+ * it through the result's `error` property, which this surface does not
15
+ * carry — status is null and both outputs are "" (Node types them null;
16
+ * divergence documented).
17
+ * - No zombies: every successfully spawned child is waitpid()ed before
18
+ * this function returns, unconditionally.
19
+ */
20
+ #include "scr_runtime.h"
21
+
22
+ #ifdef _WIN32
23
+ /* ── Windows arm: real children via CreateProcessW ────────────────────
24
+ * The POSIX arm below is untouched; this arm reimplements the exported
25
+ * surface over Win32 primitives, matching Node-on-Windows (libuv) and
26
+ * verified against the windows-dev box's Node:
27
+ * - Executable resolution ports libuv's search_path: a command with a
28
+ * path separator resolves against the effective cwd; a bare name
29
+ * searches cwd first (CreateProcess's legacy behavior, kept by libuv)
30
+ * then each PATH entry — trying the literal name only when it has an
31
+ * extension, then appending (never replacing) .com and .exe.
32
+ * Resolution failure is Node's ENOENT spawn failure.
33
+ * - The command line is libuv's quote_cmd_arg algorithm, ported exactly:
34
+ * args without space/tab/quote ride verbatim, args with neither quote
35
+ * nor backslash just get wrapped, everything else takes the
36
+ * reverse-scan escape (backslash runs double only before a quote or
37
+ * the closing wrap; quotes escape with a backslash; "" for empty).
38
+ * - A replaced env becomes a SORTED UTF-16 block (CompareStringOrdinal,
39
+ * ordinal case-insensitive on the key — libuv's env_strncmp) with
40
+ * libuv's required vars (SYSTEMROOT, PATH, TEMP, ...) copied from the
41
+ * parent when missing, so winsock and PATH-dependent children keep
42
+ * working; the child's PATH (its own, or the merged parent's) is what
43
+ * the executable search walks, like uv_spawn.
44
+ * - stdio is HANDLE inheritance: anonymous pipes for capture and the
45
+ * child.stdout/stderr streams (PeekNamedPipe polls readiness —
46
+ * anonymous pipes cannot overlap), NUL for ignore, duplicated std
47
+ * handles for inherit, _get_osfhandle for the fd form, PIPE_NOWAIT on
48
+ * the input pipe's write end so the sync feed never blocks.
49
+ * - There are no signals: kill/killSignal for SIGTERM/SIGKILL/SIGINT/
50
+ * SIGQUIT/SIGHUP is TerminateProcess(handle, 1) with the signal NAME
51
+ * reported on the result/exit event, libuv's uv_kill emulation (a
52
+ * killed child's status is null + the signal name; nothing else can
53
+ * die "to a signal"). Other names answer false (libuv: ENOSYS).
54
+ * - The async family rides the loop's existing hooks: scr_children_wait
55
+ * waits on the child process HANDLES (WaitForMultipleObjects) so an
56
+ * exit wakes the quiescent sleep immediately; piped streams with a
57
+ * consumer and pending spawn failures DECLINE the wait, keeping the
58
+ * loop's ~1ms polling cap (pipe readability has no waitable handle),
59
+ * exactly the POSIX arm's unwatched fallback.
60
+ * - execSync's shell is lowered as /bin/sh at compile time, which
61
+ * resolves ENOENT here where Windows Node would run cmd.exe — the
62
+ * documented shell fence until a cmd.exe story exists (SEMANTICS).
63
+ * - libuv also assigns children to a kill-on-parent-death job object;
64
+ * this arm does not (a parent crash can orphan a running child — the
65
+ * sync cores always reap or kill before returning). */
66
+
67
+ #include <windows.h>
68
+
69
+ #include <io.h>
70
+ #include <signal.h>
71
+ #include <stdio.h>
72
+ #include <stdlib.h>
73
+ #include <string.h>
74
+ #include <wchar.h>
75
+
76
+ /* uv's emulated numbers for the rows mingw's signal.h lacks (the same
77
+ * values scr_lib.c's name table uses). */
78
+ #ifndef SIGHUP
79
+ #define SIGHUP 1
80
+ #endif
81
+ #ifndef SIGQUIT
82
+ #define SIGQUIT 3
83
+ #endif
84
+ #ifndef SIGKILL
85
+ #define SIGKILL 9
86
+ #endif
87
+
88
+ static void scr_child_oom(void) {
89
+ fputs("scriptc: out of memory\n", stderr);
90
+ abort();
91
+ }
92
+
93
+ /* ── small helpers ───────────────────────────────────────────────────── */
94
+
95
+ /* utf8 (the runtime's strings) → malloc'd, NUL-terminated UTF-16. */
96
+ static WCHAR *scr_child_wide(const char *s, size_t len) {
97
+ int n = len > 0 ? MultiByteToWideChar(CP_UTF8, 0, s, (int)len, NULL, 0) : 0;
98
+ WCHAR *w = malloc(((size_t)n + 1) * sizeof(WCHAR));
99
+ if (!w) scr_child_oom();
100
+ if (n > 0) MultiByteToWideChar(CP_UTF8, 0, s, (int)len, w, n);
101
+ w[n] = L'\0';
102
+ return w;
103
+ }
104
+
105
+ /* Node's error-code names for the failures a spawn can hit (libuv's
106
+ * sys-error translation, the reachable rows). */
107
+ static const char *scr_win_errname(DWORD err) {
108
+ switch (err) {
109
+ case ERROR_FILE_NOT_FOUND:
110
+ case ERROR_PATH_NOT_FOUND:
111
+ case ERROR_INVALID_NAME:
112
+ case ERROR_DIRECTORY:
113
+ case ERROR_BAD_PATHNAME:
114
+ return "ENOENT";
115
+ case ERROR_ACCESS_DENIED:
116
+ return "EACCES";
117
+ case ERROR_NOT_ENOUGH_MEMORY:
118
+ case ERROR_OUTOFMEMORY:
119
+ return "ENOMEM";
120
+ case ERROR_BAD_EXE_FORMAT:
121
+ return "EFTYPE"; /* libuv's map for a non-PE spawn target */
122
+ default:
123
+ return "EUNKNOWN";
124
+ }
125
+ }
126
+
127
+ /* child.exitCode after a spawn-failure settle: Node flips it to the
128
+ * NEGATIVE uv errno, whose values are Windows-specific (uv-errno.h). */
129
+ static int scr_win_uv_errno(const char *name) {
130
+ if (strcmp(name, "ENOENT") == 0) return -4058;
131
+ if (strcmp(name, "EACCES") == 0) return -4092;
132
+ return -4094; /* UV_UNKNOWN */
133
+ }
134
+
135
+ /* ── libuv's quote_cmd_arg, ported exactly ───────────────────────────── */
136
+
137
+ /* Quotes one argument into `target` (caller sized it: 2*len + 2 worst
138
+ * case); returns the next write position. The three tiers and the
139
+ * reverse-scan escape are libuv's, byte-for-byte — Windows Node builds
140
+ * its child command lines with this exact function, so matching it IS
141
+ * the parity story (CommandLineToArgvW round-trips every case). */
142
+ static WCHAR *scr_quote_cmd_arg(const WCHAR *source, WCHAR *target) {
143
+ size_t len = wcslen(source);
144
+ size_t i;
145
+ int quote_hit;
146
+ WCHAR *start;
147
+
148
+ if (len == 0) {
149
+ /* Need double quotation for empty argument. */
150
+ *(target++) = L'"';
151
+ *(target++) = L'"';
152
+ return target;
153
+ }
154
+
155
+ if (NULL == wcspbrk(source, L" \t\"")) {
156
+ /* No quotation needed. */
157
+ wcsncpy(target, source, len);
158
+ target += len;
159
+ return target;
160
+ }
161
+
162
+ if (NULL == wcspbrk(source, L"\"\\")) {
163
+ /* No embedded double quotes or backslashes: wrap in quote marks. */
164
+ *(target++) = L'"';
165
+ wcsncpy(target, source, len);
166
+ target += len;
167
+ *(target++) = L'"';
168
+ return target;
169
+ }
170
+
171
+ /* The expensive way: reverse scan so a backslash doubles exactly when
172
+ * a quote (or the closing wrap) follows it.
173
+ * hello"world → "hello\"world"
174
+ * hello\world → hello\world (tier 2 caught it — unreachable here)
175
+ * hello\"world → "hello\\\"world"
176
+ * hello world\ → "hello world\\" */
177
+ *(target++) = L'"';
178
+ start = target;
179
+ quote_hit = 1;
180
+ for (i = len; i > 0; --i) {
181
+ *(target++) = source[i - 1];
182
+ if (quote_hit && source[i - 1] == L'\\') {
183
+ *(target++) = L'\\';
184
+ } else if (source[i - 1] == L'"') {
185
+ quote_hit = 1;
186
+ *(target++) = L'\\';
187
+ } else {
188
+ quote_hit = 0;
189
+ }
190
+ }
191
+ target[0] = L'\0';
192
+ _wcsrev(start);
193
+ *(target++) = L'"';
194
+ return target;
195
+ }
196
+
197
+ /* cmd + args → the full malloc'd command line (argv[0] is the command AS
198
+ * TYPED — CreateProcessW's lpApplicationName carries the resolved path,
199
+ * so the child's argv[0] stays the caller's spelling, like Node). */
200
+ static WCHAR *scr_child_cmdline(ScrStr *cmd, ScrArr *args) {
201
+ size_t n = (size_t)scr_arr_len(args);
202
+ size_t argc = n + 1;
203
+ WCHAR **wargs = malloc(argc * sizeof(WCHAR *));
204
+ if (!wargs) scr_child_oom();
205
+ wargs[0] = scr_child_wide(cmd->data, cmd->len);
206
+ for (size_t i = 0; i < n; i++) {
207
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
208
+ wargs[i + 1] = scr_child_wide(s->data, s->len);
209
+ scr_str_release(s);
210
+ }
211
+ size_t total = 1; /* the terminator */
212
+ for (size_t i = 0; i < argc; i++) total += wcslen(wargs[i]) * 2 + 3;
213
+ WCHAR *dst = malloc(total * sizeof(WCHAR));
214
+ if (!dst) scr_child_oom();
215
+ WCHAR *pos = dst;
216
+ for (size_t i = 0; i < argc; i++) {
217
+ pos = scr_quote_cmd_arg(wargs[i], pos);
218
+ *pos++ = i + 1 < argc ? L' ' : L'\0';
219
+ free(wargs[i]);
220
+ }
221
+ free(wargs);
222
+ return dst;
223
+ }
224
+
225
+ /* ── the environment block (libuv's make_program_env) ────────────────── */
226
+
227
+ /* Ordinal case-insensitive key comparison — libuv's env_strncmp: keys
228
+ * run to the first '='; na < 0 means "find it in a". */
229
+ static int scr_env_keycmp(const WCHAR *a, int na, const WCHAR *b) {
230
+ if (na < 0) {
231
+ const WCHAR *a_eq = wcschr(a, L'=');
232
+ na = a_eq != NULL ? (int)(a_eq - a) : (int)wcslen(a);
233
+ }
234
+ const WCHAR *b_eq = wcschr(b, L'=');
235
+ int nb = b_eq != NULL ? (int)(b_eq - b) : (int)wcslen(b);
236
+ return CompareStringOrdinal(a, na, b, nb, TRUE) - CSTR_EQUAL;
237
+ }
238
+
239
+ static int scr_env_qsortcmp(const void *a, const void *b) {
240
+ return scr_env_keycmp(*(WCHAR *const *)a, -1, *(WCHAR *const *)b);
241
+ }
242
+
243
+ /* libuv's required vars: winsock fails to initialize without SYSTEMROOT,
244
+ * PATH keeps executable search alive, TEMP feeds every temp-file API —
245
+ * a replaced env inherits these from the parent when it does not set
246
+ * them itself. Keys only (values fetched live). */
247
+ static const WCHAR *const scr_env_required[] = {
248
+ L"HOMEDRIVE", L"HOMEPATH", L"LOGONSERVER", L"PATH", L"SYSTEMDRIVE",
249
+ L"SYSTEMROOT", L"TEMP", L"USERDOMAIN", L"USERNAME", L"USERPROFILE",
250
+ };
251
+
252
+ /* [k,v,...] pairs → the sorted, double-NUL-terminated CreateProcessW
253
+ * block (CREATE_UNICODE_ENVIRONMENT), required vars merged in. */
254
+ static WCHAR *scr_child_env_block(ScrArr *pairs) {
255
+ size_t n = (size_t)scr_arr_len(pairs) / 2;
256
+ size_t nreq = sizeof scr_env_required / sizeof scr_env_required[0];
257
+ WCHAR **items = malloc((n + nreq) * sizeof(WCHAR *));
258
+ if (!items) scr_child_oom();
259
+ size_t count = 0;
260
+ for (size_t i = 0; i < n; i++) {
261
+ ScrStr *k = (ScrStr *)scr_arr_get_ref(pairs, (double)(2 * i));
262
+ ScrStr *v = (ScrStr *)scr_arr_get_ref(pairs, (double)(2 * i + 1));
263
+ WCHAR *kw = scr_child_wide(k->data, k->len);
264
+ WCHAR *vw = scr_child_wide(v->data, v->len);
265
+ size_t kl = wcslen(kw), vl = wcslen(vw);
266
+ WCHAR *kv = malloc((kl + 1 + vl + 1) * sizeof(WCHAR));
267
+ if (!kv) scr_child_oom();
268
+ wmemcpy(kv, kw, kl);
269
+ kv[kl] = L'=';
270
+ wmemcpy(kv + kl + 1, vw, vl + 1);
271
+ items[count++] = kv;
272
+ free(kw);
273
+ free(vw);
274
+ scr_str_release(k);
275
+ scr_str_release(v);
276
+ }
277
+ for (size_t r = 0; r < nreq; r++) {
278
+ bool present = false;
279
+ for (size_t i = 0; i < count && !present; i++) {
280
+ present = scr_env_keycmp(scr_env_required[r], (int)wcslen(scr_env_required[r]), items[i]) == 0;
281
+ }
282
+ if (present) continue;
283
+ DWORD need = GetEnvironmentVariableW(scr_env_required[r], NULL, 0);
284
+ if (need == 0) continue; /* the parent lacks it too */
285
+ size_t kl = wcslen(scr_env_required[r]);
286
+ WCHAR *kv = malloc((kl + 1 + (size_t)need) * sizeof(WCHAR));
287
+ if (!kv) scr_child_oom();
288
+ wmemcpy(kv, scr_env_required[r], kl);
289
+ kv[kl] = L'=';
290
+ (void)GetEnvironmentVariableW(scr_env_required[r], kv + kl + 1, need);
291
+ items[count++] = kv;
292
+ }
293
+ qsort(items, count, sizeof(WCHAR *), scr_env_qsortcmp);
294
+ size_t total = 1; /* the block's extra terminator */
295
+ for (size_t i = 0; i < count; i++) total += wcslen(items[i]) + 1;
296
+ WCHAR *block = malloc(total * sizeof(WCHAR));
297
+ if (!block) scr_child_oom();
298
+ WCHAR *at = block;
299
+ for (size_t i = 0; i < count; i++) {
300
+ size_t len = wcslen(items[i]) + 1;
301
+ wmemcpy(at, items[i], len);
302
+ at += len;
303
+ free(items[i]);
304
+ }
305
+ *at = L'\0';
306
+ free(items);
307
+ return block;
308
+ }
309
+
310
+ /* The child's PATH: its own block's entry when env was replaced (the
311
+ * required-vars merge already pulled the parent's in when unset), the
312
+ * parent's otherwise. malloc'd, NULL when absent everywhere. */
313
+ static WCHAR *scr_child_path(const WCHAR *env_block) {
314
+ if (env_block != NULL) {
315
+ for (const WCHAR *p = env_block; *p != L'\0'; p += wcslen(p) + 1) {
316
+ if ((p[0] == L'P' || p[0] == L'p') && (p[1] == L'A' || p[1] == L'a') &&
317
+ (p[2] == L'T' || p[2] == L't') && (p[3] == L'H' || p[3] == L'h') &&
318
+ p[4] == L'=') {
319
+ size_t len = wcslen(p + 5);
320
+ WCHAR *out = malloc((len + 1) * sizeof(WCHAR));
321
+ if (!out) scr_child_oom();
322
+ wmemcpy(out, p + 5, len + 1);
323
+ return out;
324
+ }
325
+ }
326
+ return NULL;
327
+ }
328
+ DWORD need = GetEnvironmentVariableW(L"PATH", NULL, 0);
329
+ if (need == 0) return NULL;
330
+ WCHAR *out = malloc((size_t)need * sizeof(WCHAR));
331
+ if (!out) scr_child_oom();
332
+ (void)GetEnvironmentVariableW(L"PATH", out, need);
333
+ return out;
334
+ }
335
+
336
+ /* ── libuv's search_path, ported ─────────────────────────────────────── */
337
+
338
+ /* Joins dir + name + ext against cwd (the drive-relative dance included)
339
+ * and answers the malloc'd path when a non-directory file exists there. */
340
+ static WCHAR *scr_spath_join_test(const WCHAR *dir, size_t dir_len,
341
+ const WCHAR *name, size_t name_len,
342
+ const WCHAR *ext, size_t ext_len,
343
+ const WCHAR *cwd, size_t cwd_len) {
344
+ if (dir_len > 2 && ((dir[0] == L'\\' || dir[0] == L'/') &&
345
+ (dir[1] == L'\\' || dir[1] == L'/'))) {
346
+ /* UNC path: ignore cwd. */
347
+ cwd_len = 0;
348
+ } else if (dir_len >= 1 && (dir[0] == L'/' || dir[0] == L'\\')) {
349
+ /* Full path without drive letter: use cwd's drive only. */
350
+ cwd_len = 2;
351
+ } else if (dir_len >= 2 && dir[1] == L':' &&
352
+ (dir_len < 3 || (dir[2] != L'/' && dir[2] != L'\\'))) {
353
+ /* Relative path with drive letter (D:../file): replace the drive
354
+ * letter with the full cwd when they agree, else the dir alone. */
355
+ if (cwd_len < 2 || _wcsnicmp(cwd, dir, 2) != 0) {
356
+ cwd_len = 0;
357
+ } else {
358
+ dir += 2;
359
+ dir_len -= 2;
360
+ }
361
+ } else if (dir_len > 2 && dir[1] == L':') {
362
+ /* Absolute path with drive letter: no cwd at all. */
363
+ cwd_len = 0;
364
+ }
365
+
366
+ WCHAR *result = malloc((cwd_len + 1 + dir_len + 1 + name_len + 1 + ext_len + 1) * sizeof(WCHAR));
367
+ if (!result) scr_child_oom();
368
+ WCHAR *pos = result;
369
+ wcsncpy(pos, cwd, cwd_len);
370
+ pos += cwd_len;
371
+ if (cwd_len && wcsrchr(L"\\/:", pos[-1]) == NULL) *pos++ = L'\\';
372
+ wcsncpy(pos, dir, dir_len);
373
+ pos += dir_len;
374
+ if (dir_len && wcsrchr(L"\\/:", pos[-1]) == NULL) *pos++ = L'\\';
375
+ wcsncpy(pos, name, name_len);
376
+ pos += name_len;
377
+ if (ext_len) {
378
+ if (name_len && pos[-1] != L'.') *pos++ = L'.';
379
+ wcsncpy(pos, ext, ext_len);
380
+ pos += ext_len;
381
+ }
382
+ *pos = L'\0';
383
+
384
+ DWORD attrs = GetFileAttributesW(result);
385
+ if (attrs != INVALID_FILE_ATTRIBUTES && !(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
386
+ return result;
387
+ }
388
+ free(result);
389
+ return NULL;
390
+ }
391
+
392
+ /* The literal name (only when it has an extension), then .com, then .exe
393
+ * — appended, never replacing (libuv follows msvcrt's spawn here). */
394
+ static WCHAR *scr_spath_walk_ext(const WCHAR *dir, size_t dir_len,
395
+ const WCHAR *name, size_t name_len,
396
+ const WCHAR *cwd, size_t cwd_len,
397
+ int name_has_ext) {
398
+ WCHAR *result;
399
+ if (name_has_ext) {
400
+ result = scr_spath_join_test(dir, dir_len, name, name_len, L"", 0, cwd, cwd_len);
401
+ if (result != NULL) return result;
402
+ }
403
+ result = scr_spath_join_test(dir, dir_len, name, name_len, L"com", 3, cwd, cwd_len);
404
+ if (result != NULL) return result;
405
+ return scr_spath_join_test(dir, dir_len, name, name_len, L"exe", 3, cwd, cwd_len);
406
+ }
407
+
408
+ /* file + cwd + PATH → the malloc'd absolute executable path, or NULL
409
+ * (Node's ENOENT). A file containing a separator never walks PATH; a
410
+ * bare name checks cwd first (CreateProcess's legacy order, kept by
411
+ * libuv behind NeedCurrentDirectoryForExePathW) then each PATH slice,
412
+ * quoted slices unwrapped, empty slices skipped. */
413
+ static WCHAR *scr_search_path(const WCHAR *file, const WCHAR *cwd, const WCHAR *path) {
414
+ size_t file_len = wcslen(file);
415
+ size_t cwd_len = wcslen(cwd);
416
+
417
+ if (file_len == 0 || (file_len == 1 && file[0] == L'.')) return NULL;
418
+
419
+ const WCHAR *file_name_start;
420
+ for (file_name_start = file + file_len;
421
+ file_name_start > file && file_name_start[-1] != L'\\' &&
422
+ file_name_start[-1] != L'/' && file_name_start[-1] != L':';
423
+ file_name_start--) {
424
+ }
425
+ int file_has_dir = file_name_start != file;
426
+ const WCHAR *dot = wcschr(file_name_start, L'.');
427
+ int name_has_ext = dot != NULL && dot[1] != L'\0';
428
+
429
+ if (file_has_dir) {
430
+ return scr_spath_walk_ext(file, (size_t)(file_name_start - file),
431
+ file_name_start, file_len - (size_t)(file_name_start - file),
432
+ cwd, cwd_len, name_has_ext);
433
+ }
434
+
435
+ WCHAR *result = NULL;
436
+ if (NeedCurrentDirectoryForExePathW(L"")) {
437
+ result = scr_spath_walk_ext(L"", 0, file, file_len, cwd, cwd_len, name_has_ext);
438
+ }
439
+ const WCHAR *dir_end = path;
440
+ while (result == NULL) {
441
+ if (dir_end == NULL || *dir_end == L'\0') break;
442
+ /* Skip the separator dir_end points at. */
443
+ if (dir_end != path || *path == L';') dir_end++;
444
+ const WCHAR *dir_start = dir_end;
445
+ if (*dir_start == L'"' || *dir_start == L'\'') {
446
+ dir_end = wcschr(dir_start + 1, *dir_start);
447
+ if (dir_end == NULL) dir_end = wcschr(dir_start, L'\0');
448
+ }
449
+ dir_end = wcschr(dir_end, L';');
450
+ if (dir_end == NULL) dir_end = wcschr(dir_start, L'\0');
451
+ if (dir_end - dir_start == 0) continue;
452
+ const WCHAR *dir_path = dir_start;
453
+ size_t dir_len = (size_t)(dir_end - dir_start);
454
+ if (dir_path[0] == L'"' || dir_path[0] == L'\'') {
455
+ ++dir_path;
456
+ --dir_len;
457
+ }
458
+ if (dir_len > 0 && (dir_path[dir_len - 1] == L'"' || dir_path[dir_len - 1] == L'\'')) {
459
+ --dir_len;
460
+ }
461
+ if (dir_len == 0) continue;
462
+ result = scr_spath_walk_ext(dir_path, dir_len, file, file_len, cwd, cwd_len, name_has_ext);
463
+ }
464
+ return result;
465
+ }
466
+
467
+ /* ── process creation (shared by the sync cores and spawn) ───────────── */
468
+
469
+ typedef struct {
470
+ /* in: the child's stdio handles (already inheritable; NULL = that
471
+ * slot stays whatever CreateProcess gives a detached-from-console
472
+ * child — callers always fill all three). */
473
+ HANDLE in_child, out_child, err_child;
474
+ const ScrStr *cwd; /* NULL: inherit */
475
+ ScrArr *env_pairs; /* NULL: inherit */
476
+ bool detached;
477
+ /* out */
478
+ HANDLE proc;
479
+ DWORD pid;
480
+ const char *errname; /* NULL = spawned */
481
+ } ScrWinSpawn;
482
+
483
+ static void scr_win_create_process(ScrStr *cmd, ScrArr *args, ScrWinSpawn *sp) {
484
+ sp->proc = NULL;
485
+ sp->pid = 0;
486
+ sp->errname = NULL;
487
+
488
+ WCHAR *cwd_w;
489
+ if (sp->cwd != NULL) {
490
+ cwd_w = scr_child_wide(sp->cwd->data, sp->cwd->len);
491
+ } else {
492
+ DWORD need = GetCurrentDirectoryW(0, NULL);
493
+ cwd_w = malloc((size_t)(need > 0 ? need : 1) * sizeof(WCHAR));
494
+ if (!cwd_w) scr_child_oom();
495
+ cwd_w[0] = L'\0';
496
+ if (need > 0) (void)GetCurrentDirectoryW(need, cwd_w);
497
+ }
498
+ WCHAR *env_block = sp->env_pairs != NULL ? scr_child_env_block(sp->env_pairs) : NULL;
499
+ WCHAR *path_w = scr_child_path(env_block);
500
+ WCHAR *file_w = scr_child_wide(cmd->data, cmd->len);
501
+ WCHAR *app = scr_search_path(file_w, cwd_w, path_w);
502
+ if (app == NULL) {
503
+ sp->errname = "ENOENT";
504
+ free(file_w);
505
+ free(path_w);
506
+ free(env_block);
507
+ free(cwd_w);
508
+ return;
509
+ }
510
+ WCHAR *cmdline = scr_child_cmdline(cmd, args);
511
+
512
+ STARTUPINFOW si;
513
+ memset(&si, 0, sizeof si);
514
+ si.cb = sizeof si;
515
+ si.dwFlags = STARTF_USESTDHANDLES;
516
+ si.hStdInput = sp->in_child;
517
+ si.hStdOutput = sp->out_child;
518
+ si.hStdError = sp->err_child;
519
+ DWORD flags = CREATE_UNICODE_ENVIRONMENT;
520
+ if (sp->detached) flags |= DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
521
+
522
+ PROCESS_INFORMATION pi;
523
+ if (!CreateProcessW(app, cmdline, NULL, NULL, TRUE, flags, env_block,
524
+ sp->cwd != NULL ? cwd_w : NULL, &si, &pi)) {
525
+ sp->errname = scr_win_errname(GetLastError());
526
+ } else {
527
+ CloseHandle(pi.hThread);
528
+ sp->proc = pi.hProcess;
529
+ sp->pid = pi.dwProcessId;
530
+ }
531
+ free(cmdline);
532
+ free(app);
533
+ free(file_w);
534
+ free(path_w);
535
+ free(env_block);
536
+ free(cwd_w);
537
+ }
538
+
539
+ /* Inheritable stdio helpers. Every handle these mint is CLOSED by the
540
+ * caller after CreateProcessW (the child holds its own copies). */
541
+ static HANDLE scr_win_nul(bool writable) {
542
+ SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
543
+ HANDLE h = CreateFileW(L"NUL", writable ? GENERIC_WRITE : GENERIC_READ,
544
+ FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, NULL);
545
+ return h == INVALID_HANDLE_VALUE ? NULL : h;
546
+ }
547
+
548
+ static HANDLE scr_win_dup_inherit(HANDLE src) {
549
+ if (src == NULL || src == INVALID_HANDLE_VALUE) return NULL;
550
+ HANDLE out = NULL;
551
+ if (!DuplicateHandle(GetCurrentProcess(), src, GetCurrentProcess(), &out, 0,
552
+ TRUE, DUPLICATE_SAME_ACCESS)) {
553
+ return NULL;
554
+ }
555
+ return out;
556
+ }
557
+
558
+ /* A capture/stream pipe: child's end inheritable, parent's not. False =
559
+ * pipe exhaustion (the caller degrades the slot to NUL). */
560
+ static bool scr_win_pipe(HANDLE *parent_end, HANDLE *child_end, bool child_writes) {
561
+ SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
562
+ HANDLE rd = NULL, wr = NULL;
563
+ if (!CreatePipe(&rd, &wr, &sa, 65536)) return false;
564
+ *parent_end = child_writes ? rd : wr;
565
+ *child_end = child_writes ? wr : rd;
566
+ (void)SetHandleInformation(*parent_end, HANDLE_FLAG_INHERIT, 0);
567
+ return true;
568
+ }
569
+
570
+ /* ── capture buffers (the POSIX arm's shape) ─────────────────────────── */
571
+
572
+ typedef struct {
573
+ char *data;
574
+ size_t len, cap;
575
+ } ScrCapBuf;
576
+
577
+ static void scr_cap_init(ScrCapBuf *b) {
578
+ b->cap = 4096;
579
+ b->len = 0;
580
+ b->data = malloc(b->cap);
581
+ if (!b->data) scr_child_oom();
582
+ }
583
+
584
+ /* One drain pass over a capture pipe: everything PeekNamedPipe reports,
585
+ * chunk by chunk. EOF (every writer closed — ERROR_BROKEN_PIPE) closes
586
+ * the handle and flips *open_. Returns true when anything changed. */
587
+ static bool scr_win_cap_pump(HANDLE h, ScrCapBuf *b, bool *open_) {
588
+ bool progress = false;
589
+ for (;;) {
590
+ DWORD avail = 0;
591
+ if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) {
592
+ CloseHandle(h);
593
+ *open_ = false;
594
+ return true;
595
+ }
596
+ if (avail == 0) return progress;
597
+ if (avail > 65536) avail = 65536;
598
+ if (b->len + avail > b->cap) {
599
+ while (b->len + avail > b->cap) b->cap *= 2;
600
+ b->data = realloc(b->data, b->cap);
601
+ if (!b->data) scr_child_oom();
602
+ }
603
+ DWORD got = 0;
604
+ if (!ReadFile(h, b->data + b->len, avail, &got, NULL) || got == 0) {
605
+ CloseHandle(h);
606
+ *open_ = false;
607
+ return true;
608
+ }
609
+ b->len += got;
610
+ progress = true;
611
+ }
612
+ }
613
+
614
+ /* ── the synchronous run core ────────────────────────────────────────── */
615
+
616
+ typedef struct {
617
+ const char *spawn_errname; /* NULL = the child ran (and was reaped) */
618
+ bool timed_out; /* the deadline TerminateProcess fired */
619
+ DWORD exit_code;
620
+ ScrCapBuf out, err; /* always initialized; caller frees */
621
+ } ScrWinSyncRes;
622
+
623
+ /* in_mode: 0 = NUL (Node's no-input default; an `input` string swaps in
624
+ * a fed pipe), 2 = inherit. out/err_mode: 0 = capture, 1 = NUL, 2 =
625
+ * inherit. timeout_ms > 0 arms the deadline: TerminateProcess(h, 1) —
626
+ * libuv's kill emulation; the caller reports the killSignal's name. */
627
+ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input,
628
+ int in_mode, int out_mode, int err_mode,
629
+ const ScrStr *cwd, ScrArr *env_pairs,
630
+ double timeout_ms, ScrWinSyncRes *res) {
631
+ res->spawn_errname = NULL;
632
+ res->timed_out = false;
633
+ res->exit_code = 0;
634
+ scr_cap_init(&res->out);
635
+ scr_cap_init(&res->err);
636
+
637
+ if (in_mode == 2 || out_mode == 2 || err_mode == 2) {
638
+ /* Inherited stdio: flush the parent's buffers first so earlier logs
639
+ * precede the child's writes (Node's practical ordering). */
640
+ fflush(stdout);
641
+ fflush(stderr);
642
+ }
643
+
644
+ HANDLE in_child = NULL, in_parent = NULL;
645
+ HANDLE out_child = NULL, out_parent = NULL;
646
+ HANDLE err_child = NULL, err_parent = NULL;
647
+ if (input != NULL) {
648
+ if (scr_win_pipe(&in_parent, &in_child, false)) {
649
+ DWORD mode = PIPE_NOWAIT;
650
+ (void)SetNamedPipeHandleState(in_parent, &mode, NULL, NULL);
651
+ } else {
652
+ in_child = scr_win_nul(false);
653
+ }
654
+ } else if (in_mode == 2) {
655
+ in_child = scr_win_dup_inherit(GetStdHandle(STD_INPUT_HANDLE));
656
+ if (in_child == NULL) in_child = scr_win_nul(false);
657
+ } else {
658
+ in_child = scr_win_nul(false);
659
+ }
660
+ if (out_mode == 0) {
661
+ if (!scr_win_pipe(&out_parent, &out_child, true)) out_child = scr_win_nul(true);
662
+ } else if (out_mode == 2) {
663
+ out_child = scr_win_dup_inherit(GetStdHandle(STD_OUTPUT_HANDLE));
664
+ if (out_child == NULL) out_child = scr_win_nul(true);
665
+ } else {
666
+ out_child = scr_win_nul(true);
667
+ }
668
+ if (err_mode == 0) {
669
+ if (!scr_win_pipe(&err_parent, &err_child, true)) err_child = scr_win_nul(true);
670
+ } else if (err_mode == 2) {
671
+ err_child = scr_win_dup_inherit(GetStdHandle(STD_ERROR_HANDLE));
672
+ if (err_child == NULL) err_child = scr_win_nul(true);
673
+ } else {
674
+ err_child = scr_win_nul(true);
675
+ }
676
+
677
+ ScrWinSpawn sp = {
678
+ .in_child = in_child,
679
+ .out_child = out_child,
680
+ .err_child = err_child,
681
+ .cwd = cwd,
682
+ .env_pairs = env_pairs,
683
+ .detached = false,
684
+ };
685
+ scr_win_create_process(cmd, args, &sp);
686
+ if (in_child != NULL) CloseHandle(in_child);
687
+ if (out_child != NULL) CloseHandle(out_child);
688
+ if (err_child != NULL) CloseHandle(err_child);
689
+ if (sp.errname != NULL) {
690
+ if (in_parent != NULL) CloseHandle(in_parent);
691
+ if (out_parent != NULL) CloseHandle(out_parent);
692
+ if (err_parent != NULL) CloseHandle(err_parent);
693
+ res->spawn_errname = sp.errname;
694
+ return;
695
+ }
696
+
697
+ /* Pump: drain both captures, feed stdin (PIPE_NOWAIT — a full pipe
698
+ * writes short, never blocks), watch the deadline. The 1ms wait rides
699
+ * the process handle so an exit ends the sleep immediately; pipes keep
700
+ * draining after exit until every writer's copy closes (a grandchild
701
+ * may hold one — the POSIX arm's discipline). */
702
+ bool out_open = out_parent != NULL, err_open = err_parent != NULL;
703
+ bool in_open = in_parent != NULL;
704
+ size_t in_at = 0;
705
+ bool exited = false;
706
+ double deadline = timeout_ms > 0 ? scr_now_ms() + timeout_ms : 0;
707
+ for (;;) {
708
+ bool progress = false;
709
+ if (out_open) progress |= scr_win_cap_pump(out_parent, &res->out, &out_open);
710
+ if (err_open) progress |= scr_win_cap_pump(err_parent, &res->err, &err_open);
711
+ if (in_open) {
712
+ /* Small chunks: PIPE_NOWAIT's partial-write behavior is only
713
+ * dependable for writes no larger than the pipe buffer. */
714
+ DWORD want = input->len - in_at > 2048 ? 2048 : (DWORD)(input->len - in_at);
715
+ DWORD wrote = 0;
716
+ if (want == 0) {
717
+ CloseHandle(in_parent);
718
+ in_open = false;
719
+ } else if (WriteFile(in_parent, input->data + in_at, want, &wrote, NULL)) {
720
+ if (wrote > 0) {
721
+ in_at += wrote;
722
+ progress = true;
723
+ }
724
+ if (in_at >= input->len) {
725
+ CloseHandle(in_parent);
726
+ in_open = false;
727
+ }
728
+ } else {
729
+ /* The child closed stdin early: fine, like Node. */
730
+ CloseHandle(in_parent);
731
+ in_open = false;
732
+ }
733
+ }
734
+ if (deadline > 0 && !res->timed_out && scr_now_ms() >= deadline) {
735
+ res->timed_out = true;
736
+ (void)TerminateProcess(sp.proc, 1); /* killed children exit 1 (uv) */
737
+ }
738
+ if (!exited) {
739
+ exited = WaitForSingleObject(sp.proc, progress ? 0 : 1) == WAIT_OBJECT_0;
740
+ } else if (!progress) {
741
+ Sleep(1);
742
+ }
743
+ if (exited && !out_open && !err_open) break;
744
+ }
745
+ if (in_open) CloseHandle(in_parent);
746
+ (void)GetExitCodeProcess(sp.proc, &res->exit_code);
747
+ CloseHandle(sp.proc);
748
+ }
749
+
750
+ /* ── the spawnSync result value (the POSIX arm's container) ──────────── */
751
+
752
+ struct ScrSpawnRes {
753
+ size_t rc;
754
+ bool has_status;
755
+ double status;
756
+ ScrStr *out;
757
+ ScrStr *err;
758
+ const char *spawn_errname;
759
+ ScrStr *cmd;
760
+ const char *signal_name;
761
+ };
762
+
763
+ ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r) {
764
+ if (r->rc != SIZE_MAX) r->rc++;
765
+ return r;
766
+ }
767
+
768
+ void scr_spawn_res_release(ScrSpawnRes *r) {
769
+ if (!r || r->rc == SIZE_MAX) return;
770
+ if (--r->rc == 0) {
771
+ scr_str_release(r->out);
772
+ scr_str_release(r->err);
773
+ scr_str_release(r->cmd); /* NULL-safe */
774
+ free(r);
775
+ }
776
+ }
777
+
778
+ void *scr_spawn_res_retain_v(void *p) { return scr_spawn_res_retain((ScrSpawnRes *)p); }
779
+ void scr_spawn_res_release_v(void *p) { scr_spawn_res_release((ScrSpawnRes *)p); }
780
+
781
+ bool scr_spawn_res_has_status(ScrSpawnRes *r) { return r->has_status; }
782
+ double scr_spawn_res_status(ScrSpawnRes *r) { return r->status; }
783
+ ScrStr *scr_spawn_res_stdout(ScrSpawnRes *r) { return scr_str_retain(r->out); }
784
+ ScrStr *scr_spawn_res_stderr(ScrSpawnRes *r) { return scr_str_retain(r->err); }
785
+ bool scr_spawn_res_has_signal(ScrSpawnRes *r) { return r->signal_name != NULL; }
786
+ ScrStr *scr_spawn_res_signal(ScrSpawnRes *r) {
787
+ return scr_str_new(r->signal_name, strlen(r->signal_name));
788
+ }
789
+
790
+ ScrError *scr_spawn_res_error(ScrSpawnRes *r) {
791
+ if (r->spawn_errname == NULL) return NULL;
792
+ size_t cap = 10 + (r->cmd ? r->cmd->len : 0) + 1 + strlen(r->spawn_errname) + 1;
793
+ char *msg = malloc(cap);
794
+ if (!msg) scr_child_oom();
795
+ snprintf(msg, cap, "spawnSync %s %s", r->cmd ? r->cmd->data : "", r->spawn_errname);
796
+ ScrStr *m = scr_str_new(msg, strlen(msg));
797
+ free(msg);
798
+ ScrError *e = scr_error_new(0 /* Error */, m);
799
+ scr_str_release(m);
800
+ scr_error_set_code(e, r->spawn_errname);
801
+ return e;
802
+ }
803
+
804
+ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status,
805
+ ScrStr *out /*moved*/, ScrStr *err /*moved*/) {
806
+ ScrSpawnRes *r = malloc(sizeof(ScrSpawnRes));
807
+ if (!r) scr_child_oom();
808
+ r->rc = 1;
809
+ r->has_status = has_status;
810
+ r->status = status;
811
+ r->out = out;
812
+ r->err = err;
813
+ r->spawn_errname = NULL;
814
+ r->cmd = NULL;
815
+ r->signal_name = NULL;
816
+ return r;
817
+ }
818
+
819
+ /* Kept for the header's contract (the POSIX arms share it); no Windows
820
+ * caller — CreateProcessW takes the quoted command LINE instead. */
821
+ char **scr_child_argv(ScrStr *cmd, ScrArr *args) {
822
+ size_t n = (size_t)scr_arr_len(args);
823
+ char **argv = malloc((n + 2) * sizeof(char *));
824
+ if (!argv) scr_child_oom();
825
+ argv[0] = cmd->data;
826
+ for (size_t i = 0; i < n; i++) {
827
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
828
+ argv[i + 1] = s->data;
829
+ scr_str_release(s); /* borrow: the array's reference keeps it alive */
830
+ }
831
+ argv[n + 1] = NULL;
832
+ return argv;
833
+ }
834
+
835
+ /* ── spawnSync ───────────────────────────────────────────────────────── */
836
+
837
+ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms,
838
+ int killsig, int in_mode, int out_mode,
839
+ int err_mode) {
840
+ ScrWinSyncRes w;
841
+ scr_win_run_sync(cmd, args, NULL, in_mode, out_mode, err_mode, NULL, NULL,
842
+ timeout_ms, &w);
843
+ if (w.spawn_errname != NULL) {
844
+ free(w.out.data);
845
+ free(w.err.data);
846
+ ScrSpawnRes *r = scr_spawn_res_new(false, 0, scr_str_new("", 0), scr_str_new("", 0));
847
+ r->spawn_errname = w.spawn_errname;
848
+ r->cmd = scr_str_retain(cmd);
849
+ return r;
850
+ }
851
+ ScrStr *out_s = scr_str_new(w.out.data, w.out.len);
852
+ ScrStr *err_s = scr_str_new(w.err.data, w.err.len);
853
+ free(w.out.data);
854
+ free(w.err.data);
855
+ if (!w.timed_out) {
856
+ return scr_spawn_res_new(true, (double)w.exit_code, out_s, err_s);
857
+ }
858
+ /* Node's timeout shape: status null, signal = killSignal's name, error
859
+ * ETIMEDOUT (the kill may have raced a normal exit; Node reports the
860
+ * timeout either way). */
861
+ ScrSpawnRes *r = scr_spawn_res_new(false, 0, out_s, err_s);
862
+ r->signal_name = scr_signal_name(killsig);
863
+ if (r->signal_name == NULL) r->signal_name = "SIGTERM";
864
+ r->spawn_errname = "ETIMEDOUT";
865
+ r->cmd = scr_str_retain(cmd);
866
+ return r;
867
+ }
868
+
869
+ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) {
870
+ return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0);
871
+ }
872
+
873
+ ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
874
+ ScrStr *killsignal, double in_mode,
875
+ double out_mode, double err_mode) {
876
+ int killsig = SIGTERM;
877
+ if (killsignal->len > 0) {
878
+ int resolved = scr_signal_from_name(killsignal);
879
+ if (resolved > 0) killsig = resolved;
880
+ }
881
+ return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode,
882
+ (int)out_mode, (int)err_mode);
883
+ }
884
+
885
+ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms,
886
+ ScrStr *killsignal, ScrStr *stdio) {
887
+ double in_mode = 0, out_mode = 0, err_mode = 0; /* "pipe": the defaults */
888
+ if (stdio->len == 7 && memcmp(stdio->data, "inherit", 7) == 0) {
889
+ in_mode = 2;
890
+ out_mode = 2;
891
+ err_mode = 2;
892
+ } else if (stdio->len == 6 && memcmp(stdio->data, "ignore", 6) == 0) {
893
+ out_mode = 1;
894
+ err_mode = 1;
895
+ }
896
+ return scr_spawn_sync_opts(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode);
897
+ }
898
+
899
+ /* ── execFileSync / execSync (the POSIX arm's message contracts) ─────── */
900
+
901
+ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) {
902
+ if (shell) {
903
+ ScrStr *c = (ScrStr *)scr_arr_get_ref(args, 1); /* ["-c", cmd] */
904
+ char *out = malloc(c->len + 1);
905
+ if (!out) scr_child_oom();
906
+ memcpy(out, c->data, c->len + 1);
907
+ scr_str_release(c);
908
+ return out;
909
+ }
910
+ size_t n = (size_t)scr_arr_len(args);
911
+ size_t total = cmd->len;
912
+ for (size_t i = 0; i < n; i++) {
913
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
914
+ total += 1 + s->len;
915
+ scr_str_release(s);
916
+ }
917
+ char *out = malloc(total + 1);
918
+ if (!out) scr_child_oom();
919
+ memcpy(out, cmd->data, cmd->len);
920
+ size_t at = cmd->len;
921
+ for (size_t i = 0; i < n; i++) {
922
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
923
+ out[at++] = ' ';
924
+ memcpy(out + at, s->data, s->len);
925
+ at += s->len;
926
+ scr_str_release(s);
927
+ }
928
+ out[at] = '\0';
929
+ return out;
930
+ }
931
+
932
+ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
933
+ bool with_stderr, bool always_nl) {
934
+ size_t dlen = strlen(display);
935
+ size_t elen = with_stderr ? err_cap->len : 0;
936
+ size_t cap = 16 + dlen + 1 + elen + 1;
937
+ char *msg = malloc(cap);
938
+ if (!msg) scr_child_oom();
939
+ size_t at = (size_t)snprintf(msg, cap, "Command failed: %s", display);
940
+ if (elen > 0 || always_nl) {
941
+ msg[at++] = '\n';
942
+ memcpy(msg + at, err_cap->data, elen);
943
+ at += elen;
944
+ }
945
+ scr_throw_error_msg(SCR_ERR_ERROR, msg, at);
946
+ free(msg);
947
+ }
948
+
949
+ static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) {
950
+ size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1;
951
+ char *msg = malloc(cap);
952
+ if (!msg) scr_child_oom();
953
+ int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync",
954
+ cmd->data, errname);
955
+ scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname);
956
+ free(msg);
957
+ }
958
+
959
+ /* The shared exec body (sync + promisified shapes — the POSIX arm's
960
+ * scr_exec_sync_core semantics over the win run core). */
961
+ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell,
962
+ const ScrStr *input, const ScrStr *cwd,
963
+ ScrArr *env_pairs, double timeout_ms,
964
+ int stdout_mode, int stderr_mode, bool async_shape,
965
+ ScrStr **stderr_out) {
966
+ /* stdio "inherit"'s stdin rides bit 4 of stdout_mode (scr_runtime.h). */
967
+ bool stdin_inherit = (stdout_mode & 4) != 0;
968
+ stdout_mode &= 3;
969
+ bool cap_out = stdout_mode == 1;
970
+ bool cap_err = stderr_mode == 0 || stderr_mode == 1;
971
+ ScrWinSyncRes w;
972
+ scr_win_run_sync(cmd, args, input, stdin_inherit ? 2 : 0,
973
+ cap_out ? 0 : (stdout_mode == 2 ? 2 : 1),
974
+ cap_err ? 0 : (stderr_mode == 3 ? 2 : 1), cwd,
975
+ env_pairs, timeout_ms, &w);
976
+ if (w.spawn_errname != NULL) {
977
+ free(w.out.data);
978
+ free(w.err.data);
979
+ scr_exec_throw_spawn(cmd, w.spawn_errname, async_shape);
980
+ return NULL;
981
+ }
982
+ if (w.timed_out && !async_shape) {
983
+ /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape
984
+ * reports the kill's exit-1 death as an ordinary command failure
985
+ * below, exactly Node's promisified execFile. */
986
+ free(w.out.data);
987
+ free(w.err.data);
988
+ scr_exec_throw_spawn(cmd, "ETIMEDOUT", false);
989
+ return NULL;
990
+ }
991
+ bool failed = w.exit_code != 0;
992
+ /* Node's inheritStderr: no stdio option given → the captured stderr
993
+ * echoes to the parent's stderr AFTER completion, success or failure. */
994
+ if (stderr_mode == 0 && w.err.len > 0) {
995
+ fflush(stdout);
996
+ fwrite(w.err.data, 1, w.err.len, stderr);
997
+ }
998
+ if (failed) {
999
+ char *display = scr_exec_display(cmd, args, shell);
1000
+ scr_exec_throw_failed(display, &w.err, cap_err, async_shape);
1001
+ free(display);
1002
+ free(w.out.data);
1003
+ free(w.err.data);
1004
+ return NULL;
1005
+ }
1006
+ if (async_shape && stderr_out != NULL) {
1007
+ *stderr_out = scr_str_new(w.err.data, w.err.len);
1008
+ }
1009
+ ScrStr *out_s = scr_str_new(w.out.data, w.out.len);
1010
+ free(w.out.data);
1011
+ free(w.err.data);
1012
+ return out_s;
1013
+ }
1014
+
1015
+ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
1016
+ bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
1017
+ double timeout_ms, double stdout_mode, double stderr_mode) {
1018
+ return scr_exec_run(cmd, args, shell, has_input ? input : NULL,
1019
+ cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL,
1020
+ timeout_ms, (int)stdout_mode, (int)stderr_mode, false, NULL);
1021
+ }
1022
+
1023
+ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, ScrStr *cwd,
1024
+ bool has_env, ScrArr *env_pairs, double timeout_ms) {
1025
+ ScrStr *err_s = NULL;
1026
+ ScrStr *out_s = scr_exec_run(cmd, args, false, NULL, cwd->len > 0 ? cwd : NULL,
1027
+ has_env ? env_pairs : NULL, timeout_ms, 1, 1,
1028
+ true, &err_s);
1029
+ if (out_s == NULL) return NULL; /* exception pending; dummy result */
1030
+ return scr_spawn_res_new(true, 0, out_s, err_s);
1031
+ }
1032
+
1033
+ /* ── spawn: the asynchronous child + its event registry ──────────────────
1034
+ * The POSIX arm's design (see its block comment below), re-plumbed:
1035
+ * waitpid(WNOHANG) → WaitForSingleObject(h, 0) + GetExitCodeProcess,
1036
+ * kqueue/pidfd wakeups → WaitForMultipleObjects over process handles in
1037
+ * scr_children_wait, piped streams → PeekNamedPipe pumps under the
1038
+ * loop's ~1ms polling cap (anonymous-pipe readability has no waitable
1039
+ * handle, so consumer-owning streams decline the wait — the unwatched
1040
+ * fallback). Listener ordering, settle discipline, unref/teardown, and
1041
+ * the unhandled-'error' exit are copied unchanged. */
1042
+
1043
+ typedef enum {
1044
+ SCR_CHILD_RUNNING = 0,
1045
+ SCR_CHILD_EXITED = 1,
1046
+ SCR_CHILD_SPAWN_FAILED = 2
1047
+ } ScrChildState;
1048
+
1049
+ typedef struct {
1050
+ ScrClosure *cb;
1051
+ ScrChildExitFn fn;
1052
+ } ScrChildExitEntry;
1053
+
1054
+ typedef struct {
1055
+ ScrClosure *cb;
1056
+ ScrChildErrFn fn;
1057
+ } ScrChildErrEntry;
1058
+
1059
+ typedef struct {
1060
+ ScrClosure *cb; /* owned */
1061
+ ScrChildStreamDataFn fn;
1062
+ bool once;
1063
+ } ScrChildStreamDataL;
1064
+
1065
+ typedef struct {
1066
+ ScrClosure *cb; /* owned */
1067
+ bool once;
1068
+ } ScrChildStreamEndL;
1069
+
1070
+ struct ScrChildStream {
1071
+ size_t rc;
1072
+ HANDLE h; /* the pipe's read end; NULL after EOF or spawn failure */
1073
+ bool eof;
1074
+ ScrChildStreamDataL *data_ls;
1075
+ size_t n_data, cap_data;
1076
+ ScrChildStreamEndL *end_ls;
1077
+ size_t n_end, cap_end;
1078
+ struct ScrChildStream *next; /* the service registry (+1) */
1079
+ };
1080
+
1081
+ static ScrChildStream *scr_child_streams = NULL;
1082
+ static size_t scr_child_streams_watching = 0;
1083
+
1084
+ struct ScrChild {
1085
+ size_t rc;
1086
+ HANDLE proc; /* NULL after the reap (or spawn failure) */
1087
+ DWORD pid;
1088
+ ScrChildState state;
1089
+ bool settled;
1090
+ bool has_code;
1091
+ bool killed;
1092
+ bool reffed;
1093
+ int spawn_uv_errno; /* spawn failure: Node's exitCode (negative) */
1094
+ const char *spawn_errname; /* spawn failure: the `code` stamp */
1095
+ double code;
1096
+ const char *exit_signal; /* we killed it: the signal's name (static) */
1097
+ const char *kill_signal; /* recorded when kill() terminates the child */
1098
+ ScrStr *err_msg; /* spawn failure only: "spawn <cmd> <ERRNAME>" */
1099
+ ScrChildExitEntry *exit_cbs;
1100
+ size_t n_exit, cap_exit;
1101
+ ScrChildErrEntry *err_cbs;
1102
+ size_t n_err, cap_err;
1103
+ ScrChildStream *out_stream;
1104
+ ScrChildStream *err_stream;
1105
+ struct ScrChild *next; /* the pending registry */
1106
+ };
1107
+
1108
+ static ScrChild *scr_children = NULL;
1109
+ static size_t scr_children_reffed_n = 0;
1110
+ /* Pending children the handle wait can NOT represent — spawn failures
1111
+ * awaiting their first-pass settle. While any exist, scr_children_wait
1112
+ * declines and the loop keeps the ~1ms polling cap (the POSIX arm's
1113
+ * unwatched fallback, exactly). */
1114
+ static size_t scr_children_unwatched = 0;
1115
+
1116
+ static void scr_child_drop_listeners(ScrChild *c) {
1117
+ for (size_t i = 0; i < c->n_exit; i++) scr_closure_release(c->exit_cbs[i].cb);
1118
+ for (size_t i = 0; i < c->n_err; i++) scr_closure_release(c->err_cbs[i].cb);
1119
+ free(c->exit_cbs);
1120
+ free(c->err_cbs);
1121
+ c->exit_cbs = NULL;
1122
+ c->err_cbs = NULL;
1123
+ c->n_exit = c->n_err = c->cap_exit = c->cap_err = 0;
1124
+ }
1125
+
1126
+ ScrChild *scr_child_retain(ScrChild *c) {
1127
+ if (c->rc != SIZE_MAX) c->rc++;
1128
+ return c;
1129
+ }
1130
+
1131
+ void scr_child_release(ScrChild *c) {
1132
+ if (!c || c->rc == SIZE_MAX) return;
1133
+ if (--c->rc == 0) {
1134
+ scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */
1135
+ scr_str_release(c->err_msg);
1136
+ scr_child_stream_release(c->out_stream);
1137
+ scr_child_stream_release(c->err_stream);
1138
+ if (c->proc != NULL) CloseHandle(c->proc);
1139
+ free(c);
1140
+ }
1141
+ }
1142
+
1143
+ void *scr_child_retain_v(void *p) { return scr_child_retain((ScrChild *)p); }
1144
+ void scr_child_release_v(void *p) { scr_child_release((ScrChild *)p); }
1145
+
1146
+ /* ── the stream slice (PeekNamedPipe pumps) ──────────────────────────── */
1147
+
1148
+ ScrChildStream *scr_child_stream_retain(ScrChildStream *s) {
1149
+ if (s->rc != SIZE_MAX) s->rc++;
1150
+ return s;
1151
+ }
1152
+
1153
+ static void scr_child_stream_drop_listeners(ScrChildStream *s) {
1154
+ for (size_t i = 0; i < s->n_data; i++) scr_closure_release(s->data_ls[i].cb);
1155
+ for (size_t i = 0; i < s->n_end; i++) scr_closure_release(s->end_ls[i].cb);
1156
+ free(s->data_ls);
1157
+ free(s->end_ls);
1158
+ s->data_ls = NULL;
1159
+ s->end_ls = NULL;
1160
+ s->n_data = s->n_end = s->cap_data = s->cap_end = 0;
1161
+ }
1162
+
1163
+ void scr_child_stream_release(ScrChildStream *s) {
1164
+ if (!s || s->rc == SIZE_MAX) return;
1165
+ if (--s->rc == 0) {
1166
+ scr_child_stream_drop_listeners(s); /* only reachable pre-'end' via leaks */
1167
+ if (s->h != NULL) CloseHandle(s->h);
1168
+ free(s);
1169
+ }
1170
+ }
1171
+
1172
+ void *scr_child_stream_retain_v(void *p) { return scr_child_stream_retain((ScrChildStream *)p); }
1173
+ void scr_child_stream_release_v(void *p) { scr_child_stream_release((ScrChildStream *)p); }
1174
+
1175
+ /* True while the stream has a consumer and can still deliver. */
1176
+ static bool scr_child_stream_watching(const ScrChildStream *s) {
1177
+ return !s->eof && s->h != NULL && s->n_data > 0;
1178
+ }
1179
+
1180
+ /* A fresh piped stream over the pipe's read end, registered with the
1181
+ * service registry (+1). NULL = the never-opened husk a degraded pipe
1182
+ * slot hands out (eof from birth, never registered). */
1183
+ static ScrChildStream *scr_child_stream_new(HANDLE h) {
1184
+ ScrChildStream *s = calloc(1, sizeof *s);
1185
+ if (!s) scr_child_oom();
1186
+ s->rc = 1;
1187
+ s->h = h;
1188
+ if (h == NULL) {
1189
+ s->eof = true;
1190
+ return s;
1191
+ }
1192
+ s->next = scr_child_streams;
1193
+ scr_child_streams = scr_child_stream_retain(s);
1194
+ return s;
1195
+ }
1196
+
1197
+ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) {
1198
+ if (scr_child_stream_watching(s)) scr_child_streams_watching--;
1199
+ s->eof = true;
1200
+ if (s->h != NULL) {
1201
+ CloseHandle(s->h);
1202
+ s->h = NULL;
1203
+ }
1204
+ if (fire_end) {
1205
+ size_t n = s->n_end;
1206
+ ScrChildStreamEndL *snap = malloc(n * sizeof *snap);
1207
+ if (!snap) scr_child_oom();
1208
+ for (size_t i = 0; i < n; i++) {
1209
+ snap[i] = s->end_ls[i];
1210
+ scr_closure_retain(snap[i].cb);
1211
+ }
1212
+ for (size_t i = 0; i < n; i++) {
1213
+ if (!scr_exc_pending()) {
1214
+ ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
1215
+ }
1216
+ scr_closure_release(snap[i].cb);
1217
+ }
1218
+ free(snap);
1219
+ }
1220
+ scr_child_stream_drop_listeners(s);
1221
+ }
1222
+
1223
+ /* One read pump: while a consumer exists, PeekNamedPipe/ReadFile to
1224
+ * empty (one 'data' per ≤64KB chunk, the libuv pipe-read size) or EOF
1225
+ * (broken pipe: every writer closed). Returns true at EOF: the caller
1226
+ * finishes and unlinks. */
1227
+ static bool scr_child_stream_pump(ScrChildStream *s) {
1228
+ while (scr_child_stream_watching(s)) {
1229
+ DWORD avail = 0;
1230
+ if (!PeekNamedPipe(s->h, NULL, 0, NULL, &avail, NULL)) return true;
1231
+ if (avail == 0) return false;
1232
+ if (avail > 65536) avail = 65536;
1233
+ char buf[65536];
1234
+ DWORD got = 0;
1235
+ if (!ReadFile(s->h, buf, avail, &got, NULL) || got == 0) return true;
1236
+ ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)got);
1237
+ memcpy(chunk->data, buf, (size_t)got);
1238
+ size_t nd = s->n_data;
1239
+ ScrChildStreamDataL *snap = malloc(nd * sizeof *snap);
1240
+ if (!snap) scr_child_oom();
1241
+ for (size_t i = 0; i < nd; i++) {
1242
+ snap[i] = s->data_ls[i];
1243
+ scr_closure_retain(snap[i].cb);
1244
+ }
1245
+ for (size_t i = 0; i < nd; i++) {
1246
+ if (snap[i].once) {
1247
+ for (size_t j = 0; j < s->n_data; j++) {
1248
+ if (s->data_ls[j].cb == snap[i].cb) {
1249
+ scr_closure_release(s->data_ls[j].cb);
1250
+ memmove(s->data_ls + j, s->data_ls + j + 1,
1251
+ (s->n_data - j - 1) * sizeof *s->data_ls);
1252
+ s->n_data--;
1253
+ if (s->n_data == 0) scr_child_streams_watching--;
1254
+ break;
1255
+ }
1256
+ }
1257
+ }
1258
+ if (!scr_exc_pending()) snap[i].fn(snap[i].cb, chunk);
1259
+ scr_closure_release(snap[i].cb);
1260
+ }
1261
+ free(snap);
1262
+ scr_bytes_release(chunk);
1263
+ if (scr_exc_pending()) return false;
1264
+ }
1265
+ return false;
1266
+ }
1267
+
1268
+ static void scr_child_streams_service(void) {
1269
+ ScrChildStream **link = &scr_child_streams;
1270
+ while (*link) {
1271
+ ScrChildStream *s = *link;
1272
+ bool ended = scr_child_stream_pump(s);
1273
+ if (ended) {
1274
+ *link = s->next;
1275
+ s->next = NULL;
1276
+ scr_child_stream_finish(s, true);
1277
+ scr_child_stream_release(s); /* the registry's reference */
1278
+ } else {
1279
+ link = &s->next;
1280
+ }
1281
+ if (scr_exc_pending()) return;
1282
+ }
1283
+ }
1284
+
1285
+ /* The settle-path drain (Node's pinned ordering: stream 'end' before
1286
+ * 'exit'): pump one stream to EOF/empty right now; on EOF, finish and
1287
+ * unlink. Data still arriving with the child reaped means a grandchild
1288
+ * holds the write end — 'exit' proceeds and the stream keeps delivering
1289
+ * on later turns, exactly Node. */
1290
+ static void scr_child_stream_drain_now(ScrChildStream *s) {
1291
+ if (s == NULL || !scr_child_stream_watching(s)) return;
1292
+ if (!scr_child_stream_pump(s)) return;
1293
+ ScrChildStream **link = &scr_child_streams;
1294
+ while (*link && *link != s) link = &(*link)->next;
1295
+ if (*link) {
1296
+ *link = s->next;
1297
+ s->next = NULL;
1298
+ scr_child_stream_finish(s, true);
1299
+ scr_child_stream_release(s);
1300
+ } else {
1301
+ scr_child_stream_finish(s, true); /* defensive: not registered */
1302
+ }
1303
+ }
1304
+
1305
+ void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
1306
+ ScrChildStreamDataFn fn, bool once) {
1307
+ if (s->eof || s->h == NULL) {
1308
+ scr_closure_release(cb);
1309
+ return;
1310
+ }
1311
+ if (s->n_data == s->cap_data) {
1312
+ s->cap_data = s->cap_data ? s->cap_data * 2 : 2;
1313
+ s->data_ls = realloc(s->data_ls, s->cap_data * sizeof *s->data_ls);
1314
+ if (!s->data_ls) scr_child_oom();
1315
+ }
1316
+ s->data_ls[s->n_data].cb = cb;
1317
+ s->data_ls[s->n_data].fn = fn;
1318
+ s->data_ls[s->n_data].once = once;
1319
+ s->n_data++;
1320
+ if (s->n_data == 1) scr_child_streams_watching++;
1321
+ }
1322
+
1323
+ void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool once) {
1324
+ (void)once; /* 'end' fires at most once; the flag changes nothing */
1325
+ if (s->eof || s->h == NULL) {
1326
+ scr_closure_release(cb);
1327
+ return;
1328
+ }
1329
+ if (s->n_end == s->cap_end) {
1330
+ s->cap_end = s->cap_end ? s->cap_end * 2 : 2;
1331
+ s->end_ls = realloc(s->end_ls, s->cap_end * sizeof *s->end_ls);
1332
+ if (!s->end_ls) scr_child_oom();
1333
+ }
1334
+ s->end_ls[s->n_end].cb = cb;
1335
+ s->end_ls[s->n_end].once = once;
1336
+ s->n_end++;
1337
+ }
1338
+
1339
+ ScrChildStream *scr_child_stdout(ScrChild *c) {
1340
+ return c->out_stream ? scr_child_stream_retain(c->out_stream) : NULL;
1341
+ }
1342
+ ScrChildStream *scr_child_stderr(ScrChild *c) {
1343
+ return c->err_stream ? scr_child_stream_retain(c->err_stream) : NULL;
1344
+ }
1345
+
1346
+ void scr_child_stream_thunk0(ScrClosure *cb, ScrBytes *chunk) {
1347
+ (void)chunk;
1348
+ ((void (*)(ScrClosure *))cb->fn)(cb);
1349
+ }
1350
+ void scr_child_stream_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) {
1351
+ /* The listener owns its +1 param per the universal convention. */
1352
+ ((void (*)(ScrClosure *, ScrBytes *))cb->fn)(cb, scr_bytes_retain(chunk));
1353
+ }
1354
+
1355
+ /* ── spawn ───────────────────────────────────────────────────────────── */
1356
+
1357
+ ScrChild *scr_spawn(ScrStr *cmd, ScrArr *args) {
1358
+ return scr_spawn_opts(cmd, args, 0, 0, 0, 0, 0, false, false, NULL, NULL);
1359
+ }
1360
+
1361
+ /* PER-SLOT stdio modes (the POSIX arm's numbering): in 0 = ignore (NUL),
1362
+ * 1 = inherit; out/err 0 = ignore, 1 = inherit, 2 = fd, 3 = pipe. */
1363
+ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
1364
+ double out_mode, double err_mode, double out_fd,
1365
+ double err_fd, bool detached, bool has_env,
1366
+ ScrArr *env_pairs, ScrStr *cwd) {
1367
+ if ((int)out_mode == 1 || (int)err_mode == 1 || (int)out_mode == 2 || (int)err_mode == 2) {
1368
+ fflush(stdout);
1369
+ fflush(stderr);
1370
+ }
1371
+ ScrChild *c = calloc(1, sizeof(ScrChild));
1372
+ if (!c) scr_child_oom();
1373
+ c->rc = 1;
1374
+
1375
+ /* Piped slots: the pipe NOW; a pipe failure degrades the slot to NUL
1376
+ * and the stream husk answers eof (fd exhaustion — nothing real). */
1377
+ HANDLE out_parent = NULL, out_child = NULL;
1378
+ HANDLE err_parent = NULL, err_child = NULL;
1379
+ int out_m = (int)out_mode, err_m = (int)err_mode;
1380
+ if (out_m == 3 && !scr_win_pipe(&out_parent, &out_child, true)) out_m = 0;
1381
+ if (err_m == 3 && !scr_win_pipe(&err_parent, &err_child, true)) err_m = 0;
1382
+
1383
+ HANDLE in_child = (int)in_mode == 1
1384
+ ? scr_win_dup_inherit(GetStdHandle(STD_INPUT_HANDLE))
1385
+ : NULL;
1386
+ if (in_child == NULL) in_child = scr_win_nul(false);
1387
+ if (out_child == NULL) {
1388
+ if (out_m == 1) out_child = scr_win_dup_inherit(GetStdHandle(STD_OUTPUT_HANDLE));
1389
+ else if (out_m == 2) out_child = scr_win_dup_inherit((HANDLE)_get_osfhandle((int)out_fd));
1390
+ if (out_child == NULL) out_child = scr_win_nul(true);
1391
+ }
1392
+ if (err_child == NULL) {
1393
+ if (err_m == 1) err_child = scr_win_dup_inherit(GetStdHandle(STD_ERROR_HANDLE));
1394
+ else if (err_m == 2) err_child = scr_win_dup_inherit((HANDLE)_get_osfhandle((int)err_fd));
1395
+ if (err_child == NULL) err_child = scr_win_nul(true);
1396
+ }
1397
+
1398
+ ScrWinSpawn sp = {
1399
+ .in_child = in_child,
1400
+ .out_child = out_child,
1401
+ .err_child = err_child,
1402
+ .cwd = cwd != NULL && cwd->len > 0 ? cwd : NULL,
1403
+ .env_pairs = has_env ? env_pairs : NULL,
1404
+ .detached = detached,
1405
+ };
1406
+ scr_win_create_process(cmd, args, &sp);
1407
+ if (in_child != NULL) CloseHandle(in_child);
1408
+ if (out_child != NULL) CloseHandle(out_child);
1409
+ if (err_child != NULL) CloseHandle(err_child);
1410
+
1411
+ /* The parent's pipe read ends become the streams regardless of the
1412
+ * spawn's outcome — a FAILED spawn keeps them with no writer at all,
1413
+ * so a consumer sees immediate EOF and 'end' fires on the turn after
1414
+ * the 'error' event, Node's exact order (the POSIX arm's stance). */
1415
+ if (out_m == 3) c->out_stream = scr_child_stream_new(out_parent);
1416
+ if (err_m == 3) c->err_stream = scr_child_stream_new(err_parent);
1417
+
1418
+ if (sp.errname != NULL) {
1419
+ size_t len = 6 + cmd->len + 1 + strlen(sp.errname);
1420
+ char *msg = malloc(len + 1);
1421
+ if (!msg) scr_child_oom();
1422
+ snprintf(msg, len + 1, "spawn %s %s", cmd->data, sp.errname);
1423
+ c->state = SCR_CHILD_SPAWN_FAILED;
1424
+ c->spawn_errname = sp.errname;
1425
+ c->spawn_uv_errno = scr_win_uv_errno(sp.errname);
1426
+ c->err_msg = scr_str_new(msg, len);
1427
+ free(msg);
1428
+ /* Settles at the next quiescent pass; no exit handle will ever wake
1429
+ * the loop for it, so it counts as unwatched until then. */
1430
+ scr_children_unwatched++;
1431
+ } else {
1432
+ c->state = SCR_CHILD_RUNNING;
1433
+ c->proc = sp.proc;
1434
+ c->pid = sp.pid;
1435
+ }
1436
+ /* The registry's reference: dropped when the child settles. */
1437
+ c->reffed = true;
1438
+ scr_children_reffed_n++;
1439
+ c->next = scr_children;
1440
+ scr_children = scr_child_retain(c);
1441
+ return c;
1442
+ }
1443
+
1444
+ /* ── the lifecycle members ───────────────────────────────────────────── */
1445
+
1446
+ bool scr_child_has_pid(ScrChild *c) { return c->state != SCR_CHILD_SPAWN_FAILED; }
1447
+ double scr_child_pid(ScrChild *c) { return (double)c->pid; }
1448
+
1449
+ bool scr_child_has_exit_code(ScrChild *c) {
1450
+ if (c->state == SCR_CHILD_EXITED) return c->has_code;
1451
+ return c->state == SCR_CHILD_SPAWN_FAILED && c->settled;
1452
+ }
1453
+ double scr_child_exit_code(ScrChild *c) {
1454
+ if (c->state == SCR_CHILD_SPAWN_FAILED) return (double)c->spawn_uv_errno;
1455
+ return c->code;
1456
+ }
1457
+
1458
+ bool scr_child_killed(ScrChild *c) { return c->killed; }
1459
+
1460
+ /* child.kill core: libuv's uv_kill emulation. SIGTERM/SIGKILL/SIGINT/
1461
+ * SIGQUIT/SIGHUP → TerminateProcess(h, 1) with the signal name recorded
1462
+ * for the exit event (Node's code null + signal); 0 probes liveness;
1463
+ * anything else answers false (libuv: ENOSYS — no error event wired,
1464
+ * the POSIX arm's unreachable-error stance). Node sets `killed` on ANY
1465
+ * successful send, signal 0 included. */
1466
+ static bool scr_child_kill_signo(ScrChild *c, int signo) {
1467
+ if (c->state != SCR_CHILD_RUNNING || c->proc == NULL) return false;
1468
+ if (signo == 0) {
1469
+ if (WaitForSingleObject(c->proc, 0) == WAIT_OBJECT_0) return false; /* exited: ESRCH */
1470
+ c->killed = true;
1471
+ return true;
1472
+ }
1473
+ if (signo != SIGTERM && signo != SIGKILL && signo != SIGINT &&
1474
+ signo != SIGQUIT && signo != SIGHUP) {
1475
+ return false;
1476
+ }
1477
+ if (!TerminateProcess(c->proc, 1)) return false; /* already exited: ESRCH */
1478
+ c->kill_signal = scr_signal_name(signo);
1479
+ c->killed = true;
1480
+ return true;
1481
+ }
1482
+
1483
+ bool scr_child_kill(ScrChild *c, const ScrStr *signal) {
1484
+ int signo = scr_signal_from_name(signal);
1485
+ if (signo < 0) {
1486
+ size_t cap = 16 + signal->len + 1;
1487
+ char *msg = malloc(cap);
1488
+ if (!msg) scr_child_oom();
1489
+ int len = snprintf(msg, cap, "Unknown signal: %s", signal->data);
1490
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
1491
+ free(msg);
1492
+ return false;
1493
+ }
1494
+ return scr_child_kill_signo(c, signo);
1495
+ }
1496
+
1497
+ bool scr_child_kill_num(ScrChild *c, double signum) {
1498
+ return scr_child_kill_signo(c, (int)signum);
1499
+ }
1500
+
1501
+ void scr_child_unref(ScrChild *c) {
1502
+ if (c->reffed) {
1503
+ c->reffed = false;
1504
+ scr_children_reffed_n--; /* settle/teardown clear reffed first */
1505
+ }
1506
+ }
1507
+
1508
+ bool scr_children_reffed_pending(void) {
1509
+ return scr_children_reffed_n > 0 || scr_child_streams_watching > 0;
1510
+ }
1511
+
1512
+ void scr_children_teardown(void) {
1513
+ while (scr_child_streams != NULL) {
1514
+ ScrChildStream *s = scr_child_streams;
1515
+ scr_child_streams = s->next;
1516
+ s->next = NULL;
1517
+ scr_child_stream_finish(s, false);
1518
+ scr_child_stream_release(s);
1519
+ }
1520
+ while (scr_children != NULL) {
1521
+ ScrChild *c = scr_children;
1522
+ scr_children = c->next;
1523
+ c->next = NULL;
1524
+ c->settled = true; /* late listener registrations release immediately */
1525
+ if (c->reffed) {
1526
+ c->reffed = false;
1527
+ scr_children_reffed_n--;
1528
+ }
1529
+ if (c->state == SCR_CHILD_SPAWN_FAILED) scr_children_unwatched--;
1530
+ scr_child_drop_listeners(c);
1531
+ scr_child_release(c);
1532
+ }
1533
+ }
1534
+
1535
+ void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn) {
1536
+ if (c->settled) {
1537
+ scr_closure_release(cb); /* after the terminal event: never fires */
1538
+ return;
1539
+ }
1540
+ if (c->n_exit == c->cap_exit) {
1541
+ c->cap_exit = c->cap_exit ? c->cap_exit * 2 : 2;
1542
+ c->exit_cbs = realloc(c->exit_cbs, c->cap_exit * sizeof(*c->exit_cbs));
1543
+ if (!c->exit_cbs) scr_child_oom();
1544
+ }
1545
+ c->exit_cbs[c->n_exit].cb = cb;
1546
+ c->exit_cbs[c->n_exit].fn = fn;
1547
+ c->n_exit++;
1548
+ }
1549
+
1550
+ void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn) {
1551
+ if (c->settled) {
1552
+ scr_closure_release(cb);
1553
+ return;
1554
+ }
1555
+ if (c->n_err == c->cap_err) {
1556
+ c->cap_err = c->cap_err ? c->cap_err * 2 : 2;
1557
+ c->err_cbs = realloc(c->err_cbs, c->cap_err * sizeof(*c->err_cbs));
1558
+ if (!c->err_cbs) scr_child_oom();
1559
+ }
1560
+ c->err_cbs[c->n_err].cb = cb;
1561
+ c->err_cbs[c->n_err].fn = fn;
1562
+ c->n_err++;
1563
+ }
1564
+
1565
+ /* ── the runtime-provided listener adapters (the POSIX arm's) ────────── */
1566
+
1567
+ void scr_child_exit_thunk0(ScrClosure *cb, bool has_code, double code,
1568
+ const char *signal_name) {
1569
+ (void)has_code;
1570
+ (void)code;
1571
+ (void)signal_name;
1572
+ ((void (*)(ScrClosure *))cb->fn)(cb);
1573
+ }
1574
+
1575
+ void scr_child_err_thunk0(ScrClosure *cb, ScrStr *msg) {
1576
+ (void)msg;
1577
+ ((void (*)(ScrClosure *))cb->fn)(cb);
1578
+ }
1579
+
1580
+ static const char *scr_child_err_code = NULL;
1581
+
1582
+ /* The errno name embedded in an errnoException-style message — the
1583
+ * POSIX arm's parser, shared story (see its comment). */
1584
+ static const char *scr_err_msg_code(const ScrStr *msg) {
1585
+ static char buf[32];
1586
+ const char *sp = strchr((const char *)msg->data, ' ');
1587
+ if (sp == NULL || sp[1] != 'E') return NULL;
1588
+ const char *tok = sp + 1;
1589
+ size_t n = 1;
1590
+ while (tok[n] != '\0' && tok[n] != ' ' && tok[n] != ':') {
1591
+ char ch = tok[n];
1592
+ if (!((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))) return NULL;
1593
+ if (n + 1 >= sizeof buf) return NULL;
1594
+ n++;
1595
+ }
1596
+ if (n < 3) return NULL; /* "EIO" is the shortest real name */
1597
+ memcpy(buf, tok, n);
1598
+ buf[n] = '\0';
1599
+ return buf;
1600
+ }
1601
+
1602
+ void scr_child_err_thunk_error(ScrClosure *cb, ScrStr *msg) {
1603
+ ScrError *e = scr_error_new(0 /* Error */, msg);
1604
+ if (scr_child_err_code != NULL) {
1605
+ scr_error_set_code(e, scr_child_err_code);
1606
+ } else {
1607
+ const char *code = scr_err_msg_code(msg);
1608
+ if (code != NULL) scr_error_set_code(e, code);
1609
+ }
1610
+ ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, e);
1611
+ }
1612
+
1613
+ /* ── the loop's half (called from scr_async.c) ───────────────────────── */
1614
+
1615
+ bool scr_children_pending(void) {
1616
+ return scr_children != NULL || scr_child_streams_watching > 0;
1617
+ }
1618
+
1619
+ bool scr_children_failed_pending(void) {
1620
+ for (ScrChild *c = scr_children; c; c = c->next) {
1621
+ if (c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) return true;
1622
+ }
1623
+ return false;
1624
+ }
1625
+
1626
+ /* No pollable wake fd on Windows (the loop's poll(2) branch is POSIX-
1627
+ * only); scr_children_wait below is the wakeup story. */
1628
+ int scr_children_wake_fd(void) { return -1; }
1629
+
1630
+ /* The loop's quiescent sleep while children are pending: wait on the
1631
+ * child process HANDLES up to max_wait_ms — an exit ends the sleep
1632
+ * immediately and the next reap pass settles it. Declines (false: the
1633
+ * caller keeps the ~1ms polling cap) when a pending child has no
1634
+ * waitable handle (a spawn failure awaiting settle), a consumer-owning
1635
+ * stream needs pumping (pipe readability is poll-only), or the handle
1636
+ * table outgrows WaitForMultipleObjects. */
1637
+ bool scr_children_wait(double max_wait_ms) {
1638
+ if (scr_children_unwatched > 0 || scr_child_streams_watching > 0) return false;
1639
+ HANDLE hs[MAXIMUM_WAIT_OBJECTS];
1640
+ DWORD n = 0;
1641
+ for (ScrChild *c = scr_children; c; c = c->next) {
1642
+ if (c->state != SCR_CHILD_RUNNING || c->proc == NULL) continue;
1643
+ if (n == MAXIMUM_WAIT_OBJECTS) return false;
1644
+ hs[n++] = c->proc;
1645
+ }
1646
+ if (n == 0) return false;
1647
+ if (!(max_wait_ms > 0)) max_wait_ms = 0;
1648
+ if (max_wait_ms > 2147483000.0) max_wait_ms = 2147483000.0;
1649
+ (void)WaitForMultipleObjects(n, hs, FALSE, (DWORD)(max_wait_ms + 0.999));
1650
+ return true;
1651
+ }
1652
+
1653
+ /* Fires one settled child's terminal event — the POSIX arm's settle,
1654
+ * verbatim (streams drain first so 'end' precedes 'exit'; spawn
1655
+ * failures fire 'error' or die unhandled). */
1656
+ static void scr_child_settle(ScrChild *c) {
1657
+ if (c->reffed) {
1658
+ c->reffed = false;
1659
+ scr_children_reffed_n--;
1660
+ }
1661
+ if (c->state == SCR_CHILD_EXITED) {
1662
+ scr_child_stream_drain_now(c->out_stream);
1663
+ if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream);
1664
+ }
1665
+ c->settled = true;
1666
+ if (scr_exc_pending()) {
1667
+ scr_child_drop_listeners(c);
1668
+ scr_child_release(c);
1669
+ return;
1670
+ }
1671
+ if (c->state == SCR_CHILD_SPAWN_FAILED) {
1672
+ if (c->n_err == 0) {
1673
+ fflush(stdout);
1674
+ fprintf(stderr, "Unhandled 'error' event: Error: %s\n", c->err_msg->data);
1675
+ _Exit(1);
1676
+ }
1677
+ scr_child_err_code = c->spawn_errname;
1678
+ for (size_t i = 0; i < c->n_err; i++) {
1679
+ c->err_cbs[i].fn(c->err_cbs[i].cb, c->err_msg);
1680
+ if (scr_exc_pending()) break; /* the loop surfaces it */
1681
+ }
1682
+ scr_child_err_code = NULL;
1683
+ } else {
1684
+ for (size_t i = 0; i < c->n_exit; i++) {
1685
+ c->exit_cbs[i].fn(c->exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
1686
+ if (scr_exc_pending()) break;
1687
+ }
1688
+ }
1689
+ scr_child_drop_listeners(c);
1690
+ scr_child_release(c); /* the registry's reference */
1691
+ }
1692
+
1693
+ /* One reap pass: streams service first (the pinned 'end'-before-'exit'
1694
+ * ordering), then every running child answers WaitForSingleObject(h, 0)
1695
+ * — the WNOHANG analogue; spawn failures settle on their first pass. */
1696
+ void scr_children_poll(void) {
1697
+ scr_child_streams_service();
1698
+ if (scr_exc_pending()) return;
1699
+ ScrChild **link = &scr_children;
1700
+ while (*link) {
1701
+ ScrChild *c = *link;
1702
+ bool settle = false;
1703
+ if (c->state == SCR_CHILD_SPAWN_FAILED) {
1704
+ scr_children_unwatched--;
1705
+ settle = true;
1706
+ } else {
1707
+ DWORD w = WaitForSingleObject(c->proc, 0);
1708
+ if (w == WAIT_OBJECT_0) {
1709
+ DWORD code = 0;
1710
+ (void)GetExitCodeProcess(c->proc, &code);
1711
+ CloseHandle(c->proc);
1712
+ c->proc = NULL;
1713
+ c->state = SCR_CHILD_EXITED;
1714
+ if (c->kill_signal != NULL) {
1715
+ /* We terminated it: Node's code null + the signal's name
1716
+ * (libuv's term_signal reporting). */
1717
+ c->has_code = false;
1718
+ c->code = 0;
1719
+ c->exit_signal = c->kill_signal;
1720
+ } else {
1721
+ c->has_code = true;
1722
+ c->code = (double)code;
1723
+ }
1724
+ settle = true;
1725
+ } else if (w == WAIT_FAILED) {
1726
+ /* An unwaitable handle: settle as a signal-style exit so the
1727
+ * loop can never spin forever (the POSIX ECHILD stance). */
1728
+ CloseHandle(c->proc);
1729
+ c->proc = NULL;
1730
+ c->state = SCR_CHILD_EXITED;
1731
+ c->has_code = false;
1732
+ c->code = 0;
1733
+ settle = true;
1734
+ }
1735
+ }
1736
+ if (settle) {
1737
+ *link = c->next; /* unlink BEFORE firing: callbacks may spawn */
1738
+ c->next = NULL;
1739
+ scr_child_settle(c);
1740
+ if (scr_exc_pending()) return;
1741
+ } else {
1742
+ link = &c->next;
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ #else /* !_WIN32 — the POSIX implementation, untouched below */
1748
+
1749
+ #include <errno.h>
1750
+ #include <fcntl.h>
1751
+ #include <poll.h>
1752
+ #include <signal.h>
1753
+ #include <spawn.h>
1754
+ #include <stdio.h>
1755
+ #include <stdlib.h>
1756
+ #include <string.h>
1757
+ #include <sys/wait.h>
1758
+ #include <time.h>
1759
+ #include <unistd.h>
1760
+
1761
+ /* Child-exit wakeups without polling — the narrow seam scr_platform.h
1762
+ * points here for (this unit links into EVERY binary, so its plumbing
1763
+ * stays inline rather than pulling both poller backends onto every link
1764
+ * line). BSD: kqueue's PROC filter — the loop's quiescent sleep waits on
1765
+ * the kqueue fd and a NOTE_EXIT wakes it the moment a child dies; piped
1766
+ * stream read-ends ride the same kqueue as level-triggered read filters.
1767
+ * Linux: pidfd_open per spawned child registered EPOLLIN in a dedicated
1768
+ * epoll (a pidfd polls readable from exit until the reap CLOSES it —
1769
+ * scr_children_poll unwatches right after waitpid succeeds); stream
1770
+ * read-ends ride the same epoll, level-triggered like the kqueue arm.
1771
+ * Both arms deliver WAKEUPS ONLY (events drained and discarded; the
1772
+ * WNOHANG sweep stays the single reaping mechanism), and both fall back
1773
+ * to the portable ~1ms polling cap whenever any pending child cannot be
1774
+ * represented (spawn failures awaiting settle, a failed watch
1775
+ * registration — kqueue refuses zombies, pidfd_open can hit ENOSYS/
1776
+ * EMFILE). Everywhere else the polling path compiles alone. */
1777
+ #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
1778
+ defined(__OpenBSD__) || defined(__DragonFly__)
1779
+ #define SCR_HAVE_KQUEUE 1
1780
+ #include <sys/event.h>
1781
+ #elif defined(__linux__)
1782
+ #define SCR_CHILD_HAVE_PIDFD 1
1783
+ #include <sys/epoll.h>
1784
+ #if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 36))
1785
+ #include <sys/pidfd.h> /* pidfd_open has a libc wrapper from glibc 2.36 */
1786
+ #else
1787
+ #include <sys/syscall.h>
1788
+ static int pidfd_open(pid_t pid, unsigned int flags) {
1789
+ return (int)syscall(SYS_pidfd_open, pid, flags);
1790
+ }
1791
+ #endif
1792
+ #endif
1793
+
1794
+ extern char **environ;
1795
+
1796
+ static void scr_child_oom(void) {
1797
+ fputs("scriptc: out of memory\n", stderr);
1798
+ abort();
1799
+ }
1800
+
1801
+ static const char *scr_errname(int err); /* defined with the spawn slice */
1802
+
1803
+ /* ── the spawnSync result value ──────────────────────────────────────── */
1804
+
1805
+ struct ScrSpawnRes {
1806
+ size_t rc;
1807
+ bool has_status; /* false: signal death or spawn failure (status null) */
1808
+ double status;
1809
+ ScrStr *out; /* captured stdout, utf8 */
1810
+ ScrStr *err; /* captured stderr, utf8 */
1811
+ /* Spawn failure (Node's `error` property): the errno name for the
1812
+ * message ("spawnSync <file> ENOENT") and `code` stamp — a timeout kill
1813
+ * rides the same slot as ETIMEDOUT. NULL = no error — the property
1814
+ * reads undefined, exactly Node. */
1815
+ const char *spawn_errname;
1816
+ ScrStr *cmd; /* +1, for the error message; NULL when spawn_errname is */
1817
+ /* The termination signal's name (static storage; "SIGTERM", ...) when a
1818
+ * signal killed the child — a timeout's killSignal included — NULL for
1819
+ * a normal exit or spawn failure. Node's result.signal. */
1820
+ const char *signal_name;
1821
+ };
1822
+
1823
+ ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r) {
1824
+ if (r->rc != SIZE_MAX) r->rc++;
1825
+ return r;
1826
+ }
1827
+
1828
+ void scr_spawn_res_release(ScrSpawnRes *r) {
1829
+ if (!r || r->rc == SIZE_MAX) return;
1830
+ if (--r->rc == 0) {
1831
+ scr_str_release(r->out);
1832
+ scr_str_release(r->err);
1833
+ scr_str_release(r->cmd); /* NULL-safe */
1834
+ free(r);
1835
+ }
1836
+ }
1837
+
1838
+ void *scr_spawn_res_retain_v(void *p) { return scr_spawn_res_retain((ScrSpawnRes *)p); }
1839
+ void scr_spawn_res_release_v(void *p) { scr_spawn_res_release((ScrSpawnRes *)p); }
1840
+
1841
+ bool scr_spawn_res_has_status(ScrSpawnRes *r) { return r->has_status; }
1842
+ double scr_spawn_res_status(ScrSpawnRes *r) { return r->status; }
1843
+ ScrStr *scr_spawn_res_stdout(ScrSpawnRes *r) { return scr_str_retain(r->out); }
1844
+ ScrStr *scr_spawn_res_stderr(ScrSpawnRes *r) { return scr_str_retain(r->err); }
1845
+ bool scr_spawn_res_has_signal(ScrSpawnRes *r) { return r->signal_name != NULL; }
1846
+ ScrStr *scr_spawn_res_signal(ScrSpawnRes *r) {
1847
+ return scr_str_new(r->signal_name, strlen(r->signal_name));
1848
+ }
1849
+
1850
+ /* The `error` property: a fresh +1 %Error ("spawnSync <file> ENOENT",
1851
+ * `code` stamped — Node's uv errnoException) when the spawn itself
1852
+ * failed, NULL (the undefined arm, wrapped type-directedly by the
1853
+ * emitter) otherwise. Constructed per read; Node reuses one object, but
1854
+ * the difference is only `===` identity between two reads of `.error`,
1855
+ * which nothing narrows on. */
1856
+ ScrError *scr_spawn_res_error(ScrSpawnRes *r) {
1857
+ if (r->spawn_errname == NULL) return NULL;
1858
+ size_t cap = 10 + (r->cmd ? r->cmd->len : 0) + 1 + strlen(r->spawn_errname) + 1;
1859
+ char *msg = malloc(cap);
1860
+ if (!msg) scr_child_oom();
1861
+ snprintf(msg, cap, "spawnSync %s %s", r->cmd ? r->cmd->data : "", r->spawn_errname);
1862
+ ScrStr *m = scr_str_new(msg, strlen(msg));
1863
+ free(msg);
1864
+ ScrError *e = scr_error_new(0 /* Error */, m);
1865
+ scr_str_release(m);
1866
+ scr_error_set_code(e, r->spawn_errname);
1867
+ return e;
1868
+ }
1869
+
1870
+ /* ── argv construction (shared with spawn in scr_async.c's slice) ─────── */
1871
+
1872
+ /* Builds a NULL-terminated argv from the command + one string[] — the
1873
+ * strings are BORROWED from their containers (alive for the whole spawn;
1874
+ * the +1 from scr_arr_get_ref is dropped immediately because the array
1875
+ * keeps its own reference). Free with free() (the vector only). */
1876
+ char **scr_child_argv(ScrStr *cmd, ScrArr *args) {
1877
+ size_t n = (size_t)scr_arr_len(args);
1878
+ char **argv = malloc((n + 2) * sizeof(char *));
1879
+ if (!argv) scr_child_oom();
1880
+ argv[0] = cmd->data;
1881
+ for (size_t i = 0; i < n; i++) {
1882
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
1883
+ argv[i + 1] = s->data;
1884
+ scr_str_release(s); /* borrow: the array's reference keeps it alive */
1885
+ }
1886
+ argv[n + 1] = NULL;
1887
+ return argv;
1888
+ }
1889
+
1890
+ /* ── capture buffers ─────────────────────────────────────────────────── */
1891
+
1892
+ typedef struct {
1893
+ char *data;
1894
+ size_t len, cap;
1895
+ } ScrCapBuf;
1896
+
1897
+ static void scr_cap_init(ScrCapBuf *b) {
1898
+ b->cap = 4096;
1899
+ b->len = 0;
1900
+ b->data = malloc(b->cap);
1901
+ if (!b->data) scr_child_oom();
1902
+ }
1903
+
1904
+ /* Reads once from fd into the buffer; returns false on EOF (or error —
1905
+ * a broken pipe reads as end of capture, like libuv treats it). */
1906
+ static bool scr_cap_read(ScrCapBuf *b, int fd) {
1907
+ if (b->len + 4096 > b->cap) {
1908
+ b->cap *= 2;
1909
+ b->data = realloc(b->data, b->cap);
1910
+ if (!b->data) scr_child_oom();
1911
+ }
1912
+ ssize_t got = read(fd, b->data + b->len, b->cap - b->len);
1913
+ if (got <= 0) return false;
1914
+ b->len += (size_t)got;
1915
+ return true;
1916
+ }
1917
+
1918
+ /* ── spawnSync ───────────────────────────────────────────────────────── */
1919
+
1920
+ static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status,
1921
+ ScrStr *out /*moved*/, ScrStr *err /*moved*/) {
1922
+ ScrSpawnRes *r = malloc(sizeof(ScrSpawnRes));
1923
+ if (!r) scr_child_oom();
1924
+ r->rc = 1;
1925
+ r->has_status = has_status;
1926
+ r->status = status;
1927
+ r->out = out;
1928
+ r->err = err;
1929
+ r->spawn_errname = NULL;
1930
+ r->cmd = NULL;
1931
+ r->signal_name = NULL;
1932
+ return r;
1933
+ }
1934
+
1935
+ /* The parameterized spawnSync core. stdio modes: stdin 0 = /dev/null
1936
+ * (Node's no-input default AND "ignore"/"pipe"-without-input — all read
1937
+ * nothing), 2 = inherit; stdout/stderr 0 = capture (the pipe), 1 =
1938
+ * ignore (/dev/null; the result string reads ""), 2 = inherit (""
1939
+ * likewise — Node types those null; the "" stance is spawnSync's
1940
+ * documented divergence). timeout_ms > 0 sends `killsig` at the deadline
1941
+ * and the result carries error: ETIMEDOUT + the signal — never a throw,
1942
+ * Node's spawnSync shape. */
1943
+ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeout_ms,
1944
+ int killsig, int in_mode, int out_mode,
1945
+ int err_mode) {
1946
+ if (out_mode == 2 || err_mode == 2) {
1947
+ /* Inherited output fds: flush the parent's buffers first so earlier
1948
+ * logs precede the child's writes (Node's practical ordering). */
1949
+ fflush(stdout);
1950
+ fflush(stderr);
1951
+ }
1952
+ int outfd[2] = {-1, -1}, errfd[2] = {-1, -1};
1953
+ bool cap_out = out_mode == 0, cap_err = err_mode == 0;
1954
+ if ((cap_out && pipe(outfd) != 0) || (cap_err && pipe(errfd) != 0)) {
1955
+ fputs("scriptc: pipe() failed\n", stderr);
1956
+ abort();
1957
+ }
1958
+
1959
+ posix_spawn_file_actions_t fa;
1960
+ posix_spawn_file_actions_init(&fa);
1961
+ if (in_mode != 2) {
1962
+ posix_spawn_file_actions_addopen(&fa, 0, "/dev/null", O_RDONLY, 0);
1963
+ }
1964
+ if (cap_out) {
1965
+ posix_spawn_file_actions_adddup2(&fa, outfd[1], 1);
1966
+ posix_spawn_file_actions_addclose(&fa, outfd[0]);
1967
+ posix_spawn_file_actions_addclose(&fa, outfd[1]);
1968
+ } else if (out_mode == 1) {
1969
+ posix_spawn_file_actions_addopen(&fa, 1, "/dev/null", O_WRONLY, 0);
1970
+ }
1971
+ if (cap_err) {
1972
+ posix_spawn_file_actions_adddup2(&fa, errfd[1], 2);
1973
+ posix_spawn_file_actions_addclose(&fa, errfd[0]);
1974
+ posix_spawn_file_actions_addclose(&fa, errfd[1]);
1975
+ } else if (err_mode == 1) {
1976
+ posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0);
1977
+ }
1978
+
1979
+ char **argv = scr_child_argv(cmd, args);
1980
+ pid_t pid = -1;
1981
+ int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ);
1982
+ posix_spawn_file_actions_destroy(&fa);
1983
+ free(argv);
1984
+ if (cap_out) close(outfd[1]);
1985
+ if (cap_err) close(errfd[1]);
1986
+
1987
+ if (spawn_err != 0) {
1988
+ /* Node: status null, error ENOENT/EACCES/... — never a throw. The
1989
+ * errno rides the result for the `error` property read. */
1990
+ if (cap_out) close(outfd[0]);
1991
+ if (cap_err) close(errfd[0]);
1992
+ ScrSpawnRes *r = scr_spawn_res_new(false, 0, scr_str_new("", 0), scr_str_new("", 0));
1993
+ r->spawn_errname = scr_errname(spawn_err);
1994
+ r->cmd = scr_str_retain(cmd);
1995
+ return r;
1996
+ }
1997
+
1998
+ /* Drain BOTH pipes together (a child filling one while we block on the
1999
+ * other would deadlock a sequential read); the poll deadline implements
2000
+ * the timeout, exactly the exec core's discipline. */
2001
+ ScrCapBuf out, err;
2002
+ scr_cap_init(&out);
2003
+ scr_cap_init(&err);
2004
+ bool out_open = cap_out, err_open = cap_err;
2005
+ bool timed_out = false;
2006
+ double deadline = timeout_ms > 0 ? scr_now_ms() + timeout_ms : 0;
2007
+ while (out_open || err_open) {
2008
+ struct pollfd fds[2];
2009
+ nfds_t n = 0;
2010
+ if (out_open) { fds[n].fd = outfd[0]; fds[n].events = POLLIN; n++; }
2011
+ if (err_open) { fds[n].fd = errfd[0]; fds[n].events = POLLIN; n++; }
2012
+ int wait = -1;
2013
+ if (deadline > 0 && !timed_out) {
2014
+ double left = deadline - scr_now_ms();
2015
+ wait = left > 0 ? (int)(left + 0.999) : 0;
2016
+ }
2017
+ int rc = poll(fds, n, wait);
2018
+ if (rc < 0) {
2019
+ if (errno == EINTR) continue;
2020
+ break;
2021
+ }
2022
+ if (deadline > 0 && !timed_out && scr_now_ms() >= deadline) {
2023
+ timed_out = true;
2024
+ kill(pid, killsig);
2025
+ }
2026
+ if (rc == 0) continue;
2027
+ for (nfds_t i = 0; i < n; i++) {
2028
+ if (!(fds[i].revents & (POLLIN | POLLHUP | POLLERR))) continue;
2029
+ if (out_open && fds[i].fd == outfd[0]) out_open = scr_cap_read(&out, outfd[0]);
2030
+ else if (err_open && fds[i].fd == errfd[0]) err_open = scr_cap_read(&err, errfd[0]);
2031
+ }
2032
+ }
2033
+ if (cap_out) close(outfd[0]);
2034
+ if (cap_err) close(errfd[0]);
2035
+
2036
+ /* Reap. With nothing captured the deadline lives here: WNOHANG-poll
2037
+ * until the child exits or the timeout kill fires (the exec core's
2038
+ * pipes-drained-daemon discipline). */
2039
+ int st = 0;
2040
+ pid_t reaped = -1;
2041
+ if (deadline > 0 && !timed_out) {
2042
+ for (;;) {
2043
+ reaped = waitpid(pid, &st, WNOHANG);
2044
+ if (reaped == pid || (reaped < 0 && errno != EINTR)) break;
2045
+ if (scr_now_ms() >= deadline) {
2046
+ timed_out = true;
2047
+ kill(pid, killsig);
2048
+ break;
2049
+ }
2050
+ struct timespec ts = {0, 500000}; /* 0.5ms */
2051
+ nanosleep(&ts, NULL);
2052
+ }
2053
+ }
2054
+ if (reaped != pid) {
2055
+ do {
2056
+ reaped = waitpid(pid, &st, 0);
2057
+ } while (reaped < 0 && errno == EINTR);
2058
+ }
2059
+
2060
+ ScrStr *out_s = scr_str_new(out.data, out.len);
2061
+ ScrStr *err_s = scr_str_new(err.data, err.len);
2062
+ free(out.data);
2063
+ free(err.data);
2064
+ ScrSpawnRes *r;
2065
+ if (WIFEXITED(st) && !timed_out) {
2066
+ r = scr_spawn_res_new(true, (double)WEXITSTATUS(st), out_s, err_s);
2067
+ } else {
2068
+ /* Killed by a signal (the timeout's included): Node's status null,
2069
+ * signal = the terminating signal's name. */
2070
+ r = scr_spawn_res_new(false, 0, out_s, err_s);
2071
+ if (WIFSIGNALED(st)) r->signal_name = scr_signal_name(WTERMSIG(st));
2072
+ if (timed_out) {
2073
+ /* Node: error ETIMEDOUT + signal = killSignal (the kill may have
2074
+ * raced a normal exit; Node reports the timeout either way). */
2075
+ if (r->signal_name == NULL) r->signal_name = scr_signal_name(killsig);
2076
+ r->spawn_errname = "ETIMEDOUT";
2077
+ r->cmd = scr_str_retain(cmd);
2078
+ }
2079
+ }
2080
+ return r;
2081
+ }
2082
+
2083
+ ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args) {
2084
+ return scr_spawn_sync_core(cmd, args, 0, SIGTERM, 0, 0, 0);
2085
+ }
2086
+
2087
+ /* The options entry (cp.spawnSyncOpts): killSignal as a NAME ("" = the
2088
+ * SIGTERM default — the compiler already validated the literal against
2089
+ * Node's table by fencing non-literals; an unknown name here falls back
2090
+ * to SIGTERM defensively), modes as doubles (the IR's scalar). */
2091
+ ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
2092
+ ScrStr *killsignal, double in_mode,
2093
+ double out_mode, double err_mode) {
2094
+ int killsig = SIGTERM;
2095
+ if (killsignal->len > 0) {
2096
+ int resolved = scr_signal_from_name(killsignal);
2097
+ if (resolved > 0) killsig = resolved;
2098
+ }
2099
+ return scr_spawn_sync_core(cmd, args, timeout_ms, killsig, (int)in_mode,
2100
+ (int)out_mode, (int)err_mode);
2101
+ }
2102
+
2103
+ /* The runtime-string stdio entry (cp.spawnSyncStdioStr): the compiler
2104
+ * proved the value's TYPE is a union of "pipe"/"ignore"/"inherit"
2105
+ * literals (the defaultRunner idiom `stdio: options?.stdio ?? "pipe"`),
2106
+ * so mapping to the three modes happens here at the call. */
2107
+ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms,
2108
+ ScrStr *killsignal, ScrStr *stdio) {
2109
+ double in_mode = 0, out_mode = 0, err_mode = 0; /* "pipe": the defaults */
2110
+ if (stdio->len == 7 && memcmp(stdio->data, "inherit", 7) == 0) {
2111
+ in_mode = 2; out_mode = 2; err_mode = 2;
2112
+ } else if (stdio->len == 6 && memcmp(stdio->data, "ignore", 6) == 0) {
2113
+ out_mode = 1; err_mode = 1;
2114
+ }
2115
+ return scr_spawn_sync_opts(cmd, args, timeout_ms, killsignal, in_mode, out_mode, err_mode);
2116
+ }
2117
+
2118
+ /* ── execFileSync / execSync ──────────────────────────────────────────
2119
+ * One parameterized core (scr_exec_sync) behind both: posix_spawn(p) +
2120
+ * piped utf8 capture + waitpid, with the option slice portless-class CLIs
2121
+ * actually pass — cwd, env replacement, stdin input, per-fd stdio modes,
2122
+ * and a SIGTERM timeout. Node semantics, matched exactly:
2123
+ * - Non-zero exit (or signal death) THROWS an Error whose message is
2124
+ * Node's checkExecSyncError text: "Command failed: <cmd>" with the
2125
+ * captured stderr appended on its own line when there is any.
2126
+ * - Spawn failure throws "spawnSync <file> ENOENT" (Node routes the
2127
+ * uv_spawn error through the same thrower).
2128
+ * - Timeout kills with SIGTERM and throws "spawnSync <file> ETIMEDOUT".
2129
+ * - Default stdio ECHOES the captured stderr to the parent's stderr after
2130
+ * the child completes (Node's inheritStderr flag — the capture is
2131
+ * unconditional); an explicit stdio option turns that off, and an
2132
+ * "ignore" stderr slot discards it entirely (nothing in the message).
2133
+ * - The thrown error carries the MESSAGE only: Node's status/stdout/
2134
+ * stderr properties don't exist here (SEMANTICS.md — nothing the
2135
+ * supported catch forms can read anyway).
2136
+ * The result is the captured utf8 stdout (+1). */
2137
+
2138
+ typedef struct {
2139
+ bool shell; /* argv = /bin/sh -c <cmd> */
2140
+ const ScrStr *input; /* NULL/absent: stdin from /dev/null */
2141
+ const ScrStr *cwd; /* NULL: inherit */
2142
+ ScrArr *env_pairs; /* NULL: inherit; else [k,v,...] REPLACES environ */
2143
+ double timeout_ms; /* <= 0: none */
2144
+ int stdout_mode; /* 1 capture (default), 0 ignore, 2 inherit */
2145
+ int stderr_mode; /* 0 capture+echo (default), 1 capture, 2 ignore, 3 inherit */
2146
+ bool stdin_inherit; /* stdio "inherit": the child keeps the parent's fd 0 */
2147
+ /* The promisified-execFile shape (Node's async execFile, pinned by
2148
+ * oracle): failure messages read "Command failed: <cmd>\n<stderr>" with
2149
+ * the newline UNCONDITIONAL (the sync form appends only when stderr is
2150
+ * non-empty), spawn failure reads "spawn <file> <ERR>" (not spawnSync)
2151
+ * with `code` stamped, and a timeout is NOT ETIMEDOUT — the SIGTERM
2152
+ * lands and the death reports as an ordinary command failure, exactly
2153
+ * Node's promisified behavior. */
2154
+ bool async_shape;
2155
+ /* async_shape success: the captured stderr string (+1) lands here. */
2156
+ ScrStr **stderr_out;
2157
+ } ScrExecOpts;
2158
+
2159
+ /* Node's message-command: the shell form shows the command string, the
2160
+ * file form joins file + args with spaces. */
2161
+ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) {
2162
+ if (shell) {
2163
+ ScrStr *c = (ScrStr *)scr_arr_get_ref(args, 1); /* ["-c", cmd] */
2164
+ char *out = malloc(c->len + 1);
2165
+ if (!out) scr_child_oom();
2166
+ memcpy(out, c->data, c->len + 1);
2167
+ scr_str_release(c);
2168
+ return out;
2169
+ }
2170
+ size_t n = (size_t)scr_arr_len(args);
2171
+ size_t total = cmd->len;
2172
+ for (size_t i = 0; i < n; i++) {
2173
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
2174
+ total += 1 + s->len;
2175
+ scr_str_release(s);
2176
+ }
2177
+ char *out = malloc(total + 1);
2178
+ if (!out) scr_child_oom();
2179
+ memcpy(out, cmd->data, cmd->len);
2180
+ size_t at = cmd->len;
2181
+ for (size_t i = 0; i < n; i++) {
2182
+ ScrStr *s = (ScrStr *)scr_arr_get_ref(args, (double)i);
2183
+ out[at++] = ' ';
2184
+ memcpy(out + at, s->data, s->len);
2185
+ at += s->len;
2186
+ scr_str_release(s);
2187
+ }
2188
+ out[at] = '\0';
2189
+ return out;
2190
+ }
2191
+
2192
+ /* Builds a NULL-terminated envp from [k,v,...] pairs ("k=v" strings, each
2193
+ * malloc'd). Free with scr_exec_envp_free. */
2194
+ static char **scr_exec_envp(ScrArr *pairs) {
2195
+ size_t n = (size_t)scr_arr_len(pairs) / 2;
2196
+ char **envp = malloc((n + 1) * sizeof(char *));
2197
+ if (!envp) scr_child_oom();
2198
+ for (size_t i = 0; i < n; i++) {
2199
+ ScrStr *k = (ScrStr *)scr_arr_get_ref(pairs, (double)(2 * i));
2200
+ ScrStr *v = (ScrStr *)scr_arr_get_ref(pairs, (double)(2 * i + 1));
2201
+ char *kv = malloc(k->len + 1 + v->len + 1);
2202
+ if (!kv) scr_child_oom();
2203
+ memcpy(kv, k->data, k->len);
2204
+ kv[k->len] = '=';
2205
+ memcpy(kv + k->len + 1, v->data, v->len + 1);
2206
+ envp[i] = kv;
2207
+ scr_str_release(k);
2208
+ scr_str_release(v);
2209
+ }
2210
+ envp[n] = NULL;
2211
+ return envp;
2212
+ }
2213
+
2214
+ static void scr_exec_envp_free(char **envp) {
2215
+ if (!envp) return;
2216
+ for (size_t i = 0; envp[i]; i++) free(envp[i]);
2217
+ free(envp);
2218
+ }
2219
+
2220
+ /* The Command-failed thrower (message only — see the block comment).
2221
+ * always_nl is the async shape: Node's promisified execFile appends
2222
+ * "\n<stderr>" unconditionally (a trailing newline on empty stderr),
2223
+ * where the sync form appends only when stderr is non-empty. */
2224
+ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap,
2225
+ bool with_stderr, bool always_nl) {
2226
+ size_t dlen = strlen(display);
2227
+ size_t elen = with_stderr ? err_cap->len : 0;
2228
+ size_t cap = 16 + dlen + 1 + elen + 1;
2229
+ char *msg = malloc(cap);
2230
+ if (!msg) scr_child_oom();
2231
+ size_t at = (size_t)snprintf(msg, cap, "Command failed: %s", display);
2232
+ if (elen > 0 || always_nl) {
2233
+ msg[at++] = '\n';
2234
+ memcpy(msg + at, err_cap->data, elen);
2235
+ at += elen;
2236
+ }
2237
+ scr_throw_error_msg(SCR_ERR_ERROR, msg, at);
2238
+ free(msg);
2239
+ }
2240
+
2241
+ /* "spawnSync <file> <ERRNAME>" — Node's uv-error message for spawn
2242
+ * failures and timeouts, with `code` = the errno name (Node's
2243
+ * errnoException shape; the Command-failed error stays code-less — Node
2244
+ * puts the exit STATUS there, in properties this surface doesn't carry). */
2245
+ static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) {
2246
+ size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1;
2247
+ char *msg = malloc(cap);
2248
+ if (!msg) scr_child_oom();
2249
+ int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync",
2250
+ cmd->data, errname);
2251
+ scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname);
2252
+ free(msg);
2253
+ }
2254
+
2255
+ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
2256
+ int outfd[2] = {-1, -1}, errfd[2] = {-1, -1}, infd[2] = {-1, -1};
2257
+ bool cap_out = o->stdout_mode == 1;
2258
+ bool cap_err = o->stderr_mode == 0 || o->stderr_mode == 1;
2259
+ if (o->stdout_mode == 2 || o->stderr_mode == 3) {
2260
+ /* Inherited output fds: flush the parent's buffers first so earlier
2261
+ * logs precede the child's writes (Node's practical ordering). */
2262
+ fflush(stdout);
2263
+ fflush(stderr);
2264
+ }
2265
+ bool has_input = o->input != NULL;
2266
+ if ((cap_out && pipe(outfd) != 0) || (cap_err && pipe(errfd) != 0) ||
2267
+ (has_input && pipe(infd) != 0)) {
2268
+ fputs("scriptc: pipe() failed\n", stderr);
2269
+ abort();
2270
+ }
2271
+
2272
+ posix_spawn_file_actions_t fa;
2273
+ posix_spawn_file_actions_init(&fa);
2274
+ if (has_input) {
2275
+ posix_spawn_file_actions_adddup2(&fa, infd[0], 0);
2276
+ posix_spawn_file_actions_addclose(&fa, infd[0]);
2277
+ posix_spawn_file_actions_addclose(&fa, infd[1]);
2278
+ } else if (!o->stdin_inherit) {
2279
+ posix_spawn_file_actions_addopen(&fa, 0, "/dev/null", O_RDONLY, 0);
2280
+ } /* inherit: no action — the child keeps the parent's fd 0 */
2281
+ if (cap_out) {
2282
+ posix_spawn_file_actions_adddup2(&fa, outfd[1], 1);
2283
+ posix_spawn_file_actions_addclose(&fa, outfd[0]);
2284
+ posix_spawn_file_actions_addclose(&fa, outfd[1]);
2285
+ } else if (o->stdout_mode != 2) {
2286
+ posix_spawn_file_actions_addopen(&fa, 1, "/dev/null", O_WRONLY, 0);
2287
+ } /* mode 2 (inherit): the child keeps the parent's fd 1 */
2288
+ if (cap_err) {
2289
+ posix_spawn_file_actions_adddup2(&fa, errfd[1], 2);
2290
+ posix_spawn_file_actions_addclose(&fa, errfd[0]);
2291
+ posix_spawn_file_actions_addclose(&fa, errfd[1]);
2292
+ } else if (o->stderr_mode != 3) {
2293
+ posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0);
2294
+ } /* mode 3 (inherit): the child keeps the parent's fd 2 */
2295
+ #if defined(__APPLE__) || defined(__GLIBC__)
2296
+ if (o->cwd != NULL) {
2297
+ posix_spawn_file_actions_addchdir_np(&fa, o->cwd->data);
2298
+ }
2299
+ #endif
2300
+
2301
+ char **argv = scr_child_argv(cmd, args);
2302
+ char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL;
2303
+ pid_t pid = -1;
2304
+ int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv,
2305
+ envp != NULL ? envp : environ);
2306
+ posix_spawn_file_actions_destroy(&fa);
2307
+ free(argv);
2308
+ scr_exec_envp_free(envp);
2309
+ if (cap_out) close(outfd[1]);
2310
+ if (cap_err) close(errfd[1]);
2311
+ if (has_input) close(infd[0]);
2312
+
2313
+ if (spawn_err != 0) {
2314
+ if (cap_out) close(outfd[0]);
2315
+ if (cap_err) close(errfd[0]);
2316
+ if (has_input) close(infd[1]);
2317
+ /* execSync's ENOENT is the SHELL missing — can't happen; the file
2318
+ * form reports Node's spawnSync error. */
2319
+ scr_exec_throw_spawn(cmd, scr_errname(spawn_err), o->async_shape);
2320
+ return NULL;
2321
+ }
2322
+
2323
+ /* Feed stdin and drain both outputs together (either alone can fill and
2324
+ * deadlock a sequential loop); the poll deadline implements timeout. */
2325
+ if (has_input) fcntl(infd[1], F_SETFL, O_NONBLOCK);
2326
+ ScrCapBuf out, err;
2327
+ scr_cap_init(&out);
2328
+ scr_cap_init(&err);
2329
+ bool out_open = cap_out, err_open = cap_err, in_open = has_input;
2330
+ size_t in_at = 0;
2331
+ bool timed_out = false;
2332
+ double deadline = o->timeout_ms > 0 ? scr_now_ms() + o->timeout_ms : 0;
2333
+ while (out_open || err_open || in_open) {
2334
+ struct pollfd fds[3];
2335
+ nfds_t n = 0;
2336
+ if (out_open) { fds[n].fd = outfd[0]; fds[n].events = POLLIN; n++; }
2337
+ if (err_open) { fds[n].fd = errfd[0]; fds[n].events = POLLIN; n++; }
2338
+ if (in_open) { fds[n].fd = infd[1]; fds[n].events = POLLOUT; n++; }
2339
+ int wait = -1;
2340
+ if (deadline > 0 && !timed_out) {
2341
+ double left = deadline - scr_now_ms();
2342
+ wait = left > 0 ? (int)(left + 0.999) : 0;
2343
+ }
2344
+ int rc = poll(fds, n, wait);
2345
+ if (rc < 0) {
2346
+ if (errno == EINTR) continue;
2347
+ break;
2348
+ }
2349
+ if (deadline > 0 && !timed_out && scr_now_ms() >= deadline) {
2350
+ /* Node's timeout: killSignal (SIGTERM default) to the child; the
2351
+ * capture loop keeps draining until the pipes close. */
2352
+ timed_out = true;
2353
+ kill(pid, SIGTERM);
2354
+ }
2355
+ if (rc == 0) continue;
2356
+ for (nfds_t i = 0; i < n; i++) {
2357
+ if (fds[i].revents == 0) continue;
2358
+ if (out_open && fds[i].fd == outfd[0]) {
2359
+ if (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) out_open = scr_cap_read(&out, outfd[0]);
2360
+ } else if (err_open && fds[i].fd == errfd[0]) {
2361
+ if (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) err_open = scr_cap_read(&err, errfd[0]);
2362
+ } else if (in_open && fds[i].fd == infd[1]) {
2363
+ if (fds[i].revents & (POLLERR | POLLHUP)) {
2364
+ close(infd[1]);
2365
+ in_open = false; /* child closed stdin early: fine, like Node */
2366
+ } else if (fds[i].revents & POLLOUT) {
2367
+ ssize_t wrote = write(infd[1], o->input->data + in_at, o->input->len - in_at);
2368
+ if (wrote > 0) in_at += (size_t)wrote;
2369
+ else if (wrote < 0 && errno != EINTR && errno != EAGAIN) {
2370
+ close(infd[1]);
2371
+ in_open = false;
2372
+ }
2373
+ if (in_at >= o->input->len) {
2374
+ close(infd[1]);
2375
+ in_open = false;
2376
+ }
2377
+ }
2378
+ }
2379
+ }
2380
+ }
2381
+ if (cap_out) close(outfd[0]);
2382
+ if (cap_err) close(errfd[0]);
2383
+ if (in_open) close(infd[1]);
2384
+
2385
+ /* Reap. A live deadline still applies here: a child that closed its
2386
+ * stdio but keeps running (the pipes-drained daemon shape) must still be
2387
+ * killed at the timeout, so the wait polls WNOHANG until then. */
2388
+ int st = 0;
2389
+ pid_t reaped = -1;
2390
+ if (deadline > 0 && !timed_out) {
2391
+ for (;;) {
2392
+ reaped = waitpid(pid, &st, WNOHANG);
2393
+ if (reaped == pid || (reaped < 0 && errno != EINTR)) break;
2394
+ if (scr_now_ms() >= deadline) {
2395
+ timed_out = true;
2396
+ kill(pid, SIGTERM);
2397
+ break;
2398
+ }
2399
+ struct timespec ts = {0, 500000}; /* 0.5ms */
2400
+ nanosleep(&ts, NULL);
2401
+ }
2402
+ }
2403
+ if (reaped != pid) {
2404
+ do {
2405
+ reaped = waitpid(pid, &st, 0);
2406
+ } while (reaped < 0 && errno == EINTR);
2407
+ }
2408
+
2409
+ char *display = NULL;
2410
+ if (timed_out && !o->async_shape) {
2411
+ /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape
2412
+ * reports the SIGTERM death as an ordinary command failure below,
2413
+ * exactly Node's promisified execFile. */
2414
+ free(out.data);
2415
+ free(err.data);
2416
+ scr_exec_throw_spawn(cmd, "ETIMEDOUT", false);
2417
+ return NULL;
2418
+ }
2419
+ bool failed = !WIFEXITED(st) || WEXITSTATUS(st) != 0;
2420
+ /* Node's inheritStderr: no stdio option given → the captured stderr
2421
+ * echoes to the parent's stderr AFTER completion, success or failure. */
2422
+ if (o->stderr_mode == 0 && err.len > 0) {
2423
+ fflush(stdout);
2424
+ fwrite(err.data, 1, err.len, stderr);
2425
+ }
2426
+ if (failed) {
2427
+ display = scr_exec_display(cmd, args, o->shell);
2428
+ scr_exec_throw_failed(display, &err, cap_err, o->async_shape);
2429
+ free(display);
2430
+ free(out.data);
2431
+ free(err.data);
2432
+ return NULL;
2433
+ }
2434
+ if (o->async_shape && o->stderr_out != NULL) {
2435
+ *o->stderr_out = scr_str_new(err.data, err.len);
2436
+ }
2437
+ ScrStr *out_s = scr_str_new(out.data, out.len);
2438
+ free(out.data);
2439
+ free(err.data);
2440
+ return out_s;
2441
+ }
2442
+
2443
+ /* The libCall entry: modes as doubles (the IR's scalar), input as string +
2444
+ * presence flag (Node's exact member reading: an absent/undefined input
2445
+ * means NO stdin pipe — /dev/null — while "" pipes EMPTY stdin, the child
2446
+ * reading immediate EOF from the pipe), absent cwd as "" ("" is never a
2447
+ * valid cwd), env as has_env + pairs (an EMPTY env option is legal Node —
2448
+ * it means an empty environment). */
2449
+ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
2450
+ bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
2451
+ double timeout_ms, double stdout_mode, double stderr_mode) {
2452
+ ScrExecOpts o = {
2453
+ .shell = shell,
2454
+ .input = has_input ? input : NULL,
2455
+ .cwd = cwd->len > 0 ? cwd : NULL,
2456
+ .env_pairs = has_env ? env_pairs : NULL,
2457
+ .timeout_ms = timeout_ms,
2458
+ /* stdio "inherit"'s stdin rides bit 4 of stdout_mode (the libCall
2459
+ * signature predates the mode; see scr_runtime.h). */
2460
+ .stdout_mode = (int)stdout_mode & 3,
2461
+ .stderr_mode = (int)stderr_mode,
2462
+ .stdin_inherit = ((int)stdout_mode & 4) != 0,
2463
+ };
2464
+ return scr_exec_sync_core(cmd, args, &o);
2465
+ }
2466
+
2467
+ /* The promisified-execFile capture (cp.execCapture): the SAME exec core
2468
+ * in the async shape — both streams captured (no echo), Node's async
2469
+ * messages on the throw paths (the compiler's interned async helper turns
2470
+ * the throw into the rejection). Success answers a +1 ScrSpawnRes reusing
2471
+ * the spawnSync result container (status unused — a failure threw). */
2472
+ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, ScrStr *cwd,
2473
+ bool has_env, ScrArr *env_pairs, double timeout_ms) {
2474
+ ScrStr *err_s = NULL;
2475
+ ScrExecOpts o = {
2476
+ .shell = false,
2477
+ .input = NULL,
2478
+ .cwd = cwd->len > 0 ? cwd : NULL,
2479
+ .env_pairs = has_env ? env_pairs : NULL,
2480
+ .timeout_ms = timeout_ms,
2481
+ .stdout_mode = 1,
2482
+ .stderr_mode = 1,
2483
+ .async_shape = true,
2484
+ .stderr_out = &err_s,
2485
+ };
2486
+ ScrStr *out_s = scr_exec_sync_core(cmd, args, &o);
2487
+ if (out_s == NULL) return NULL; /* exception pending; dummy result */
2488
+ return scr_spawn_res_new(true, 0, out_s, err_s);
2489
+ }
2490
+
2491
+ /* ── spawn: the asynchronous child + its event registry ──────────────────
2492
+ *
2493
+ * spawn(cmd, args, { stdio: "ignore" }) starts the child IMMEDIATELY
2494
+ * (posix_spawnp, all three stdio fds on /dev/null) and registers it with
2495
+ * the loop: scr_async.c polls scr_children_poll() at quiescence, which
2496
+ * waitpid(WNOHANG)s every running child and fires listeners when one is
2497
+ * reaped. Node semantics, matched exactly:
2498
+ * - "exit" fires once with the exit code, or NO code (→ the null arm)
2499
+ * when the child died to a signal.
2500
+ * - "error" fires ONLY for spawn failure (the binary could not be
2501
+ * spawned); a spawn failure never fires "exit".
2502
+ * - An "error" event with no registered listener prints the error and
2503
+ * exits 1 (the unhandled-'error' EventEmitter behavior).
2504
+ * - The loop keeps the process alive until every child is reaped —
2505
+ * scr_children_pending() is part of its exhaustion test.
2506
+ * Wakeup: every spawned child is registered with a kqueue PROC filter
2507
+ * (NOTE_EXIT), and the loop's quiescent sleep waits on that kqueue
2508
+ * (scr_children_wait) so an exit wakes it immediately — the reap itself
2509
+ * stays the WNOHANG sweep below, which the wakeup merely triggers. When
2510
+ * kqueue is unavailable (non-kqueue platforms, or a child whose filter
2511
+ * registration failed because it already exited), the loop falls back to
2512
+ * the original ~1ms polling cap until that child settles.
2513
+ *
2514
+ * Ownership/cycles: the child holds its listeners (+1 each, moved in)
2515
+ * only until the terminal event fires — settling releases every
2516
+ * listener, so a listener capturing its own child cannot cycle past the
2517
+ * reap, and children allocate lean (no trace header). A listener
2518
+ * registered after settling is released immediately and never fires
2519
+ * (Node: listeners added after 'exit' emitted don't fire). */
2520
+
2521
+ typedef enum {
2522
+ SCR_CHILD_RUNNING = 0,
2523
+ SCR_CHILD_EXITED = 1, /* reaped; has_code/code valid */
2524
+ SCR_CHILD_SPAWN_FAILED = 2 /* err_msg valid */
2525
+ } ScrChildState;
2526
+
2527
+ typedef struct {
2528
+ ScrClosure *cb;
2529
+ ScrChildExitFn fn;
2530
+ } ScrChildExitEntry;
2531
+
2532
+ typedef struct {
2533
+ ScrClosure *cb;
2534
+ ScrChildErrFn fn;
2535
+ } ScrChildErrEntry;
2536
+
2537
+ /* ── piped child output (child.stdout / child.stderr) ────────────────────
2538
+ *
2539
+ * spawn with stdio ["ignore", "pipe", "pipe"] gives the child fresh pipes
2540
+ * for fds 1/2 and the parent a ScrChildStream handle per piped fd —
2541
+ * refcounted like the child itself (the child holds one reference so
2542
+ * child.stdout answers for the handle's whole life; the service registry
2543
+ * holds another until EOF). The stdin-consumer pattern in reverse:
2544
+ * - The pipe's read end is NONBLOCKING and read ONLY while a 'data'
2545
+ * listener exists (no consumer = no read: the pipe itself is the
2546
+ * buffer, backpressure exactly like a paused Node stream).
2547
+ * - One service pass reads to EAGAIN/EOF, firing one 'data' per read(2)
2548
+ * chunk (≤64KB — libuv's pipe reads chunk the same way, pinned against
2549
+ * Node's out(65536)... sequence).
2550
+ * - EOF fires 'end' once, drops the listeners, closes the fd, and leaves
2551
+ * the registry; nothing fires after (listeners registered post-'end'
2552
+ * release immediately — the stdin slice's rule).
2553
+ * - ORDERING, pinned against Node: a child's stream 'end' events fire
2554
+ * BEFORE its 'exit' — the settle path drains consumer-owning streams
2555
+ * to EOF/EAGAIN before the exit listeners run. EAGAIN with the child
2556
+ * reaped means another process still holds the write end (a detached
2557
+ * grandchild): 'exit' fires and the stream keeps delivering on later
2558
+ * turns, exactly Node.
2559
+ * - LIVENESS: a stream with a 'data' consumer that has not hit EOF holds
2560
+ * the loop alive (Node's flowing-pipe keep-alive); 'end' listeners
2561
+ * alone do not. Wakeups ride the child kqueue: the read end is armed
2562
+ * LEVEL-TRIGGERED (no EV_CLEAR — scr_children_wait's drain discards
2563
+ * events, and a level filter keeps the kqueue readable while unread
2564
+ * data sits in the pipe, so no wakeup is ever lost) when the first
2565
+ * consumer attaches; an arm failure counts into the unwatched-children
2566
+ * fallback (the ~1ms polling cap). */
2567
+
2568
+ typedef struct {
2569
+ ScrClosure *cb; /* owned */
2570
+ ScrChildStreamDataFn fn;
2571
+ bool once;
2572
+ } ScrChildStreamDataL;
2573
+
2574
+ typedef struct {
2575
+ ScrClosure *cb; /* owned */
2576
+ bool once;
2577
+ } ScrChildStreamEndL;
2578
+
2579
+ struct ScrChildStream {
2580
+ size_t rc;
2581
+ int fd; /* the pipe's read end; -1 after EOF or spawn failure */
2582
+ bool eof; /* 'end' delivered (or the stream never opened) */
2583
+ bool armed; /* EVFILT_READ registered on the child kqueue */
2584
+ bool uncounted; /* counted into scr_children_unwatched (arm failure) */
2585
+ ScrChildStreamDataL *data_ls;
2586
+ size_t n_data, cap_data;
2587
+ ScrChildStreamEndL *end_ls;
2588
+ size_t n_end, cap_end;
2589
+ struct ScrChildStream *next; /* the service registry (+1) */
2590
+ };
2591
+
2592
+ static ScrChildStream *scr_child_streams = NULL;
2593
+ /* Streams with a live consumer (data listener, not yet EOF): the loop's
2594
+ * keep-alive and service predicate. */
2595
+ static size_t scr_child_streams_watching = 0;
2596
+
2597
+ struct ScrChild {
2598
+ size_t rc;
2599
+ pid_t pid;
2600
+ ScrChildState state;
2601
+ bool settled; /* terminal event delivered; listeners released */
2602
+ bool unwatched; /* no kqueue NOTE_EXIT armed: the loop must poll for it */
2603
+ bool has_code;
2604
+ bool killed; /* a kill() successfully sent a signal (Node's flag) */
2605
+ bool reffed; /* keeps the loop alive; unref() clears it (Node) */
2606
+ int spawn_errno; /* spawn failure only: Node's exitCode is -errno there */
2607
+ double code;
2608
+ /* Signal death only: the terminating signal's name (static storage) —
2609
+ * Node's second exit-listener parameter. NULL for a normal exit. */
2610
+ const char *exit_signal;
2611
+ ScrStr *err_msg; /* spawn failure only: "spawn <cmd> <ERRNAME>" */
2612
+ ScrChildExitEntry *exit_cbs;
2613
+ size_t n_exit, cap_exit;
2614
+ ScrChildErrEntry *err_cbs;
2615
+ size_t n_err, cap_err;
2616
+ /* Piped stdio (stdio mode 3): the stream handles child.stdout /
2617
+ * child.stderr answer — owned (+1 each), NULL when not piped. */
2618
+ ScrChildStream *out_stream;
2619
+ ScrChildStream *err_stream;
2620
+ struct ScrChild *next; /* the pending registry */
2621
+ };
2622
+
2623
+ static ScrChild *scr_children = NULL; /* unsettled children (registry +1) */
2624
+ static size_t scr_children_reffed_n = 0; /* unsettled AND reffed (liveness) */
2625
+
2626
+ /* Children the kqueue can NOT wake the loop for (spawn failures awaiting
2627
+ * their first-pass settle, exit-filter registration failures, every child
2628
+ * on a non-kqueue platform): while any exist, scr_children_wait refuses
2629
+ * and the loop keeps the polling cap for them. */
2630
+ static size_t scr_children_unwatched = 0;
2631
+
2632
+ #ifdef SCR_HAVE_KQUEUE
2633
+ static int scr_child_kq = -1; /* created with the first spawn; lives forever */
2634
+
2635
+ /* Arms NOTE_EXIT for a freshly spawned pid. False = the child cannot be
2636
+ * watched (it already exited and EVFILT_PROC refuses zombies — the very
2637
+ * next WNOHANG sweep reaps it). EV_ONESHOT: the kernel drops the filter at
2638
+ * delivery; a reaped-before-consumed event is a harmless spurious wakeup. */
2639
+ static bool scr_child_watch(pid_t pid) {
2640
+ if (scr_child_kq < 0) {
2641
+ scr_child_kq = kqueue();
2642
+ if (scr_child_kq < 0) return false;
2643
+ }
2644
+ struct kevent ev;
2645
+ EV_SET(&ev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, NULL);
2646
+ struct timespec zero = {0, 0};
2647
+ return kevent(scr_child_kq, &ev, 1, NULL, 0, &zero) == 0;
2648
+ }
2649
+
2650
+ /* The reap already consumed the wakeup — EV_ONESHOT dropped the filter at
2651
+ * delivery, so there is nothing to release. */
2652
+ static void scr_child_unwatch(pid_t pid) { (void)pid; }
2653
+ #endif
2654
+
2655
+ #ifdef SCR_CHILD_HAVE_PIDFD
2656
+ static int scr_child_ep = -1; /* created with the first spawn; lives forever */
2657
+
2658
+ /* pid -> pidfd, so the reap can CLOSE the pidfd (it polls readable from
2659
+ * exit until closed — kqueue's ONESHOT drop has no epoll analogue). A
2660
+ * handful of entries at the corpus's scale; linear is fine. */
2661
+ typedef struct {
2662
+ pid_t pid;
2663
+ int pidfd;
2664
+ } ScrChildPidfd;
2665
+
2666
+ static ScrChildPidfd *scr_child_pidfds = NULL;
2667
+ static size_t scr_child_pidfds_n = 0, scr_child_pidfds_cap = 0;
2668
+
2669
+ /* Arms an exit wakeup for a freshly spawned pid: pidfd_open registered
2670
+ * EPOLLIN. False = the child cannot be watched (ENOSYS on pre-5.3
2671
+ * kernels, EMFILE) — the caller keeps the polling cap for it. A pidfd
2672
+ * opens fine on a zombie and polls readable at once, so the unwatched
2673
+ * set is rarer here than kqueue's refuses-zombies case. */
2674
+ static bool scr_child_watch(pid_t pid) {
2675
+ if (scr_child_ep < 0) {
2676
+ scr_child_ep = epoll_create1(EPOLL_CLOEXEC);
2677
+ if (scr_child_ep < 0) return false;
2678
+ }
2679
+ int pfd = pidfd_open(pid, 0);
2680
+ if (pfd < 0) return false;
2681
+ if (scr_child_pidfds_n == scr_child_pidfds_cap) {
2682
+ size_t cap = scr_child_pidfds_cap == 0 ? 8 : scr_child_pidfds_cap * 2;
2683
+ ScrChildPidfd *grown = realloc(scr_child_pidfds, cap * sizeof *grown);
2684
+ if (grown == NULL) {
2685
+ close(pfd);
2686
+ return false;
2687
+ }
2688
+ scr_child_pidfds = grown;
2689
+ scr_child_pidfds_cap = cap;
2690
+ }
2691
+ struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = pfd}};
2692
+ if (epoll_ctl(scr_child_ep, EPOLL_CTL_ADD, pfd, &ev) != 0) {
2693
+ close(pfd);
2694
+ return false;
2695
+ }
2696
+ scr_child_pidfds[scr_child_pidfds_n++] = (ScrChildPidfd){pid, pfd};
2697
+ return true;
2698
+ }
2699
+
2700
+ /* After waitpid reaped the pid: deregister and close its pidfd, or it
2701
+ * would keep the epoll readable forever. */
2702
+ static void scr_child_unwatch(pid_t pid) {
2703
+ for (size_t i = 0; i < scr_child_pidfds_n; i++) {
2704
+ if (scr_child_pidfds[i].pid != pid) continue;
2705
+ (void)epoll_ctl(scr_child_ep, EPOLL_CTL_DEL, scr_child_pidfds[i].pidfd, NULL);
2706
+ close(scr_child_pidfds[i].pidfd);
2707
+ scr_child_pidfds[i] = scr_child_pidfds[scr_child_pidfds_n - 1];
2708
+ scr_child_pidfds_n--;
2709
+ return;
2710
+ }
2711
+ }
2712
+ #endif
2713
+
2714
+ #if !defined(SCR_HAVE_KQUEUE) && !defined(SCR_CHILD_HAVE_PIDFD)
2715
+ static void scr_child_unwatch(pid_t pid) { (void)pid; }
2716
+ #endif
2717
+
2718
+ /* The loop's quiescent sleep while children are pending: waits on the
2719
+ * kqueue up to max_wait_ms (the next timer deadline) — a child's NOTE_EXIT
2720
+ * wakes it immediately, and the caller's next reap pass settles the child.
2721
+ * Returns false when this can't wake for every pending child (no kqueue,
2722
+ * or an unwatched child exists): the caller falls back to the ~1ms polling
2723
+ * cap instead. Events are drained and DISCARDED — they are wakeups only;
2724
+ * the WNOHANG sweep stays the single reaping mechanism. */
2725
+ /* The pollable child-exit wake fd for the loop's poll(2) sleep (a kqueue
2726
+ * fd reads as ready while events are pending): valid only when the kqueue
2727
+ * can wake for EVERY pending child — same condition as scr_children_wait.
2728
+ * -1 tells the caller to keep the ~1ms reap cap instead. */
2729
+ int scr_children_wake_fd(void) {
2730
+ #ifdef SCR_HAVE_KQUEUE
2731
+ if (scr_child_kq < 0 || scr_children_unwatched > 0) return -1;
2732
+ return scr_child_kq;
2733
+ #elif defined(SCR_CHILD_HAVE_PIDFD)
2734
+ if (scr_child_ep < 0 || scr_children_unwatched > 0) return -1;
2735
+ return scr_child_ep; /* readable while any pidfd/stream event pends */
2736
+ #else
2737
+ return -1;
2738
+ #endif
2739
+ }
2740
+
2741
+ bool scr_children_wait(double max_wait_ms) {
2742
+ #ifdef SCR_HAVE_KQUEUE
2743
+ if (scr_child_kq < 0 || scr_children_unwatched > 0) return false;
2744
+ if (!(max_wait_ms > 0)) max_wait_ms = 0;
2745
+ struct timespec ts = {(time_t)(max_wait_ms / 1000.0),
2746
+ (long)((max_wait_ms - (double)((time_t)(max_wait_ms / 1000.0)) * 1000.0) * 1e6)};
2747
+ struct kevent evs[8];
2748
+ (void)kevent(scr_child_kq, NULL, 0, evs, 8, &ts); /* EINTR: spurious pass */
2749
+ return true;
2750
+ #elif defined(SCR_CHILD_HAVE_PIDFD)
2751
+ if (scr_child_ep < 0 || scr_children_unwatched > 0) return false;
2752
+ if (!(max_wait_ms > 0)) max_wait_ms = 0;
2753
+ double capped = max_wait_ms > 2147483000.0 ? 2147483000.0 : max_wait_ms;
2754
+ struct epoll_event evs[8];
2755
+ /* Events are wakeups only, discarded here exactly like the kqueue
2756
+ * drain; level-triggered pidfds stay readable until the reap closes
2757
+ * them, so nothing is lost. EINTR: a spurious pass. */
2758
+ (void)epoll_wait(scr_child_ep, evs, 8, (int)(capped + 0.999));
2759
+ return true;
2760
+ #else
2761
+ (void)max_wait_ms;
2762
+ return false;
2763
+ #endif
2764
+ }
2765
+
2766
+ static void scr_child_drop_listeners(ScrChild *c) {
2767
+ for (size_t i = 0; i < c->n_exit; i++) scr_closure_release(c->exit_cbs[i].cb);
2768
+ for (size_t i = 0; i < c->n_err; i++) scr_closure_release(c->err_cbs[i].cb);
2769
+ free(c->exit_cbs);
2770
+ free(c->err_cbs);
2771
+ c->exit_cbs = NULL;
2772
+ c->err_cbs = NULL;
2773
+ c->n_exit = c->n_err = c->cap_exit = c->cap_err = 0;
2774
+ }
2775
+
2776
+ ScrChild *scr_child_retain(ScrChild *c) {
2777
+ if (c->rc != SIZE_MAX) c->rc++;
2778
+ return c;
2779
+ }
2780
+
2781
+ void scr_child_release(ScrChild *c) {
2782
+ if (!c || c->rc == SIZE_MAX) return;
2783
+ if (--c->rc == 0) {
2784
+ scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */
2785
+ scr_str_release(c->err_msg);
2786
+ scr_child_stream_release(c->out_stream);
2787
+ scr_child_stream_release(c->err_stream);
2788
+ free(c);
2789
+ }
2790
+ }
2791
+
2792
+ void *scr_child_retain_v(void *p) { return scr_child_retain((ScrChild *)p); }
2793
+ void scr_child_release_v(void *p) { scr_child_release((ScrChild *)p); }
2794
+
2795
+ /* Node's error-code names for the spawn failures a posix_spawnp can
2796
+ * report (libuv reports the same POSIX names). */
2797
+ static const char *scr_errname(int err) {
2798
+ switch (err) {
2799
+ case ENOENT: return "ENOENT";
2800
+ case EACCES: return "EACCES";
2801
+ case EPERM: return "EPERM";
2802
+ case ENOTDIR: return "ENOTDIR";
2803
+ case ENOEXEC: return "ENOEXEC";
2804
+ case ELOOP: return "ELOOP";
2805
+ case ENAMETOOLONG: return "ENAMETOOLONG";
2806
+ case E2BIG: return "E2BIG";
2807
+ case ENOMEM: return "ENOMEM";
2808
+ case EAGAIN: return "EAGAIN";
2809
+ case EIO: return "EIO";
2810
+ case EINVAL: return "EINVAL";
2811
+ default: return "EUNKNOWN";
2812
+ }
2813
+ }
2814
+
2815
+ /* ── the stream slice's implementation ─────────────────────────────── */
2816
+
2817
+ ScrChildStream *scr_child_stream_retain(ScrChildStream *s) {
2818
+ if (s->rc != SIZE_MAX) s->rc++;
2819
+ return s;
2820
+ }
2821
+
2822
+ static void scr_child_stream_drop_listeners(ScrChildStream *s) {
2823
+ for (size_t i = 0; i < s->n_data; i++) scr_closure_release(s->data_ls[i].cb);
2824
+ for (size_t i = 0; i < s->n_end; i++) scr_closure_release(s->end_ls[i].cb);
2825
+ free(s->data_ls);
2826
+ free(s->end_ls);
2827
+ s->data_ls = NULL;
2828
+ s->end_ls = NULL;
2829
+ s->n_data = s->n_end = s->cap_data = s->cap_end = 0;
2830
+ }
2831
+
2832
+ void scr_child_stream_release(ScrChildStream *s) {
2833
+ if (!s || s->rc == SIZE_MAX) return;
2834
+ if (--s->rc == 0) {
2835
+ scr_child_stream_drop_listeners(s); /* only reachable pre-'end' via leaks */
2836
+ if (s->fd >= 0) close(s->fd);
2837
+ free(s);
2838
+ }
2839
+ }
2840
+
2841
+ void *scr_child_stream_retain_v(void *p) { return scr_child_stream_retain((ScrChildStream *)p); }
2842
+ void scr_child_stream_release_v(void *p) { scr_child_stream_release((ScrChildStream *)p); }
2843
+
2844
+ static void scr_child_stream_dearm(ScrChildStream *s);
2845
+
2846
+ /* True while the stream has a consumer and can still deliver. */
2847
+ static bool scr_child_stream_watching(const ScrChildStream *s) {
2848
+ return !s->eof && s->fd >= 0 && s->n_data > 0;
2849
+ }
2850
+
2851
+ /* A fresh piped stream over the pipe's read end (nonblocking, cloexec);
2852
+ * registered with the service registry (+1). fd < 0 = the never-opened
2853
+ * husk a failed spawn hands out (eof from birth, never registered). */
2854
+ static ScrChildStream *scr_child_stream_new(int fd) {
2855
+ ScrChildStream *s = calloc(1, sizeof *s);
2856
+ if (!s) scr_child_oom();
2857
+ s->rc = 1;
2858
+ s->fd = fd;
2859
+ if (fd < 0) {
2860
+ s->eof = true;
2861
+ return s;
2862
+ }
2863
+ fcntl(fd, F_SETFL, O_NONBLOCK);
2864
+ fcntl(fd, F_SETFD, FD_CLOEXEC);
2865
+ s->next = scr_child_streams;
2866
+ scr_child_streams = scr_child_stream_retain(s);
2867
+ return s;
2868
+ }
2869
+
2870
+ /* EOF/error tail: 'end' fires over a snapshot, everything drops, the fd
2871
+ * closes (deleting its kqueue filter), and the liveness/fallback counters
2872
+ * settle. The caller unlinks from the registry and releases its ref. */
2873
+ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) {
2874
+ if (scr_child_stream_watching(s)) scr_child_streams_watching--;
2875
+ if (s->uncounted) {
2876
+ s->uncounted = false;
2877
+ scr_children_unwatched--;
2878
+ }
2879
+ s->eof = true;
2880
+ if (s->fd >= 0) {
2881
+ close(s->fd);
2882
+ s->fd = -1;
2883
+ }
2884
+ if (fire_end) {
2885
+ size_t n = s->n_end;
2886
+ ScrChildStreamEndL *snap = malloc(n * sizeof *snap);
2887
+ if (!snap) scr_child_oom();
2888
+ for (size_t i = 0; i < n; i++) {
2889
+ snap[i] = s->end_ls[i];
2890
+ scr_closure_retain(snap[i].cb);
2891
+ }
2892
+ for (size_t i = 0; i < n; i++) {
2893
+ if (!scr_exc_pending()) {
2894
+ ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
2895
+ }
2896
+ scr_closure_release(snap[i].cb);
2897
+ }
2898
+ free(snap);
2899
+ }
2900
+ scr_child_stream_drop_listeners(s);
2901
+ }
2902
+
2903
+ /* One read pump: while a consumer exists, read(2) to EAGAIN/EOF, one
2904
+ * 'data' per chunk (snapshot; `once` entries leave the live list before
2905
+ * running — the stdin service's discipline). Returns true when the
2906
+ * stream hit EOF (or a read error — piped read errors are not
2907
+ * meaningfully reachable, treated as EOF like the stdin slice's
2908
+ * no-listener path): the caller finishes and unlinks it. */
2909
+ static bool scr_child_stream_pump(ScrChildStream *s) {
2910
+ while (scr_child_stream_watching(s)) {
2911
+ char buf[65536];
2912
+ ssize_t n = read(s->fd, buf, sizeof buf);
2913
+ if (n < 0) {
2914
+ if (errno == EINTR) continue;
2915
+ return errno != EAGAIN;
2916
+ }
2917
+ if (n == 0) return true;
2918
+ ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
2919
+ memcpy(chunk->data, buf, (size_t)n);
2920
+ size_t nd = s->n_data;
2921
+ ScrChildStreamDataL *snap = malloc(nd * sizeof *snap);
2922
+ if (!snap) scr_child_oom();
2923
+ for (size_t i = 0; i < nd; i++) {
2924
+ snap[i] = s->data_ls[i];
2925
+ scr_closure_retain(snap[i].cb);
2926
+ }
2927
+ for (size_t i = 0; i < nd; i++) {
2928
+ if (snap[i].once) {
2929
+ for (size_t j = 0; j < s->n_data; j++) {
2930
+ if (s->data_ls[j].cb == snap[i].cb) {
2931
+ scr_closure_release(s->data_ls[j].cb);
2932
+ memmove(s->data_ls + j, s->data_ls + j + 1,
2933
+ (s->n_data - j - 1) * sizeof *s->data_ls);
2934
+ s->n_data--;
2935
+ if (s->n_data == 0) {
2936
+ scr_child_streams_watching--;
2937
+ scr_child_stream_dearm(s);
2938
+ if (s->uncounted) {
2939
+ s->uncounted = false;
2940
+ scr_children_unwatched--;
2941
+ }
2942
+ }
2943
+ break;
2944
+ }
2945
+ }
2946
+ }
2947
+ if (!scr_exc_pending()) snap[i].fn(snap[i].cb, chunk);
2948
+ scr_closure_release(snap[i].cb);
2949
+ }
2950
+ free(snap);
2951
+ scr_bytes_release(chunk);
2952
+ if (scr_exc_pending()) return false;
2953
+ }
2954
+ return false;
2955
+ }
2956
+
2957
+ /* The registry's service walk: every watched stream pumps; EOF finishes
2958
+ * and unlinks (the children_poll unlink-before-fire pattern). */
2959
+ static void scr_child_streams_service(void) {
2960
+ ScrChildStream **link = &scr_child_streams;
2961
+ while (*link) {
2962
+ ScrChildStream *s = *link;
2963
+ bool ended = scr_child_stream_pump(s);
2964
+ if (ended) {
2965
+ *link = s->next;
2966
+ s->next = NULL;
2967
+ scr_child_stream_finish(s, true);
2968
+ scr_child_stream_release(s); /* the registry's reference */
2969
+ } else {
2970
+ link = &s->next;
2971
+ }
2972
+ if (scr_exc_pending()) return;
2973
+ }
2974
+ }
2975
+
2976
+ /* The settle-path drain (Node's pinned ordering: stream 'end' before
2977
+ * 'exit'): pump one stream to EOF/EAGAIN right now; on EOF, finish it
2978
+ * and unlink it from the registry. EAGAIN with the child reaped means a
2979
+ * grandchild still holds the write end — 'exit' proceeds and the stream
2980
+ * keeps delivering on later turns, exactly Node. */
2981
+ static void scr_child_stream_drain_now(ScrChildStream *s) {
2982
+ if (s == NULL || !scr_child_stream_watching(s)) return;
2983
+ if (!scr_child_stream_pump(s)) return;
2984
+ ScrChildStream **link = &scr_child_streams;
2985
+ while (*link && *link != s) link = &(*link)->next;
2986
+ if (*link) {
2987
+ *link = s->next;
2988
+ s->next = NULL;
2989
+ scr_child_stream_finish(s, true);
2990
+ scr_child_stream_release(s);
2991
+ } else {
2992
+ scr_child_stream_finish(s, true); /* defensive: not registered */
2993
+ }
2994
+ }
2995
+
2996
+ /* Drops the read filter when the LAST consumer leaves (a once-listener
2997
+ * removal): a level-triggered filter over an unread pipe would otherwise
2998
+ * wake every idle sleep for data nobody consumes. */
2999
+ static void scr_child_stream_dearm(ScrChildStream *s) {
3000
+ if (!s->armed) return;
3001
+ s->armed = false;
3002
+ #ifdef SCR_HAVE_KQUEUE
3003
+ if (scr_child_kq >= 0 && s->fd >= 0) {
3004
+ struct kevent ev;
3005
+ EV_SET(&ev, (uintptr_t)s->fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
3006
+ struct timespec zero = {0, 0};
3007
+ (void)kevent(scr_child_kq, &ev, 1, NULL, 0, &zero);
3008
+ }
3009
+ #elif defined(SCR_CHILD_HAVE_PIDFD)
3010
+ if (scr_child_ep >= 0 && s->fd >= 0) {
3011
+ (void)epoll_ctl(scr_child_ep, EPOLL_CTL_DEL, s->fd, NULL);
3012
+ }
3013
+ #endif
3014
+ }
3015
+
3016
+ /* Arms the level-triggered read filter for a consumer-owning stream (see
3017
+ * the design note: no EV_CLEAR — the wait's drain discards events). An
3018
+ * arm failure joins the unwatched fallback so the loop keeps the ~1ms
3019
+ * polling cap for it. */
3020
+ static void scr_child_stream_arm(ScrChildStream *s) {
3021
+ if (s->armed || s->fd < 0) return;
3022
+ #ifdef SCR_HAVE_KQUEUE
3023
+ if (scr_child_kq < 0) scr_child_kq = kqueue();
3024
+ if (scr_child_kq >= 0) {
3025
+ struct kevent ev;
3026
+ EV_SET(&ev, (uintptr_t)s->fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
3027
+ struct timespec zero = {0, 0};
3028
+ if (kevent(scr_child_kq, &ev, 1, NULL, 0, &zero) == 0) {
3029
+ s->armed = true;
3030
+ return;
3031
+ }
3032
+ }
3033
+ #elif defined(SCR_CHILD_HAVE_PIDFD)
3034
+ /* Level-triggered EPOLLIN, exactly the kqueue arm's level filter: the
3035
+ * wait's drain discards events, and level readiness keeps the epoll fd
3036
+ * readable while unread data sits in the pipe — no wakeup is lost. */
3037
+ if (scr_child_ep < 0) scr_child_ep = epoll_create1(EPOLL_CLOEXEC);
3038
+ if (scr_child_ep >= 0) {
3039
+ struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = s->fd}};
3040
+ if (epoll_ctl(scr_child_ep, EPOLL_CTL_ADD, s->fd, &ev) == 0) {
3041
+ s->armed = true;
3042
+ return;
3043
+ }
3044
+ }
3045
+ #endif
3046
+ if (!s->uncounted) {
3047
+ s->uncounted = true;
3048
+ scr_children_unwatched++;
3049
+ }
3050
+ }
3051
+
3052
+ /* stream.on/once("data", cb): the consumer that makes the pipe flow (and
3053
+ * keeps the loop alive). Post-'end' registrations release immediately
3054
+ * and never fire, the stdin rule. */
3055
+ void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
3056
+ ScrChildStreamDataFn fn, bool once) {
3057
+ if (s->eof || s->fd < 0) {
3058
+ scr_closure_release(cb);
3059
+ return;
3060
+ }
3061
+ if (s->n_data == s->cap_data) {
3062
+ s->cap_data = s->cap_data ? s->cap_data * 2 : 2;
3063
+ s->data_ls = realloc(s->data_ls, s->cap_data * sizeof *s->data_ls);
3064
+ if (!s->data_ls) scr_child_oom();
3065
+ }
3066
+ s->data_ls[s->n_data].cb = cb;
3067
+ s->data_ls[s->n_data].fn = fn;
3068
+ s->data_ls[s->n_data].once = once;
3069
+ s->n_data++;
3070
+ if (s->n_data == 1) {
3071
+ scr_child_streams_watching++;
3072
+ scr_child_stream_arm(s);
3073
+ }
3074
+ }
3075
+
3076
+ /* stream.on/once("end", cb): fires once at EOF; alone it neither reads
3077
+ * nor keeps the loop alive (the stdin rule). */
3078
+ void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool once) {
3079
+ (void)once; /* 'end' fires at most once; the flag changes nothing */
3080
+ if (s->eof || s->fd < 0) {
3081
+ scr_closure_release(cb);
3082
+ return;
3083
+ }
3084
+ if (s->n_end == s->cap_end) {
3085
+ s->cap_end = s->cap_end ? s->cap_end * 2 : 2;
3086
+ s->end_ls = realloc(s->end_ls, s->cap_end * sizeof *s->end_ls);
3087
+ if (!s->end_ls) scr_child_oom();
3088
+ }
3089
+ s->end_ls[s->n_end].cb = cb;
3090
+ s->end_ls[s->n_end].once = once;
3091
+ s->n_end++;
3092
+ }
3093
+
3094
+ /* child.stdout / child.stderr — +1 handle, or NULL when the slot was not
3095
+ * piped (Node's null there). */
3096
+ ScrChildStream *scr_child_stdout(ScrChild *c) {
3097
+ return c->out_stream ? scr_child_stream_retain(c->out_stream) : NULL;
3098
+ }
3099
+ ScrChildStream *scr_child_stderr(ScrChild *c) {
3100
+ return c->err_stream ? scr_child_stream_retain(c->err_stream) : NULL;
3101
+ }
3102
+
3103
+ /* The runtime-provided data adapters (the stdin pair's shapes — local to
3104
+ * this unit so piped children never require scr_events.c to link). */
3105
+ void scr_child_stream_thunk0(ScrClosure *cb, ScrBytes *chunk) {
3106
+ (void)chunk;
3107
+ ((void (*)(ScrClosure *))cb->fn)(cb);
3108
+ }
3109
+ void scr_child_stream_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) {
3110
+ /* The listener owns its +1 param per the universal convention. */
3111
+ ((void (*)(ScrClosure *, ScrBytes *))cb->fn)(cb, scr_bytes_retain(chunk));
3112
+ }
3113
+
3114
+ ScrChild *scr_spawn(ScrStr *cmd, ScrArr *args) {
3115
+ return scr_spawn_opts(cmd, args, 0, 0, 0, 0, 0, false, false, NULL, NULL);
3116
+ }
3117
+
3118
+ /* The options core behind cp.spawn/cp.spawnOpts. PER-SLOT stdio modes:
3119
+ * 0 = ignore (/dev/null), 1 = inherit (the parent's fd), 2 = fd (out/err
3120
+ * only — out_fd/err_fd dup2 into the child's slot, Node's stdio fd form:
3121
+ * the daemon-log idiom ["ignore", logFd, logFd]), 3 = pipe (out/err only
3122
+ * — a fresh pipe whose read end becomes the child.stdout/child.stderr
3123
+ * stream; see the stream slice's design note). detached gives the
3124
+ * child its own session (POSIX_SPAWN_SETSID → setsid(2), the libuv
3125
+ * implementation of Node's flag). env_pairs, when has_env, REPLACES the
3126
+ * child environment ([k,v,...] like the exec core's); cwd (NULL/"" =
3127
+ * inherit) chdirs the child. Spawn failure stays an event, never a
3128
+ * throw. */
3129
+ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
3130
+ double out_mode, double err_mode, double out_fd,
3131
+ double err_fd, bool detached, bool has_env,
3132
+ ScrArr *env_pairs, ScrStr *cwd) {
3133
+ if ((int)out_mode == 1 || (int)err_mode == 1 || (int)out_mode == 2 || (int)err_mode == 2) {
3134
+ /* Inherited or fd-redirected stdio: flush the parent's buffered
3135
+ * output first so everything logged BEFORE the spawn lands before
3136
+ * anything the child writes to the same destinations (Node's
3137
+ * practical ordering). */
3138
+ fflush(stdout);
3139
+ fflush(stderr);
3140
+ }
3141
+ ScrChild *c = calloc(1, sizeof(ScrChild));
3142
+ if (!c) scr_child_oom();
3143
+ c->rc = 1;
3144
+
3145
+ /* Piped slots: pipe(2) NOW, both ends cloexec (the file action's dup2
3146
+ * clears the flag on the child's copy — every other descriptor closes
3147
+ * at exec). A pipe failure degrades the slot to /dev/null — nothing
3148
+ * real hits it (fd exhaustion), and the stream husk answers eof. */
3149
+ int out_pipe[2] = {-1, -1};
3150
+ int err_pipe[2] = {-1, -1};
3151
+ if ((int)out_mode == 3 && pipe(out_pipe) != 0) {
3152
+ out_pipe[0] = out_pipe[1] = -1;
3153
+ out_mode = 0;
3154
+ }
3155
+ if ((int)err_mode == 3 && pipe(err_pipe) != 0) {
3156
+ err_pipe[0] = err_pipe[1] = -1;
3157
+ err_mode = 0;
3158
+ }
3159
+ for (int i = 0; i < 2; i++) {
3160
+ if (out_pipe[i] >= 0) fcntl(out_pipe[i], F_SETFD, FD_CLOEXEC);
3161
+ if (err_pipe[i] >= 0) fcntl(err_pipe[i], F_SETFD, FD_CLOEXEC);
3162
+ }
3163
+
3164
+ posix_spawn_file_actions_t fa;
3165
+ posix_spawn_file_actions_init(&fa);
3166
+ if ((int)in_mode == 0) {
3167
+ posix_spawn_file_actions_addopen(&fa, 0, "/dev/null", O_RDONLY, 0);
3168
+ } /* 1 = inherit: no redirection */
3169
+ if ((int)out_mode == 0) {
3170
+ posix_spawn_file_actions_addopen(&fa, 1, "/dev/null", O_WRONLY, 0);
3171
+ } else if ((int)out_mode == 2) {
3172
+ posix_spawn_file_actions_adddup2(&fa, (int)out_fd, 1);
3173
+ } else if ((int)out_mode == 3) {
3174
+ posix_spawn_file_actions_adddup2(&fa, out_pipe[1], 1);
3175
+ }
3176
+ if ((int)err_mode == 0) {
3177
+ posix_spawn_file_actions_addopen(&fa, 2, "/dev/null", O_WRONLY, 0);
3178
+ } else if ((int)err_mode == 2) {
3179
+ posix_spawn_file_actions_adddup2(&fa, (int)err_fd, 2);
3180
+ } else if ((int)err_mode == 3) {
3181
+ posix_spawn_file_actions_adddup2(&fa, err_pipe[1], 2);
3182
+ }
3183
+ #if defined(__APPLE__) || defined(__GLIBC__)
3184
+ if (cwd != NULL && cwd->len > 0) {
3185
+ posix_spawn_file_actions_addchdir_np(&fa, cwd->data);
3186
+ }
3187
+ #endif
3188
+ posix_spawnattr_t at;
3189
+ posix_spawnattr_init(&at);
3190
+ #ifdef POSIX_SPAWN_SETSID
3191
+ if (detached) posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSID);
3192
+ #else
3193
+ (void)detached; /* hosts without the flag: the child stays attached */
3194
+ #endif
3195
+ char **argv = scr_child_argv(cmd, args);
3196
+ char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL;
3197
+ pid_t pid = -1;
3198
+ int spawn_err = posix_spawnp(&pid, cmd->data, &fa, &at, argv,
3199
+ envp != NULL ? envp : environ);
3200
+ posix_spawnattr_destroy(&at);
3201
+ posix_spawn_file_actions_destroy(&fa);
3202
+ free(argv);
3203
+ scr_exec_envp_free(envp);
3204
+
3205
+ /* The parent's copies of the write ends close regardless of outcome —
3206
+ * the child (when it spawned) holds the only writer, so EOF arrives
3207
+ * exactly when it exits (or its last inheritor does). A FAILED spawn
3208
+ * keeps its streams too, with no writer at all: a consumer sees
3209
+ * immediate EOF, so 'end' fires on the turn after the 'error' event —
3210
+ * Node's exact order, pinned by corpus. */
3211
+ if (out_pipe[1] >= 0) close(out_pipe[1]);
3212
+ if (err_pipe[1] >= 0) close(err_pipe[1]);
3213
+ if ((int)out_mode == 3) c->out_stream = scr_child_stream_new(out_pipe[0]);
3214
+ if ((int)err_mode == 3) c->err_stream = scr_child_stream_new(err_pipe[0]);
3215
+
3216
+ if (spawn_err != 0) {
3217
+ /* Node: `Error: spawn <cmd> <ERRNAME>` on the "error" event. */
3218
+ const char *name = scr_errname(spawn_err);
3219
+ size_t len = 6 + cmd->len + 1 + strlen(name);
3220
+ char *msg = malloc(len + 1);
3221
+ if (!msg) scr_child_oom();
3222
+ snprintf(msg, len + 1, "spawn %s %s", cmd->data, name);
3223
+ c->state = SCR_CHILD_SPAWN_FAILED;
3224
+ c->spawn_errno = spawn_err;
3225
+ c->err_msg = scr_str_new(msg, len);
3226
+ free(msg);
3227
+ /* Settles at the next quiescent pass; no exit event will ever wake the
3228
+ * loop for it, so it counts as unwatched until then. */
3229
+ c->unwatched = true;
3230
+ scr_children_unwatched++;
3231
+ } else {
3232
+ c->state = SCR_CHILD_RUNNING;
3233
+ c->pid = pid;
3234
+ #if defined(SCR_HAVE_KQUEUE) || defined(SCR_CHILD_HAVE_PIDFD)
3235
+ c->unwatched = !scr_child_watch(pid);
3236
+ #else
3237
+ c->unwatched = true;
3238
+ #endif
3239
+ if (c->unwatched) scr_children_unwatched++;
3240
+ }
3241
+ /* The registry's reference: dropped when the child settles. */
3242
+ c->reffed = true;
3243
+ scr_children_reffed_n++;
3244
+ c->next = scr_children;
3245
+ scr_children = scr_child_retain(c);
3246
+ return c;
3247
+ }
3248
+
3249
+ /* ── the lifecycle members (child.pid/exitCode/killed/kill/unref) ──────── */
3250
+
3251
+ /* child.pid — undefined exactly when the spawn failed (Node's shape). */
3252
+ bool scr_child_has_pid(ScrChild *c) { return c->state != SCR_CHILD_SPAWN_FAILED; }
3253
+ double scr_child_pid(ScrChild *c) { return (double)c->pid; }
3254
+
3255
+ /* child.exitCode — null while running and for a signal death; the exit
3256
+ * code after a normal exit; -errno once a spawn failure SETTLED (Node
3257
+ * flips it when the "error" event fires — before that it reads null). */
3258
+ bool scr_child_has_exit_code(ScrChild *c) {
3259
+ if (c->state == SCR_CHILD_EXITED) return c->has_code;
3260
+ return c->state == SCR_CHILD_SPAWN_FAILED && c->settled;
3261
+ }
3262
+ double scr_child_exit_code(ScrChild *c) {
3263
+ if (c->state == SCR_CHILD_SPAWN_FAILED) return -(double)c->spawn_errno;
3264
+ return c->code;
3265
+ }
3266
+
3267
+ bool scr_child_killed(ScrChild *c) { return c->killed; }
3268
+
3269
+ /* child.kill core, numeric signal already resolved: kill(2) while the
3270
+ * handle is live (spawned, not yet reaped — a zombie counts, exactly
3271
+ * Node's window, which closes when 'exit' is emitted), false after the
3272
+ * reap or on spawn failure (Node's null-handle answer). A kill(2) failure
3273
+ * answers false too — Node also emits 'error' there, but for a child WE
3274
+ * spawned ESRCH can't happen before the reap and EPERM can't happen at
3275
+ * all, so the event is unreachable and stays unwired. Node sets `killed`
3276
+ * on ANY successful send, signal 0 included (pinned against Node). */
3277
+ static bool scr_child_kill_signo(ScrChild *c, int signo) {
3278
+ if (c->state != SCR_CHILD_RUNNING) return false;
3279
+ if (kill(c->pid, signo) != 0) return false;
3280
+ c->killed = true;
3281
+ return true;
3282
+ }
3283
+
3284
+ /* child.kill(name?) — the name resolves through Node's signal table
3285
+ * (scr_signal_from_name; unknown names throw the ERR_UNKNOWN_SIGNAL
3286
+ * TypeError BEFORE the handle check, like Node). */
3287
+ bool scr_child_kill(ScrChild *c, const ScrStr *signal) {
3288
+ int signo = scr_signal_from_name(signal);
3289
+ if (signo < 0) {
3290
+ size_t cap = 16 + signal->len + 1;
3291
+ char *msg = malloc(cap);
3292
+ if (!msg) scr_child_oom();
3293
+ int len = snprintf(msg, cap, "Unknown signal: %s", signal->data);
3294
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)len);
3295
+ free(msg);
3296
+ return false;
3297
+ }
3298
+ return scr_child_kill_signo(c, signo);
3299
+ }
3300
+
3301
+ /* child.kill(number) — the numeric form passes through (0 probes). */
3302
+ bool scr_child_kill_num(ScrChild *c, double signum) {
3303
+ return scr_child_kill_signo(c, (int)signum);
3304
+ }
3305
+
3306
+ /* child.unref() — drops the child from the loop's keep-alive set (Node's
3307
+ * semantics: the process may exit while the child runs). The child is
3308
+ * still REAPED whenever the loop runs for other reasons; one the loop
3309
+ * never reaps is left to the OS at process exit (init reparents it — no
3310
+ * zombie outlives the parent). */
3311
+ void scr_child_unref(ScrChild *c) {
3312
+ if (c->reffed) {
3313
+ c->reffed = false;
3314
+ scr_children_reffed_n--; /* settle/teardown clear reffed first */
3315
+ }
3316
+ }
3317
+
3318
+ /* The loop's liveness half: true while some UNSETTLED, REFFED child
3319
+ * exists (unref'd children never keep the process alive), or a piped
3320
+ * stream still flows for a consumer (Node's flowing-pipe keep-alive —
3321
+ * it may outlive the child's own settle when a grandchild holds the
3322
+ * write end). */
3323
+ bool scr_children_reffed_pending(void) {
3324
+ return scr_children_reffed_n > 0 || scr_child_streams_watching > 0;
3325
+ }
3326
+
3327
+ /* Loop-exhaustion teardown (the scr_timers_teardown twin): the loop may
3328
+ * exit with unref'd children still running — release the registry's
3329
+ * references (listeners never fire; Node's process-exit behavior) so the
3330
+ * RC audit stays clean. */
3331
+ void scr_children_teardown(void) {
3332
+ /* Streams the loop abandons (a data consumer over a pipe whose child
3333
+ * was unref'd, exception exits): release the registry's references so
3334
+ * the RC audit stays clean; listeners never fire again. */
3335
+ while (scr_child_streams != NULL) {
3336
+ ScrChildStream *s = scr_child_streams;
3337
+ scr_child_streams = s->next;
3338
+ s->next = NULL;
3339
+ scr_child_stream_finish(s, false);
3340
+ scr_child_stream_release(s);
3341
+ }
3342
+ while (scr_children != NULL) {
3343
+ ScrChild *c = scr_children;
3344
+ scr_children = c->next;
3345
+ c->next = NULL;
3346
+ c->settled = true; /* late listener registrations release immediately */
3347
+ if (c->reffed) {
3348
+ c->reffed = false;
3349
+ scr_children_reffed_n--;
3350
+ }
3351
+ if (c->unwatched) {
3352
+ c->unwatched = false;
3353
+ scr_children_unwatched--;
3354
+ }
3355
+ scr_child_drop_listeners(c);
3356
+ scr_child_release(c);
3357
+ }
3358
+ }
3359
+
3360
+ void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn) {
3361
+ if (c->settled) {
3362
+ scr_closure_release(cb); /* after the terminal event: never fires */
3363
+ return;
3364
+ }
3365
+ if (c->n_exit == c->cap_exit) {
3366
+ c->cap_exit = c->cap_exit ? c->cap_exit * 2 : 2;
3367
+ c->exit_cbs = realloc(c->exit_cbs, c->cap_exit * sizeof(*c->exit_cbs));
3368
+ if (!c->exit_cbs) scr_child_oom();
3369
+ }
3370
+ c->exit_cbs[c->n_exit].cb = cb;
3371
+ c->exit_cbs[c->n_exit].fn = fn;
3372
+ c->n_exit++;
3373
+ }
3374
+
3375
+ void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn) {
3376
+ if (c->settled) {
3377
+ scr_closure_release(cb);
3378
+ return;
3379
+ }
3380
+ if (c->n_err == c->cap_err) {
3381
+ c->cap_err = c->cap_err ? c->cap_err * 2 : 2;
3382
+ c->err_cbs = realloc(c->err_cbs, c->cap_err * sizeof(*c->err_cbs));
3383
+ if (!c->err_cbs) scr_child_oom();
3384
+ }
3385
+ c->err_cbs[c->n_err].cb = cb;
3386
+ c->err_cbs[c->n_err].fn = fn;
3387
+ c->n_err++;
3388
+ }
3389
+
3390
+ /* ── the runtime-provided listener adapters ──────────────────────────── */
3391
+
3392
+ /* A zero-param exit listener: the code and signal are ignored. */
3393
+ void scr_child_exit_thunk0(ScrClosure *cb, bool has_code, double code,
3394
+ const char *signal_name) {
3395
+ (void)has_code;
3396
+ (void)code;
3397
+ (void)signal_name;
3398
+ ((void (*)(ScrClosure *))cb->fn)(cb);
3399
+ }
3400
+
3401
+ /* A zero-param error listener: the message is ignored. */
3402
+ void scr_child_err_thunk0(ScrClosure *cb, ScrStr *msg) {
3403
+ (void)msg;
3404
+ ((void (*)(ScrClosure *))cb->fn)(cb);
3405
+ }
3406
+
3407
+ /* The errno name of the child whose error listeners are FIRING — set by
3408
+ * scr_child_settle around its error pass so the thunk below can stamp
3409
+ * Node's `code` on the constructed error (the thunk signature is shared
3410
+ * with scr_net.c's error events, which parse theirs out of the message
3411
+ * below). NULL outside a spawn-failure settle. */
3412
+ static const char *scr_child_err_code = NULL;
3413
+
3414
+ /* [.code on event errors — net/http/dgram parity with spawn failures]
3415
+ * The errno name embedded in an errnoException-style message ("connect
3416
+ * ECONNREFUSED 127.0.0.1:80", "listen EADDRINUSE: address already in use
3417
+ * :::4000", "bind EADDRINUSE 0.0.0.0:4000"): the SECOND space-separated
3418
+ * token when it spells an errno name (E + capitals/digits, a colon or
3419
+ * space terminating). Every message fired through this thunk is this
3420
+ * runtime's own construction, and exactly the ones carrying a code embed
3421
+ * it in this shape — Node builds the same messages FROM the code
3422
+ * (errnoException), so parsing it back is exact over the closed message
3423
+ * set. Fallback only (an explicit context wins); returns a static buffer
3424
+ * valid until the next call, or NULL for code-less messages ("socket
3425
+ * hang up" stays undefined — divergence 54's documented bound). */
3426
+ static const char *scr_err_msg_code(const ScrStr *msg) {
3427
+ static char buf[32];
3428
+ const char *sp = strchr((const char *)msg->data, ' ');
3429
+ if (sp == NULL || sp[1] != 'E') return NULL;
3430
+ const char *tok = sp + 1;
3431
+ size_t n = 1;
3432
+ while (tok[n] != '\0' && tok[n] != ' ' && tok[n] != ':') {
3433
+ char c = tok[n];
3434
+ if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) return NULL;
3435
+ if (n + 1 >= sizeof buf) return NULL;
3436
+ n++;
3437
+ }
3438
+ if (n < 3) return NULL; /* "EIO" is the shortest real name */
3439
+ memcpy(buf, tok, n);
3440
+ buf[n] = '\0';
3441
+ return buf;
3442
+ }
3443
+
3444
+ /* An (err: Error) listener: constructs the %Error instance from the
3445
+ * message (borrowed; scr_error_new retains its copy) — the listener owns
3446
+ * the +1 error param per the universal convention. `code` stamps from the
3447
+ * settle context (spawn failures) or the message's own embedded errno name
3448
+ * (net/http/dgram 'error' events — see scr_err_msg_code). */
3449
+ void scr_child_err_thunk_error(ScrClosure *cb, ScrStr *msg) {
3450
+ ScrError *e = scr_error_new(0 /* Error */, msg);
3451
+ if (scr_child_err_code != NULL) {
3452
+ scr_error_set_code(e, scr_child_err_code);
3453
+ } else {
3454
+ const char *code = scr_err_msg_code(msg);
3455
+ if (code != NULL) scr_error_set_code(e, code);
3456
+ }
3457
+ ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, e);
3458
+ }
3459
+
3460
+ /* ── the loop's half (called from scr_async.c) ───────────────────────── */
3461
+
3462
+ bool scr_children_pending(void) {
3463
+ return scr_children != NULL || scr_child_streams_watching > 0;
3464
+ }
3465
+
3466
+ /* True while an UNSETTLED spawn failure sits in the registry: its 'error'
3467
+ * is a next-tick-shaped event Node delivers regardless of ref state (an
3468
+ * unref'd failed spawn still crashes an error-listener-less program), so
3469
+ * the loop's only-unref'd-children exit gate must not skip it. */
3470
+ bool scr_children_failed_pending(void) {
3471
+ for (ScrChild *c = scr_children; c; c = c->next) {
3472
+ if (c->state == SCR_CHILD_SPAWN_FAILED && !c->settled) return true;
3473
+ }
3474
+ return false;
3475
+ }
3476
+
3477
+ /* Fires one settled child's terminal event. The child is already off the
3478
+ * registry; drops the listeners and the registry's reference. The pinned
3479
+ * ordering runs first: consumer-owning piped streams drain to EOF/EAGAIN
3480
+ * so their 'end' events fire BEFORE 'exit' (Node-exact — verified: data*,
3481
+ * end, then exit for every output shape). */
3482
+ static void scr_child_settle(ScrChild *c) {
3483
+ if (c->reffed) {
3484
+ c->reffed = false;
3485
+ scr_children_reffed_n--;
3486
+ }
3487
+ if (c->unwatched) {
3488
+ c->unwatched = false;
3489
+ scr_children_unwatched--;
3490
+ }
3491
+ /* Streams first, settled after: an exit listener registered from a
3492
+ * draining 'data'/'end' callback still fires below, like Node's. The
3493
+ * SPAWN-FAILURE path skips the drain — Node fires 'error' first and
3494
+ * the streams' immediate EOF delivers 'end' on the next turn. */
3495
+ if (c->state == SCR_CHILD_EXITED) {
3496
+ scr_child_stream_drain_now(c->out_stream);
3497
+ if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream);
3498
+ }
3499
+ c->settled = true;
3500
+ if (scr_exc_pending()) {
3501
+ /* A data/end listener threw: the loop surfaces it (the exit event is
3502
+ * lost with the process, like Node's crash-on-uncaught). */
3503
+ scr_child_drop_listeners(c);
3504
+ scr_child_release(c);
3505
+ return;
3506
+ }
3507
+ if (c->state == SCR_CHILD_SPAWN_FAILED) {
3508
+ if (c->n_err == 0) {
3509
+ /* Node: an 'error' event with no listener is an uncaught throw.
3510
+ * _Exit skips atexit handlers (the RC audit above all) on purpose —
3511
+ * the loop dies mid-turn with frames and registries still live,
3512
+ * exactly like process.exit(). */
3513
+ fflush(stdout);
3514
+ fprintf(stderr, "Unhandled 'error' event: Error: %s\n", c->err_msg->data);
3515
+ _Exit(1);
3516
+ }
3517
+ /* Node's spawn-failure error is an errnoException: `code` carries the
3518
+ * errno name. The thunk reads this while the pass runs. */
3519
+ scr_child_err_code = scr_errname(c->spawn_errno);
3520
+ for (size_t i = 0; i < c->n_err; i++) {
3521
+ c->err_cbs[i].fn(c->err_cbs[i].cb, c->err_msg);
3522
+ if (scr_exc_pending()) break; /* the loop surfaces it */
3523
+ }
3524
+ scr_child_err_code = NULL;
3525
+ } else {
3526
+ for (size_t i = 0; i < c->n_exit; i++) {
3527
+ c->exit_cbs[i].fn(c->exit_cbs[i].cb, c->has_code, c->code, c->exit_signal);
3528
+ if (scr_exc_pending()) break;
3529
+ }
3530
+ }
3531
+ scr_child_drop_listeners(c);
3532
+ scr_child_release(c); /* the registry's reference */
3533
+ }
3534
+
3535
+ /* One reap pass: piped streams service FIRST (arrived chunks fire their
3536
+ * 'data' listeners; a pipe that hit EOF fires 'end' — before any 'exit'
3537
+ * this pass delivers, the pinned ordering), then waitpid(WNOHANG) every
3538
+ * running child; spawn failures settle on their first pass (their "next
3539
+ * tick"). Listeners run synchronously on the main stack, like timer
3540
+ * callbacks — a throw stops the pass and leaves the exception pending
3541
+ * for the loop. */
3542
+ void scr_children_poll(void) {
3543
+ scr_child_streams_service();
3544
+ if (scr_exc_pending()) return;
3545
+ ScrChild **link = &scr_children;
3546
+ while (*link) {
3547
+ ScrChild *c = *link;
3548
+ bool settle = false;
3549
+ if (c->state == SCR_CHILD_SPAWN_FAILED) {
3550
+ settle = true;
3551
+ } else {
3552
+ int st = 0;
3553
+ pid_t got = waitpid(c->pid, &st, WNOHANG);
3554
+ if (got == c->pid) {
3555
+ scr_child_unwatch(c->pid); /* Linux: the reaped pidfd must close */
3556
+ c->state = SCR_CHILD_EXITED;
3557
+ if (WIFEXITED(st)) {
3558
+ c->has_code = true;
3559
+ c->code = (double)WEXITSTATUS(st);
3560
+ } else {
3561
+ c->has_code = false; /* signal death: Node's code null */
3562
+ c->code = 0;
3563
+ if (WIFSIGNALED(st)) c->exit_signal = scr_signal_name(WTERMSIG(st));
3564
+ }
3565
+ settle = true;
3566
+ } else if (got < 0 && errno != EINTR) {
3567
+ /* ECHILD or another wait failure: nothing to reap — settle as a
3568
+ * signal-style exit so the loop can never spin forever. */
3569
+ scr_child_unwatch(c->pid);
3570
+ c->state = SCR_CHILD_EXITED;
3571
+ c->has_code = false;
3572
+ c->code = 0;
3573
+ settle = true;
3574
+ }
3575
+ }
3576
+ if (settle) {
3577
+ *link = c->next; /* unlink BEFORE firing: callbacks may spawn */
3578
+ c->next = NULL;
3579
+ scr_child_settle(c);
3580
+ if (scr_exc_pending()) return;
3581
+ } else {
3582
+ link = &c->next;
3583
+ }
3584
+ }
3585
+ }
3586
+
3587
+ #endif /* !_WIN32 */