@velarscript/desktop 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +247 -0
- package/dist/build.d.ts +53 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +351 -0
- package/dist/build.js.map +1 -0
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +1967 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +71 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/package-host.d.ts +3 -0
- package/dist/package-host.d.ts.map +1 -0
- package/dist/package-host.js +20 -0
- package/dist/package-host.js.map +1 -0
- package/dist/test-runtime.d.ts +16 -0
- package/dist/test-runtime.d.ts.map +1 -0
- package/dist/test-runtime.js +475 -0
- package/dist/test-runtime.js.map +1 -0
- package/dist/worker.js +546 -0
- package/native/macos/VelarDesktopHost.swift +1258 -0
- package/native/macos/VelarScript.icns +0 -0
- package/native/macos/VelarTerminalHost.swift +203 -0
- package/native/node/project-transactions.js +157 -0
- package/native/node/worker.js +2220 -0
- package/package.json +63 -0
|
Binary file
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
private let frameWrite: UInt8 = 1
|
|
5
|
+
private let frameResize: UInt8 = 2
|
|
6
|
+
private let frameClose: UInt8 = 3
|
|
7
|
+
private let maximumFrameBytes = 1024 * 1024
|
|
8
|
+
private var terminationSignal: Int32 = 0
|
|
9
|
+
|
|
10
|
+
private func fail(_ message: String) -> Never {
|
|
11
|
+
FileHandle.standardError.write(Data(("Velar terminal host: \(message)\n").utf8))
|
|
12
|
+
Darwin.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private func integer(_ value: String, minimum: Int, maximum: Int, name: String) -> Int {
|
|
16
|
+
guard let parsed = Int(value), parsed >= minimum, parsed <= maximum else {
|
|
17
|
+
fail("\(name) must be an integer from \(minimum) through \(maximum)")
|
|
18
|
+
}
|
|
19
|
+
return parsed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private func loginShell() -> String {
|
|
23
|
+
var metadata = stat()
|
|
24
|
+
guard stat("/etc/shells", &metadata) == 0, metadata.st_size >= 1, metadata.st_size <= 64 * 1024,
|
|
25
|
+
let source = try? String(contentsOfFile: "/etc/shells", encoding: .utf8) else {
|
|
26
|
+
fail("the trusted login-shell registry is unavailable")
|
|
27
|
+
}
|
|
28
|
+
let approved = Set(source.split(whereSeparator: { $0 == "\n" || $0 == "\r" })
|
|
29
|
+
.map(String.init)
|
|
30
|
+
.filter({ $0.hasPrefix("/") && !$0.contains("#") }))
|
|
31
|
+
if let account = getpwuid(getuid()), let shell = account.pointee.pw_shell {
|
|
32
|
+
let value = String(cString: shell)
|
|
33
|
+
if approved.contains(value), value.utf8.count <= 4096, access(value, X_OK) == 0 { return value }
|
|
34
|
+
}
|
|
35
|
+
guard approved.contains("/bin/zsh"), access("/bin/zsh", X_OK) == 0 else { fail("no trusted login shell is available") }
|
|
36
|
+
return "/bin/zsh"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func writeAll(_ descriptor: Int32, _ data: Data) -> Bool {
|
|
40
|
+
return data.withUnsafeBytes { bytes in
|
|
41
|
+
guard let start = bytes.baseAddress else { return true }
|
|
42
|
+
var offset = 0
|
|
43
|
+
while offset < data.count {
|
|
44
|
+
let count = Darwin.write(descriptor, start.advanced(by: offset), data.count - offset)
|
|
45
|
+
if count > 0 { offset += count; continue }
|
|
46
|
+
if count == -1 && errno == EINTR { continue }
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private func signalShell(_ pid: pid_t, _ signal: Int32) {
|
|
54
|
+
if Darwin.kill(-pid, signal) == -1 { _ = Darwin.kill(pid, signal) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func childExitCode(_ status: Int32) -> Int32 {
|
|
58
|
+
let signal = status & 0x7f
|
|
59
|
+
if signal == 0 { return (status >> 8) & 0xff }
|
|
60
|
+
if signal != 0x7f { return min(255, 128 + signal) }
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func decodeUInt32(_ data: Data, at offset: Int) -> UInt32 {
|
|
65
|
+
return data[offset..<offset + 4].reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private func run() -> Never {
|
|
69
|
+
guard CommandLine.arguments.count == 4 else { fail("expected project directory, columns, and rows") }
|
|
70
|
+
let directory = CommandLine.arguments[1]
|
|
71
|
+
guard directory.hasPrefix("/"), directory.utf8.count <= 4096, !directory.contains("\0") else {
|
|
72
|
+
fail("project directory must be a bounded absolute path")
|
|
73
|
+
}
|
|
74
|
+
let columns = integer(CommandLine.arguments[2], minimum: 20, maximum: 1000, name: "columns")
|
|
75
|
+
let rows = integer(CommandLine.arguments[3], minimum: 5, maximum: 1000, name: "rows")
|
|
76
|
+
let shell = loginShell()
|
|
77
|
+
var window = winsize(ws_row: UInt16(rows), ws_col: UInt16(columns), ws_xpixel: 0, ws_ypixel: 0)
|
|
78
|
+
var master: Int32 = -1
|
|
79
|
+
let child = forkpty(&master, nil, nil, &window)
|
|
80
|
+
guard child >= 0 else { fail("forkpty failed: \(String(cString: strerror(errno)))") }
|
|
81
|
+
|
|
82
|
+
if child == 0 {
|
|
83
|
+
_ = Darwin.close(3)
|
|
84
|
+
guard chdir(directory) == 0 else { _exit(126) }
|
|
85
|
+
let name = URL(fileURLWithPath: shell).lastPathComponent
|
|
86
|
+
let argument = strdup("-\(name)")
|
|
87
|
+
let path = strdup(shell)
|
|
88
|
+
guard let argument, let path else { _exit(126) }
|
|
89
|
+
var arguments: [UnsafeMutablePointer<CChar>?] = [argument, nil]
|
|
90
|
+
arguments.withUnsafeMutableBufferPointer { buffer in
|
|
91
|
+
_ = execv(path, buffer.baseAddress)
|
|
92
|
+
}
|
|
93
|
+
_exit(126)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let metadata = Data("{\"protocolVersion\":1,\"pid\":\(child)}\n".utf8)
|
|
97
|
+
guard writeAll(3, metadata) else {
|
|
98
|
+
signalShell(child, SIGKILL)
|
|
99
|
+
_ = waitpid(child, nil, 0)
|
|
100
|
+
fail("could not publish shell ownership")
|
|
101
|
+
}
|
|
102
|
+
_ = Darwin.close(3)
|
|
103
|
+
signal(SIGTERM) { value in terminationSignal = value }
|
|
104
|
+
signal(SIGHUP) { value in terminationSignal = value }
|
|
105
|
+
signal(SIGINT) { value in terminationSignal = value }
|
|
106
|
+
|
|
107
|
+
var input = Data()
|
|
108
|
+
var shellStatus: Int32 = 0
|
|
109
|
+
var shellSettled = false
|
|
110
|
+
var masterClosed = false
|
|
111
|
+
var closeRequested = false
|
|
112
|
+
var bytes = [UInt8](repeating: 0, count: 64 * 1024)
|
|
113
|
+
|
|
114
|
+
while !shellSettled || !masterClosed {
|
|
115
|
+
if terminationSignal != 0 && !closeRequested {
|
|
116
|
+
closeRequested = true
|
|
117
|
+
signalShell(child, SIGTERM)
|
|
118
|
+
}
|
|
119
|
+
if !shellSettled {
|
|
120
|
+
let result = waitpid(child, &shellStatus, WNOHANG)
|
|
121
|
+
if result == child { shellSettled = true }
|
|
122
|
+
else if result == -1 && errno != EINTR { shellSettled = true; shellStatus = 1 << 8 }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var descriptors = [
|
|
126
|
+
pollfd(fd: masterClosed ? -1 : master, events: Int16(POLLIN | POLLHUP), revents: 0),
|
|
127
|
+
pollfd(fd: closeRequested ? -1 : STDIN_FILENO, events: Int16(POLLIN | POLLHUP), revents: 0),
|
|
128
|
+
]
|
|
129
|
+
let polled = poll(&descriptors, nfds_t(descriptors.count), shellSettled ? 25 : 100)
|
|
130
|
+
if polled == -1 && errno != EINTR {
|
|
131
|
+
signalShell(child, SIGKILL)
|
|
132
|
+
if !shellSettled { _ = waitpid(child, &shellStatus, 0); shellSettled = true }
|
|
133
|
+
fail("poll failed: \(String(cString: strerror(errno)))")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if !masterClosed && descriptors[0].revents & Int16(POLLIN | POLLHUP | POLLERR) != 0 {
|
|
137
|
+
let count = Darwin.read(master, &bytes, bytes.count)
|
|
138
|
+
if count > 0 {
|
|
139
|
+
if !writeAll(STDOUT_FILENO, Data(bytes[0..<count])) {
|
|
140
|
+
signalShell(child, SIGKILL)
|
|
141
|
+
if !shellSettled { _ = waitpid(child, &shellStatus, 0); shellSettled = true }
|
|
142
|
+
masterClosed = true
|
|
143
|
+
}
|
|
144
|
+
} else if count == 0 || count == -1 && (errno == EIO || errno != EINTR) {
|
|
145
|
+
_ = Darwin.close(master)
|
|
146
|
+
masterClosed = true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if !closeRequested && descriptors[1].revents & Int16(POLLIN | POLLHUP | POLLERR) != 0 {
|
|
151
|
+
let count = Darwin.read(STDIN_FILENO, &bytes, bytes.count)
|
|
152
|
+
if count > 0 { input.append(contentsOf: bytes[0..<count]) }
|
|
153
|
+
else if count == 0 || count == -1 && errno != EINTR {
|
|
154
|
+
closeRequested = true
|
|
155
|
+
signalShell(child, SIGHUP)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
while !closeRequested && input.count >= 5 {
|
|
160
|
+
let kind = input[0]
|
|
161
|
+
let length = Int(decodeUInt32(input, at: 1))
|
|
162
|
+
if length > maximumFrameBytes {
|
|
163
|
+
closeRequested = true
|
|
164
|
+
signalShell(child, SIGKILL)
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
if input.count < 5 + length { break }
|
|
168
|
+
let payload = Data(input[5..<5 + length])
|
|
169
|
+
input.removeSubrange(0..<5 + length)
|
|
170
|
+
if kind == frameWrite {
|
|
171
|
+
if length == 0 || !writeAll(master, payload) {
|
|
172
|
+
closeRequested = true
|
|
173
|
+
signalShell(child, SIGKILL)
|
|
174
|
+
}
|
|
175
|
+
} else if kind == frameResize && length == 8 {
|
|
176
|
+
let nextColumns = decodeUInt32(payload, at: 0)
|
|
177
|
+
let nextRows = decodeUInt32(payload, at: 4)
|
|
178
|
+
if nextColumns < 20 || nextColumns > 1000 || nextRows < 5 || nextRows > 1000 {
|
|
179
|
+
closeRequested = true
|
|
180
|
+
signalShell(child, SIGKILL)
|
|
181
|
+
} else {
|
|
182
|
+
var size = winsize(ws_row: UInt16(nextRows), ws_col: UInt16(nextColumns), ws_xpixel: 0, ws_ypixel: 0)
|
|
183
|
+
if ioctl(master, TIOCSWINSZ, &size) == -1 {
|
|
184
|
+
closeRequested = true
|
|
185
|
+
signalShell(child, SIGKILL)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else if kind == frameClose && length == 0 {
|
|
189
|
+
closeRequested = true
|
|
190
|
+
signalShell(child, SIGHUP)
|
|
191
|
+
} else {
|
|
192
|
+
closeRequested = true
|
|
193
|
+
signalShell(child, SIGKILL)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if shellSettled && masterClosed { break }
|
|
198
|
+
}
|
|
199
|
+
if !shellSettled { _ = waitpid(child, &shellStatus, 0) }
|
|
200
|
+
Darwin.exit(childExitCode(shellStatus))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
run()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { FileProjectChangeFeed } from "@velaros-ai/project/changes";
|
|
6
|
+
import { createProjectKernel } from "@velaros-ai/project/runtime";
|
|
7
|
+
import { createProjectTransactionController } from "@velaros-ai/project/transaction-controller";
|
|
8
|
+
|
|
9
|
+
const MAX_PROJECT_CHANGE_RECORD_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const MAX_PROJECT_CHANGE_PAGE_BYTES = 32 * 1024 * 1024;
|
|
11
|
+
const MAX_PROJECT_CHANGE_ITEMS = 1_000;
|
|
12
|
+
const projectChangeLifecycles = new Set([
|
|
13
|
+
"prepared",
|
|
14
|
+
"amended",
|
|
15
|
+
"validated",
|
|
16
|
+
"validation_failed",
|
|
17
|
+
"applied",
|
|
18
|
+
"rolled_back",
|
|
19
|
+
"discarded",
|
|
20
|
+
]);
|
|
21
|
+
const riskLevels = new Set(["low", "medium", "high"]);
|
|
22
|
+
|
|
23
|
+
function record(value, label) {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function text(value, label, maximumBytes, allowEmpty = false) {
|
|
29
|
+
if (typeof value !== "string" || (!allowEmpty && value.length === 0) || value.includes("\0") || Buffer.byteLength(value, "utf8") > maximumBytes) {
|
|
30
|
+
throw new TypeError(`${label} must be bounded text`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function optionalText(value, label, maximumBytes) {
|
|
36
|
+
return value === undefined ? null : text(value, label, maximumBytes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeInteger(value, label, minimum = 0) {
|
|
40
|
+
if (!Number.isSafeInteger(value) || value < minimum) throw new TypeError(`${label} must be a safe integer`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function list(value, label) {
|
|
45
|
+
if (!Array.isArray(value) || value.length > MAX_PROJECT_CHANGE_ITEMS) throw new TypeError(`${label} must be a bounded list`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function intentView(value) {
|
|
50
|
+
value = record(value, "Project change intent");
|
|
51
|
+
const operation = record(value.operation, "Project change operation");
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
type: text(operation.type, "Project change operation type", 128),
|
|
54
|
+
path: optionalText(operation.path, "Project change operation path", 4096),
|
|
55
|
+
from: optionalText(operation.from, "Project change operation source path", 4096),
|
|
56
|
+
to: optionalText(operation.to, "Project change operation target path", 4096),
|
|
57
|
+
targetId: optionalText(value.targetId, "Project change target id", 512),
|
|
58
|
+
reason: optionalText(value.reason, "Project change intent reason", 64 * 1024),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function patchView(value) {
|
|
63
|
+
value = record(value, "Project change patch");
|
|
64
|
+
if (!riskLevels.has(value.risk)) throw new TypeError("Project change patch risk is invalid");
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
patchId: text(value.patchId, "Project change patch id", 512),
|
|
67
|
+
strategyId: text(value.strategyId, "Project change strategy id", 512),
|
|
68
|
+
path: text(value.path, "Project change patch path", 4096),
|
|
69
|
+
baseRevision: optionalText(value.baseRevision, "Project change patch base revision", 512),
|
|
70
|
+
diff: text(value.diff, "Project change patch diff", MAX_PROJECT_CHANGE_RECORD_BYTES, true),
|
|
71
|
+
changedLines: safeInteger(value.changedLines, "Project change patch changed lines"),
|
|
72
|
+
risk: value.risk,
|
|
73
|
+
operation: optionalText(value.operation, "Project change patch operation", 128),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function revisionView(value) {
|
|
78
|
+
value = record(value, "Project change revision");
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
path: text(value.path, "Project change revision path", 4096),
|
|
81
|
+
before: optionalText(value.before, "Project change prior revision", 512),
|
|
82
|
+
after: optionalText(value.after, "Project change next revision", 512),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function desktopProjectChangeView(value) {
|
|
87
|
+
value = record(value, "Project change");
|
|
88
|
+
if (!projectChangeLifecycles.has(value.lifecycle)) throw new TypeError("Project change lifecycle is invalid");
|
|
89
|
+
if (!riskLevels.has(value.risk)) throw new TypeError("Project change risk is invalid");
|
|
90
|
+
const output = Object.freeze({
|
|
91
|
+
transactionId: text(value.transactionId, "Project change transaction id", 512),
|
|
92
|
+
sequence: safeInteger(value.sequence, "Project change sequence", 1),
|
|
93
|
+
lifecycle: value.lifecycle,
|
|
94
|
+
reason: optionalText(value.reason, "Project change reason", 64 * 1024),
|
|
95
|
+
intents: Object.freeze(list(value.intents, "Project change intents").map(intentView)),
|
|
96
|
+
patches: Object.freeze(list(value.patches, "Project change patches").map(patchView)),
|
|
97
|
+
changedFiles: Object.freeze(list(value.changedFiles, "Project change files")
|
|
98
|
+
.map((path) => text(path, "Project change file path", 4096))),
|
|
99
|
+
diff: text(value.diff, "Project change diff", MAX_PROJECT_CHANGE_RECORD_BYTES, true),
|
|
100
|
+
changedLines: safeInteger(value.changedLines, "Project change changed lines"),
|
|
101
|
+
risk: value.risk,
|
|
102
|
+
revisions: Object.freeze(list(value.revisions, "Project change revisions").map(revisionView)),
|
|
103
|
+
createdAt: safeInteger(value.createdAt, "Project change creation time"),
|
|
104
|
+
updatedAt: safeInteger(value.updatedAt, "Project change update time"),
|
|
105
|
+
appliedAt: value.appliedAt === undefined ? null : safeInteger(value.appliedAt, "Project change apply time"),
|
|
106
|
+
});
|
|
107
|
+
if (Buffer.byteLength(JSON.stringify(output), "utf8") > MAX_PROJECT_CHANGE_RECORD_BYTES) {
|
|
108
|
+
throw new RangeError("Project change record exceeds 16 MiB");
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function desktopProjectChangePage(values, requestedLimit) {
|
|
114
|
+
const limit = safeInteger(requestedLimit, "Project change page limit", 1);
|
|
115
|
+
if (limit > 100) throw new RangeError("Project change page limit cannot exceed 100");
|
|
116
|
+
const changes = [];
|
|
117
|
+
let bytes = 0;
|
|
118
|
+
let truncated = false;
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
if (changes.length >= limit) { truncated = true; break; }
|
|
121
|
+
const change = desktopProjectChangeView(value);
|
|
122
|
+
const changeBytes = Buffer.byteLength(JSON.stringify(change), "utf8");
|
|
123
|
+
if (bytes + changeBytes > MAX_PROJECT_CHANGE_PAGE_BYTES) { truncated = true; break; }
|
|
124
|
+
changes.push(change);
|
|
125
|
+
bytes += changeBytes;
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze({ changes: Object.freeze(changes), truncated });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One host-private owner per canonical project root. The renderer never sees
|
|
132
|
+
* the root-derived key, state path, feed path, policy or underlying kernel.
|
|
133
|
+
*/
|
|
134
|
+
export async function createDesktopProjectTransactionOwner(projectRoot, appDataRoot) {
|
|
135
|
+
const key = createHash("sha256").update(projectRoot).digest("hex");
|
|
136
|
+
const directory = resolve(appDataRoot, "project-transactions", key);
|
|
137
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
138
|
+
const feed = new FileProjectChangeFeed({ path: resolve(directory, "changes.jsonl") });
|
|
139
|
+
try {
|
|
140
|
+
const project = await createProjectKernel({
|
|
141
|
+
root: projectRoot,
|
|
142
|
+
changeFeed: feed,
|
|
143
|
+
transactionStatePath: resolve(directory, "transactions.json"),
|
|
144
|
+
// Desktop's project grant is the outer write authority. The finite
|
|
145
|
+
// apply/rollback call is already explicit and cannot choose an operation.
|
|
146
|
+
corePolicy: { approval: { requireForHighRiskPatch: false } },
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
root: projectRoot,
|
|
150
|
+
controller: createProjectTransactionController(project),
|
|
151
|
+
close() { feed.close(); },
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
feed.close();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|