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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@

Capture 和此次 Mac 分发尚未发布,直接修订候选 schema,不增加旧 HTTP、旧回执或旧本机格式兼容。保留原开发数据文件,不自动导入或清空。第三方引用必须保留许可证、固定来源、修改说明和署名;打包必须包含完整依赖许可。

同次提交不得换 UUID、变更材料或改投另一个信息库。只有经过指纹核对的共享回执可以清理本机材料。选区失败不能读旧剪贴板充数;截图只持久化明确选择的区域。
同次提交不得换 UUID、变更材料或改投另一个信息库。只有事务明确成功或核对共享 Capture 根的材料摘要后才可清理本机材料。扩展不拥有独立共享表;草稿、待办和确认结果由本机 SQLite 保存。选区失败不能读旧剪贴板充数;截图只持久化明确选择的区域。

运行 `bash scripts/check.sh` 和 `bash scripts/build-app.sh`。前者与 CI 共用 Xcode 26.2 的 Swift 格式/lint 检查和测试入口,覆盖 Host、PeerCore 与 first-party 扩展;固定来源的 Vendor 代码按上游格式维护。编译不证明系统权限、真实选区、焦点或多屏行为;真实应用验证使用合成内容,按实际结果记录。开发包使用 `dev.inkcre.capture`;发布身份、签名、公证只能使用显式提供的输入。
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ public final class BundledExtensionRuntime {
public let module: any NativeExtension
public private(set) var running = false
public private(set) var applying = false
public private(set) var syncIssue: String?
private var idleWaiters: [CheckedContinuation<Void, Never>] = []
private let defaults: UserDefaults
private var key: String { "extensions.\(module.descriptor.name).enabled" }
Expand All @@ -29,17 +28,10 @@ public final class BundledExtensionRuntime {
for waiter in idleWaiters { waiter.resume() }
idleWaiters.removeAll()
}
syncIssue = nil
if !enabled {
defaults.set(false, forKey: key)
try await stop()
}
do { try await module.setEnabled(enabled) } catch {
if enabled {
guard case PeerError.connection = error else { throw error }
}
syncIssue = "共享状态未同步:\(error.localizedDescription)"
}
if enabled {
defaults.set(true, forKey: key)
try startIfEnabled()
Expand Down
66 changes: 31 additions & 35 deletions Packages/PeerCore/Sources/PeerCore/BundledParticipation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@ extension PeerDatabase {
let defaults = UserDefaults(suiteName: "dev.inkcre.native-participation")!
let key = "\(target.peerID).\(descriptor.name)"
let firstJoin = !defaults.bool(forKey: key)
let client = try client(for: target, password: password)
let issue: String? = try await client.withTransaction(logger: Self.logger) { connection in
if firstJoin {
try await connection.query(
"INSERT INTO inkcre.extensions(name) VALUES (\(descriptor.name)) ON CONFLICT (name) DO NOTHING",
logger: Self.logger)
try await connection.query(
"""
INSERT INTO inkcre.extension_peer_participations(name,peer_id,distribution,version,enabled)
VALUES (\(descriptor.name),\(target.peerID),\(descriptor.distribution),\(descriptor.version),true)
ON CONFLICT (name,peer_id) DO NOTHING
""", logger: Self.logger)
let issue: String? = try await withConnection(for: target, password: password) {
connection in
try await connection.withTransaction(logger: Self.logger) { connection in
if firstJoin {
try await connection.query(
"INSERT INTO inkcre.extensions(name) VALUES (\(descriptor.name)) ON CONFLICT (name) DO NOTHING",
logger: Self.logger)
try await connection.query(
"""
INSERT INTO inkcre.extension_peer_participations(name,peer_id,distribution,version,enabled)
VALUES (\(descriptor.name),\(target.peerID),\(descriptor.distribution),\(descriptor.version),true)
ON CONFLICT (name,peer_id) DO NOTHING
""", logger: Self.logger)
}
do {
try await Self.requireSelection(
descriptor, peerID: target.peerID, on: connection)
return nil
} catch PeerError.needsAction(let message) { return message }
}
do {
try await Self.requireSelection(descriptor, peerID: target.peerID, on: connection)
return nil
} catch PeerError.needsAction(let message) { return message }
}
defaults.set(true, forKey: key)
if let issue { throw PeerError.needsAction(issue) }
Expand All @@ -43,41 +46,34 @@ extension PeerDatabase {
for try await (distribution, version, enabled) in rows.decode((String?, String, Bool).self)
{
guard distribution == descriptor.distribution, version == descriptor.version else {
throw PeerError.needsAction("此 Peer 选择的扩展版本不在当前应用中,请核对扩展选择。")
throw PeerError.needsAction("此 Peer 选择的扩展版本不在当前应用中,请在扩展设置中对该信息库启用本机版本。")
}
guard enabled else {
throw PeerError.needsAction("信息库中的扩展参与已停用;本机材料保留,请在扩展设置中对该信息库启用本机版本后继续。")
}
guard enabled else { throw PeerError.needsAction("信息库中的扩展参与已停用;本机材料保留,请在线明确启用后继续。") }
return
}
throw PeerError.needsAction("此 Peer 的扩展参与已移除;不会在重连时自动重建。")
}

public func setEnabled(
_ enabled: Bool, target: ConnectionTarget, password: String,
extension descriptor: NativeExtensionDescriptor
/// Explicitly select the version shipped in this app for one info-base.
public func useBundledVersion(
_ descriptor: NativeExtensionDescriptor, target: ConnectionTarget, password: String
) async throws {
_ = try await register(target, password: password)
let client = try client(for: target, password: password)
try await client.withTransaction(logger: Self.logger) { connection in
if enabled {
try await register(target, password: password)
try await withConnection(for: target, password: password) { connection in
try await connection.withTransaction(logger: Self.logger) { connection in
try await connection.query(
"INSERT INTO inkcre.extensions(name) VALUES (\(descriptor.name)) ON CONFLICT (name) DO NOTHING",
logger: Self.logger)
try await connection.query(
"""
INSERT INTO inkcre.extension_peer_participations(name,peer_id,distribution,version,enabled)
VALUES (\(descriptor.name),\(target.peerID),\(descriptor.distribution),\(descriptor.version),true)
ON CONFLICT (name,peer_id) DO NOTHING
ON CONFLICT (name,peer_id) DO UPDATE SET
distribution=EXCLUDED.distribution,version=EXCLUDED.version,enabled=true
""", logger: Self.logger)
}
let rows = try await connection.query(
"""
UPDATE inkcre.extension_peer_participations SET enabled=\(enabled)
WHERE name=\(descriptor.name) AND peer_id=\(target.peerID)
AND distribution=\(descriptor.distribution) AND version=\(descriptor.version)
RETURNING name
""", logger: Self.logger)
for try await _ in rows.decode(String.self) { return }
if enabled { throw PeerError.needsAction("信息库中没有与本机匹配的扩展选择。") }
}
let defaults = UserDefaults(suiteName: "dev.inkcre.native-participation")!
defaults.set(true, forKey: "\(target.peerID).\(descriptor.name)")
Expand Down
18 changes: 11 additions & 7 deletions Packages/PeerCore/Sources/PeerCore/ConnectionSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ public actor ConnectionSettings {
}
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
// Preserve the existing single connection, including its Keychain and Peer identities.
if let target = try? decoder.decode(ConnectionTarget.self, from: data) {
try target.validate()
return SavedConnections(selected: target.id, connections: [target])
}
let saved = try decoder.decode(SavedConnections.self, from: data)
guard Set(saved.connections.map(\.id)).count == saved.connections.count,
saved.selected == nil || saved.current != nil
Expand All @@ -32,6 +27,13 @@ public actor ConnectionSettings {

public func load() throws -> ConnectionTarget? { try loadAll().current }

public func resolve(_ frozen: ConnectionTarget) throws -> ConnectionTarget {
guard let current = try loadAll().connections.first(where: { $0.id == frozen.id }),
current.hasSameDestination(as: frozen)
else { throw PeerError.needsAction("原目标连接不可用,请恢复该信息库的连接。") }
return current
}

public func select(_ id: UUID) throws -> ConnectionTarget {
var saved = try loadAll()
guard let target = saved.connections.first(where: { $0.id == id }) else {
Expand All @@ -46,6 +48,9 @@ public actor ConnectionSettings {
try target.validate()
var saved = try loadAll()
if let index = saved.connections.firstIndex(where: { $0.id == target.id }) {
guard saved.connections[index].hasSameDestination(as: target) else {
throw PeerError.invalid("修改信息库地址需要创建新连接,已有材料的目标不能改变。")
}
saved.connections[index] = target
} else {
saved.connections.append(target)
Expand Down Expand Up @@ -73,9 +78,8 @@ public actor ConnectionSettings {
if !web.isEmpty && webURL == nil { throw PeerError.invalid("Web 地址无效。") }
let same =
previous?.host == host && previous?.port == port && previous?.database == database
let sameCredentials = same && previous?.username == username
let target = ConnectionTarget(
id: sameCredentials ? previous!.id : UUID(), peerID: same ? previous!.peerID : UUID(),
id: same ? previous!.id : UUID(), peerID: same ? previous!.peerID : UUID(),
host: host, port: port, database: database, username: username, tls: tls,
webURL: webURL,
name: name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
Expand Down
5 changes: 5 additions & 0 deletions Packages/PeerCore/Sources/PeerCore/ConnectionTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public struct ConnectionTarget: Codable, Sendable, Equatable, Identifiable {
self.name = name
}

public func hasSameDestination(as other: Self) -> Bool {
peerID == other.peerID && host == other.host && port == other.port
&& database == other.database
}

public func validate() throws {
guard !host.isEmpty, !host.contains(where: { $0.isWhitespace }), !host.contains("/"),
(1...65535).contains(port), !database.isEmpty, !username.isEmpty,
Expand Down
1 change: 0 additions & 1 deletion Packages/PeerCore/Sources/PeerCore/NativeExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public struct NativeExtensionDescriptor: Codable, Sendable, Equatable {
public protocol NativeExtension: AnyObject {
var descriptor: NativeExtensionDescriptor { get }
func start() throws
func setEnabled(_ enabled: Bool) async throws
func stop() async throws
func showMain()
}
97 changes: 81 additions & 16 deletions Packages/PeerCore/Sources/PeerCore/PeerDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@ public actor PeerDatabase {
let password: String
let client: PostgresClient
let task: Task<Void, Never>
var registered = false
}
private var pools: [UUID: Pool] = [:]

public init() {}

public func client(for target: ConnectionTarget, password: String) throws -> PostgresClient {
try target.validate()
if let pool = pools[target.id], pool.target == target, pool.password == password {
if let pool = pools[target.id], pool.target.host == target.host,
pool.target.port == target.port, pool.target.database == target.database,
pool.target.username == target.username, pool.target.tls == target.tls,
pool.password == password
{
return pool.client
}
pools.removeValue(forKey: target.id)?.task.cancel()
let previous = pools.removeValue(forKey: target.id)
previous?.task.cancel()
var configuration = PostgresClient.Configuration(
host: target.host, port: target.port, username: target.username, password: password,
database: target.database,
Expand All @@ -29,9 +35,16 @@ public actor PeerDatabase {
configuration.options.minimumConnections = 0
configuration.options.maximumConnections = 2
configuration.options.connectTimeout = .seconds(10)
// Bound server work too when a disconnected client cannot deliver cancellation.
configuration.options.additionalStartupParameters = [
("options", "-c statement_timeout=30000")
]
let client = PostgresClient(configuration: configuration, backgroundLogger: Self.logger)
let task = Task { await client.run() }
pools[target.id] = Pool(target: target, password: password, client: client, task: task)
pools[target.id] = Pool(
target: target, password: password, client: client, task: task,
registered: previous?.registered == true
&& previous?.target.hasSameDestination(as: target) == true)
return client
}

Expand All @@ -42,22 +55,74 @@ public actor PeerDatabase {
for task in tasks { await task.value }
}

public func register(_ target: ConnectionTarget, password: String) async throws -> Date {
public func register(_ target: ConnectionTarget, password: String) async throws {
let client = try client(for: target, password: password)
if pools[target.id]?.registered == true { return }
let name = Host.current().localizedName ?? "InKCre Mac"
return try await client.withTransaction(logger: Self.logger) { connection in
try await connection.query(
"""
INSERT INTO inkcre.peers (id,name,application_version,capabilities,config_schema,config,labels)
VALUES (\(target.peerID),\(name),'0.1.0','[]','{}','{}',ARRAY['macOS'])
ON CONFLICT (id) DO UPDATE SET application_version=EXCLUDED.application_version
""", logger: Self.logger)
let rows = try await connection.query(
"SELECT inkcre.renew_peer_lease(\(target.peerID),120)", logger: Self.logger)
for try await expiry in rows.decode(Date.self) {
return expiry.addingTimeInterval(-120)
try await withConnection(for: target, password: password) { connection in
try await connection.withTransaction(logger: Self.logger) { connection in
try await connection.query(
"""
INSERT INTO inkcre.peers (id,name,application_version,capabilities,config_schema,config,labels)
VALUES (\(target.peerID),\(name),'0.1.0','[]','{}','{}',ARRAY['macOS'])
ON CONFLICT (id) DO UPDATE SET application_version=EXCLUDED.application_version
""", logger: Self.logger)
try await connection.query(
"SELECT inkcre.renew_peer_lease(\(target.peerID),120)", logger: Self.logger)
}
throw PeerError.connection("信息库未返回 Peer 联络结果。")
}
if pools[target.id]?.client === client { pools[target.id]?.registered = true }
}

/// Keep row consumption and transactions inside this scope so cancellation closes their lease.
public nonisolated func withConnection<Result: Sendable>(
for target: ConnectionTarget, password: String,
_ operation: @escaping @Sendable (PostgresConnection) async throws -> Result
) async throws -> Result {
let client = try await client(for: target, password: password)
return try await withThrowingTaskGroup(of: Result.self) { group in
group.addTask {
try await client.withConnection { connection in
try await withTaskCancellationHandler {
try Task.checkCancellation()
do { return try await operation(connection) } catch {
if Task.isCancelled { throw CancellationError() }
throw error
}
} onCancel: {
// Closing the socket also interrupts queries whose driver future ignores cancellation.
connection.close().whenFailure { _ in }
}
}
}
group.addTask {
try await Task.sleep(for: .seconds(30))
throw URLError(.timedOut)
}
defer { group.cancelAll() }
return try await group.next()!
}
}

public func renewLeases() async -> [UUID: String] {
await withTaskGroup(of: (UUID, String?).self) { group in
for (id, pool) in pools where pool.registered {
group.addTask {
do {
try await self.withConnection(for: pool.target, password: pool.password) {
connection in
let rows = try await connection.query(
"SELECT inkcre.renew_peer_lease(\(pool.target.peerID),120)",
logger: Self.logger)
for try await _ in rows {}
}
return (id, nil)
} catch { return (id, Self.failure(error).localizedDescription) }
}
}
var failures: [UUID: String] = [:]
for await (id, message) in group { failures[id] = message }
return failures
}
}
}
Loading
Loading