Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ Chinese is the primary changelog and this file is its maintained English mirror.
explicitly unapproved for distribution without changing the XCFramework
Release's RootFS exclusion policy.

## v0.4.0-abi.8 (planned Stage1 maintenance prerelease)
## v0.4.0-abi.9 (planned Stage1 maintenance prerelease)

This is a compatibility maintenance release after `v0.4.0-abi.8`. It remains a
prerelease and is **not stable v0.4.0**.

- The C API adds `ish_embed_session_write_timeout` and
`ish_embed_session_close_stdin_timeout`, with matching Swift
`write(_:timeout:)`/`closeStdin(timeout:)` methods. Callers can use a short
deadline for each stdin operation; the earlier of that deadline and the
original SPAWN deadline wins, and expiry publishes no late frame. Long
commands can therefore drain output and observe cancellation between bounded
chunks.
- The public C ABI remains version 1 and wire protocol remains v4. The new
function symbols are additive. RootFS remains outside the Release. This
version does not implement a native Agent Loop or install Codex CLI.

## v0.4.0-abi.8 (published Stage1 maintenance prerelease)

This is a compatibility maintenance release after `v0.4.0-abi.7`. It remains a
prerelease and is **not stable v0.4.0**.
Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,20 @@
`fs.tar.gz`,环境差异仍保留在外部证据中。CI 不上传制品,候选仍明确标记为未获分发
批准,不改变 XCFramework Release 的 RootFS 排除策略。

## v0.4.0-abi.8(计划中的 Stage1 维护预发布)
## v0.4.0-abi.9(计划中的 Stage1 维护预发布)

这是 `v0.4.0-abi.8` 之后的兼容性维护版本,仍是 prerelease,**不是稳定
v0.4.0**。

- C API 新增 `ish_embed_session_write_timeout` 与
`ish_embed_session_close_stdin_timeout`,Swift 新增对应的
`write(_:timeout:)`/`closeStdin(timeout:)`。调用方可以为单次 stdin
控制设置短 deadline;它与 session 原始 SPAWN deadline 取更早值,超时不会发布
late frame,便于长命令在分块写入之间排空输出并及时响应取消。
- 公开 C ABI 版本仍为 1,wire protocol 仍为 v4;新增函数符号是向后兼容扩展。
RootFS 不进入 Release。本版本不实现原生 Agent Loop,也不会安装 Codex CLI。

## v0.4.0-abi.8(已发布的 Stage1 维护预发布)

这是 `v0.4.0-abi.7` 之后的兼容性维护版本,仍是 prerelease,**不是稳定
v0.4.0**。
Expand Down
29 changes: 17 additions & 12 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ and simulator slices are arm64.

## Current phase: native ABI transition

The default branch has published `v0.4.0-abi.7` and is preparing the compatible
maintenance prerelease `v0.4.0-abi.8`. Both belong to the Stage1 **native ABI
The default branch has published `v0.4.0-abi.8` and is preparing the compatible
maintenance prerelease `v0.4.0-abi.9`. Both belong to the Stage1 **native ABI
transition**. Neither is stable `v0.4.0` or the complete v0.4 Swift API. Keep
these four version surfaces distinct:

| Surface | Current `v0.4.0-abi.7` | Planned `v0.4.0-abi.8` |
| Surface | Current `v0.4.0-abi.8` | Planned `v0.4.0-abi.9` |
| --- | --- | --- |
| Public C ABI | `ISH_EMBED_ABI_VERSION == 1`; compatible symbols including atomic rename are public | Still ABI 1 with no new symbol; finite-session stdin deadline semantics are tightened |
| Public C ABI | `ISH_EMBED_ABI_VERSION == 1`; atomic rename and finite-stdin deadline behavior are public | Still ABI 1; additive per-call stdin write/close timeout symbols |
| Internal wire protocol | exact-match v4 between host and embedded supervisor | still v4; this is not the public C ABI version |
| `Package.swift` | pins the public `v0.4.0-abi.7` URL/checksum | the release transaction creates a manifest-only release commit pinned to the maintenance binary |
| Swift source | remains v0.3.3-ABI compatible and includes typed rename | same API; finite stdin writes can no longer bypass the product deadline |
| `Package.swift` | pins the public `v0.4.0-abi.8` URL/checksum | the release transaction creates a manifest-only release commit pinned to the maintenance binary |
| Swift source | remains v0.3.3-ABI compatible and includes typed rename | adds `write(_:timeout:)`/`closeStdin(timeout:)` through a weak shim compatible with the current binary |

Stage1 native code adds session retain/release, a joinable kernel thread,
soft-halt, exact wire v4, and complete session close. The existing Swift wrapper
Expand Down Expand Up @@ -90,15 +90,15 @@ gives those narrow differences independent PRs, CI, and an exact gitlink, making
PocketRoot builds and releases reproducible. We do not directly rewrite somebody
else's local upstream repository; generally useful fixes can still be contributed
to [iSH upstream](https://github.com/ish-app/ish), while the fork carries project
gates until upstream accepts and releases them. The current `v0.4.0-abi.8`
gates until upstream accepts and releases them. The current `v0.4.0-abi.9`
source change includes neither RootFS content nor any prebuilt XCFramework/guest
binary; binaries may be produced and published only by a later release
transaction after its gates pass.

## Installation status

`v0.4.0-abi.7` is public and [`Package.swift`](Package.swift) currently pins it.
Until `v0.4.0-abi.8` is published, the manifest keeps pointing at that verified
`v0.4.0-abi.8` is public and [`Package.swift`](Package.swift) currently pins it.
Until `v0.4.0-abi.9` is published, the manifest keeps pointing at that verified
asset instead of advertising a future 404 URL. Use Xcode's
**File → Add Package Dependencies…** with:

Expand All @@ -110,9 +110,10 @@ Select a version whose tag, `libIshKernel.xcframework.zip`, Corresponding Source
and manifest URL/checksum all match. Consumer projects do not need Meson, Zig,
or LLVM.

`v0.4.0-abi.7` provides guest-atomic rename without a shell or
check-then-rename race. `v0.4.0-abi.8` makes finite-timeout stdin write/close
share the SPAWN absolute deadline. It does not
`v0.4.0-abi.8` provides guest-atomic rename without a shell or
check-then-rename race. `v0.4.0-abi.9` adds short per-call stdin write/close
timeouts so callers can drain output and observe cancellation between chunks;
the call and SPAWN deadlines use the earlier value. It does not
implement a native Agent Loop or
install Codex CLI in the app.
Node.js/npm remain optional choices of the RootFS/guest package-management flow,
Expand Down Expand Up @@ -160,6 +161,10 @@ unowned guest process. NaN and either infinity return `ISH_ERR_INVALID_ARG
(-13)` before native entry. If less than 1 ms remains after marshalling, the
wrapper returns `ISH_ERR_TIMEOUT (-12)` instead of passing native `0` and
degrading to “no timeout.”
For responsive cancellation during a long command, use `write(_:timeout:)` and
`closeStdin(timeout:)` with a shorter per-call deadline and keep draining output
between chunks. The earlier of the call deadline and original SPAWN deadline
wins.

Use the `ensureDefaultVM()` and `spawn(in:)` helpers only when the verified
RootFS manifest explicitly includes `/srv/vms/.template`. Continuously drain a
Expand Down
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ RootFS 安装、产品级命令策略、Swift Concurrency 隔离和界面。项

## 当前阶段:Native ABI 过渡

当前默认分支已发布 `v0.4.0-abi.7`,正在准备兼容性维护预发布
`v0.4.0-abi.8`。它们都属于 **Stage1 native ABI 过渡**,不是稳定 `v0.4.0`,
当前默认分支已发布 `v0.4.0-abi.8`,正在准备兼容性维护预发布
`v0.4.0-abi.9`。它们都属于 **Stage1 native ABI 过渡**,不是稳定 `v0.4.0`,
也不是完整 v0.4 Swift API。请同时区分下面四个版本面:

| 版本面 | 当前 `v0.4.0-abi.7` | 计划中的 `v0.4.0-abi.8` |
| 版本面 | 当前 `v0.4.0-abi.8` | 计划中的 `v0.4.0-abi.9` |
| --- | --- | --- |
| 公开 C ABI | `ISH_EMBED_ABI_VERSION == 1`;原子 rename 等兼容性符号已发布 | 仍为 ABI 1,不新增符号;收紧有限 session 的 stdin deadline 语义 |
| 公开 C ABI | `ISH_EMBED_ABI_VERSION == 1`;原子 rename 与有限 stdin deadline 已发布 | 仍为 ABI 1;新增单次 stdin write/close timeout 兼容性符号 |
| 内部 wire protocol | host 与内嵌 supervisor 精确匹配 v4 | 仍为 v4;它不是公开 C ABI 版本 |
| `Package.swift` | 固定已公开的 `v0.4.0-abi.7` URL/checksum | 发布事务生成只改 manifest 的 release commit,固定到维护二进制 |
| Swift 源 | 保持 v0.3.3 ABI 兼容,已提供类型化 rename | 同一 API;有限 stdin write 不再绕过产品 deadline |
| `Package.swift` | 固定已公开的 `v0.4.0-abi.8` URL/checksum | 发布事务生成只改 manifest 的 release commit,固定到维护二进制 |
| Swift 源 | 保持 v0.3.3 ABI 兼容,已提供类型化 rename | 新增 `write(_:timeout:)`/`closeStdin(timeout:)`,且通过 weak shim 兼容当前 binary |

Stage1 的 native runtime 已加入 session retain/release、可等待 kernel 线程、soft-halt、
严格 v4 协议和完整 session close 等底层能力。现有 Swift wrapper 刻意不调用新增
Expand Down Expand Up @@ -69,13 +69,13 @@ JIT 脏页一致性必须修改模拟器核心,无法只在 outer package 或
窄差异拥有独立 PR、CI 和精确 gitlink,PocketRoot 的构建与发布也因此可复现。我们不会在
本地直接改写别人维护的上游仓库;适合通用化的修复仍可回馈
[iSH upstream](https://github.com/ish-app/ish),但在上游接受并发布前由 fork 承担项目门禁。
当前 `v0.4.0-abi.8` 源码变更不纳入 RootFS,也不提交任何预构建
当前 `v0.4.0-abi.9` 源码变更不纳入 RootFS,也不提交任何预构建
XCFramework/guest binary;二进制只能在后续发布事务通过后生成和发布。

## 安装状态

`v0.4.0-abi.7` 已公开且当前 [`Package.swift`](Package.swift) 固定到它。
`v0.4.0-abi.8` 发布前,manifest 继续指向这个已验证的资产,不会提前引用 404 URL。
`v0.4.0-abi.8` 已公开且当前 [`Package.swift`](Package.swift) 固定到它。
`v0.4.0-abi.9` 发布前,manifest 继续指向这个已验证的资产,不会提前引用 404 URL。
在 Xcode 的 **File → Add Package Dependencies…** 中使用:

```text
Expand All @@ -85,9 +85,10 @@ https://github.com/jacklv-coder/ish-arm64-pkg
请选择明确包含 `libIshKernel.xcframework.zip`、对应源码归档,并且 manifest URL/checksum
与同一标签匹配的版本。业务工程不需要安装 Meson、Zig 或 LLVM。

`v0.4.0-abi.7` 已提供无 shell、无 check-then-rename 竞争窗口的 guest 原子重命名。
`v0.4.0-abi.8` 让有限 timeout session 的 stdin write/close 都受同一 SPAWN
绝对 deadline 约束;它不实现原生 Agent Loop,也不会
`v0.4.0-abi.8` 已提供无 shell、无 check-then-rename 竞争窗口的 guest 原子重命名。
`v0.4.0-abi.9` 新增单次 stdin write/close 的短 timeout,让上层可在分块发送之间
排空输出并及时响应取消;调用 deadline 与 SPAWN deadline 取更早值。它不实现原生
Agent Loop,也不会
在 App 内安装 Codex CLI。Node.js/npm 如有需要仍由
RootFS/guest 包管理流程选择,不属于 runtime 的强制依赖。

Expand Down Expand Up @@ -127,6 +128,9 @@ stdin write/close 复用同一期限取得顺序锁和 writer gate,过期时
读取权威 `EXITED` 才能确认终止。stdin close 遇到 active stdin write 时返回
`ISH_ERR_BUSY`,不会排在它后面等待。如果 runtime 无法确认命令已清理,会转入
shutting-down 状态而不是遗留无主 guest 进程。
需要在长命令期间及时检查取消时,可使用 `write(_:timeout:)` 与
`closeStdin(timeout:)` 为每次调用设置更短 deadline,并在分块调用之间持续读取输出;
调用 deadline 与原始 SPAWN deadline 取更早值。
NaN/正负无穷会在进入 native 前返回 `ISH_ERR_INVALID_ARG (-13)`;封送后剩余不足
1 ms 时会返回 `ISH_ERR_TIMEOUT (-12)`,不会把 `0` 传给 native 而退化成“无超时”。

Expand Down
34 changes: 34 additions & 0 deletions Sources/CIshEmbed/CIshEmbed.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ int ish_embed_rename_noreplace(ish_embed_instance_t *inst,
return ISH_SWIFT_ERR_UNSUPPORTED;
}

__attribute__((weak))
int ish_embed_session_write_timeout(ish_embed_session_t *session,
const uint8_t *buf,
size_t len,
uint32_t timeout_ms) {
(void)session;
(void)buf;
(void)len;
(void)timeout_ms;
return ISH_SWIFT_ERR_UNSUPPORTED;
}

__attribute__((weak))
int ish_embed_session_close_stdin_timeout(ish_embed_session_t *session,
uint32_t timeout_ms) {
(void)session;
(void)timeout_ms;
return ISH_SWIFT_ERR_UNSUPPORTED;
}

int ish_embed_swift_rename_noreplace(ish_embed_instance_t *inst,
const char *source,
const char *destination,
Expand All @@ -34,3 +54,17 @@ int ish_embed_swift_rename_noreplace(ish_embed_instance_t *inst,
return ish_embed_rename_noreplace(
inst, source, destination, timeout_ms, out_guest_errno);
}

int ish_embed_swift_session_write_timeout(ish_embed_session_t *session,
const uint8_t *buf,
size_t len,
uint32_t timeout_ms) {
return ish_embed_session_write_timeout(
session, buf, len, timeout_ms);
}

int ish_embed_swift_session_close_stdin_timeout(
ish_embed_session_t *session,
uint32_t timeout_ms) {
return ish_embed_session_close_stdin_timeout(session, timeout_ms);
}
9 changes: 9 additions & 0 deletions Sources/CIshEmbed/include/IshEmbedSwiftShim.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ int ish_embed_swift_rename_noreplace(ish_embed_instance_t *inst,
uint32_t timeout_ms,
int32_t *out_guest_errno);

/* Swift source compatibility bridge for the bounded-write ABI. */
int ish_embed_swift_session_write_timeout(ish_embed_session_t *session,
const uint8_t *buf,
size_t len,
uint32_t timeout_ms);
int ish_embed_swift_session_close_stdin_timeout(
ish_embed_session_t *session,
uint32_t timeout_ms);

#ifdef __cplusplus
}
#endif
Expand Down
50 changes: 50 additions & 0 deletions Sources/IshEmbed/IshEmbed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,25 @@ public final class IshSession: @unchecked Sendable {
}
}

/// Queues stdin bytes with a deadline relative to this call.
///
/// The effective deadline is the earlier of `timeout` and the original
/// finite SPAWN deadline. A failed multi-frame call may already have
/// admitted a prefix, so stage input before an atomic commit when needed.
public func write(_ data: Data, timeout: TimeInterval) throws {
let milliseconds = try Self.callTimeoutMilliseconds(timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduct Swift-side delay from the write timeout

The relative timeout is converted once and then passed unchanged to the C API, which starts a fresh deadline at native entry. If the calling thread is suspended after line 829 or waits to acquire the session gate, a frame can therefore be admitted more than timeout seconds after the Swift call began, contrary to this method's documented call-relative deadline. Establish an absolute uptime deadline at Swift API entry and recompute the remaining milliseconds immediately before invoking the shim, as IshSpawnTimeoutBudget already does.

Useful? React with 👍 / 👎.

try withRawCall { r in
try data.withUnsafeBytes { rawBuf in
let rc = ish_embed_swift_session_write_timeout(
r,
rawBuf.baseAddress?.assumingMemoryBound(to: UInt8.self),
rawBuf.count,
milliseconds)
if rc != ishOK { throw IshError.from(rc) }
}
}
}

/// Finite-timeout sessions reuse their native SPAWN admission deadline.
/// Expiry returns `ISH_ERR_TIMEOUT` without publishing a late EOF frame.
public func closeStdin() throws {
Expand All @@ -829,6 +848,37 @@ public final class IshSession: @unchecked Sendable {
}
}

/// Queues EOF with a deadline relative to this call.
///
/// A timeout does not publish a late EOF and leaves stdin open so callers
/// can retry after checking cancellation or terminate the session.
public func closeStdin(timeout: TimeInterval) throws {
let milliseconds = try Self.callTimeoutMilliseconds(timeout)
try withRawCall { r in
let rc = ish_embed_swift_session_close_stdin_timeout(
r,
milliseconds)
if rc != ishOK { throw IshError.from(rc) }
}
}

private static func callTimeoutMilliseconds(
_ timeout: TimeInterval
) throws -> UInt32 {
guard timeout.isFinite, timeout > 0 else {
throw IshError.from(ishErrInvalidArg)
}
let maximumMilliseconds = Double(UInt32.max - 1)
let milliseconds = min(
maximumMilliseconds,
ceil(timeout * 1_000)
)
guard milliseconds >= 1 else {
throw IshError.from(ishErrTimeout)
}
return UInt32(milliseconds)
}

/// Send a signal (standard Linux signum). Use 2 for SIGINT (Ctrl+C).
///
/// In TTY mode, common terminal-control signals are translated into
Expand Down
29 changes: 29 additions & 0 deletions Tests/IshEmbedTests/IshEmbedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,35 @@ final class IshEmbedTests: XCTestCase {
XCTAssertEqual(counts.shutdown, 1)
}

func testPerCallStdinTimeoutRejectsInvalidValuesBeforeNativeEntry() throws {
let native = LifecycleNativeHarness()
let instance = IshInstance(nativeCalls: native.nativeCalls())
try instance.boot(.init(rootfsPath: "/unused-test-rootfs"))
let session = try instance.spawn(.init(argv: ["/bin/true"]))

for timeout in [
TimeInterval.zero,
-1,
.infinity,
-.infinity,
.nan,
] {
XCTAssertThrowsError(
try session.write(Data([0x41]), timeout: timeout)
) {
XCTAssertEqual(ishErrorCode($0), ISH_ERR_INVALID_ARG.rawValue)
}
XCTAssertThrowsError(
try session.closeStdin(timeout: timeout)
) {
XCTAssertEqual(ishErrorCode($0), ISH_ERR_INVALID_ARG.rawValue)
}
}

session.close()
try instance.shutdown()
}

func testProductionSessionLeaseSurvivesUntilNativeCloseReturns() throws {
let closeEntered = DispatchSemaphore(value: 0)
let releaseClose = DispatchSemaphore(value: 0)
Expand Down
Loading
Loading