arcane-os 0.56.0 → 0.57.0
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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-local.mjs +306 -0
- package/docs/reference/README.md +20 -0
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-native-packaging.md +195 -11
- package/docs/reference/inventory/package-api.json +138 -12
- package/docs/reference/local-ai.md +197 -0
- package/docs/reference/protocols.md +69 -1
- package/docs/reference/runtime-modules.md +6 -5
- package/docs/reference/sdk-api.md +165 -26
- package/package.json +5 -1
- package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
- package/schemas/arcane-app.schema.json +54 -0
- package/schemas/arcane-package.schema.json +44 -0
- package/src/app-descriptor.mjs +17 -1
- package/src/cli/main.mjs +8 -2
- package/src/core/host.mjs +23 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +3 -3
- package/src/core/hosts/windows/ArcaneHost.cs +695 -0
- package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
- package/src/core/services/local-ai.mjs +543 -0
- package/src/dev-server.mjs +7 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/archive.mjs +52 -0
- package/src/local-ai/config.mjs +62 -0
- package/src/local-ai/dev.mjs +390 -0
- package/src/local-ai/discover.mjs +71 -0
- package/src/local-ai/http.mjs +106 -0
- package/src/local-ai/install.mjs +344 -0
- package/src/local-ai/server.mjs +262 -0
- package/src/native/portable-layout.mjs +29 -4
- package/src/native/portable-provider.mjs +39 -8
- package/src/native/windows-provider.mjs +262 -0
- package/src/native-provider-loader.mjs +8 -2
- package/src/packager/core.mjs +6 -2
- package/src/process.mjs +66 -8
- package/src/toolchain.mjs +33 -5
package/src/core/host.mjs
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import Is from 'strong-type';
|
|
1
3
|
import {createCoreRuntime} from './runtime.mjs';
|
|
2
4
|
import {startCoreStdio} from './stdio.mjs';
|
|
3
5
|
|
|
6
|
+
const is = new Is(false);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read only the explicitly supplied --arcane-launch-config filename.
|
|
10
|
+
* Return the complete JSON object for the service factories' second argument;
|
|
11
|
+
* field meanings belong to the launch owner and individual services.
|
|
12
|
+
*/
|
|
13
|
+
export async function readCoreLaunchContext({argv = process.argv.slice(2)} = {}) {
|
|
14
|
+
const flag = argv.indexOf('--arcane-launch-config');
|
|
15
|
+
if (flag === -1) return {};
|
|
16
|
+
const filename = argv[flag + 1];
|
|
17
|
+
if (filename === undefined || filename === '') {
|
|
18
|
+
throw new TypeError('--arcane-launch-config requires a filename.');
|
|
19
|
+
}
|
|
20
|
+
const context = JSON.parse(await readFile(filename, 'utf8'));
|
|
21
|
+
if (context === null || !is.object(context) || is.array(context)) {
|
|
22
|
+
throw new TypeError('Core launch configuration must be a JSON object.');
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
/** Compose app services with the shared dispatcher and draining transport. */
|
|
5
28
|
export function startCoreHost({application, version, services = [], input, output, onError, signal} = {}) {
|
|
6
29
|
const runtime = createCoreRuntime({application, version, services});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
@class ArcaneCoreProcess;
|
|
6
|
+
|
|
7
|
+
typedef NS_ENUM(NSUInteger, ArcaneCoreOutputStream) {
|
|
8
|
+
ArcaneCoreStandardOutput,
|
|
9
|
+
ArcaneCoreStandardError
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Callbacks run on process-owned background queues. Each output stream retains
|
|
14
|
+
* its order; different streams, writes and exit observation can be concurrent.
|
|
15
|
+
* Marshal UI work at the host. Callbacks must return normally and must not wait
|
|
16
|
+
* for another callback or for completion. The process retains its delegate
|
|
17
|
+
* through didCompleteWithErrors, then releases it.
|
|
18
|
+
*
|
|
19
|
+
* receivedOutputData delivers every unmodified stdout/stderr chunk. Stdout
|
|
20
|
+
* includes framing; receivedFrameData additionally delivers each complete
|
|
21
|
+
* opaque JSON body. No payload is decoded, parsed or rewritten here. Following
|
|
22
|
+
* a framing error, raw stdout continues to EOF without guessing a new boundary.
|
|
23
|
+
*/
|
|
24
|
+
@protocol ArcaneCoreProcessDelegate <NSObject>
|
|
25
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process didLaunch:(pid_t)processIdentifier;
|
|
26
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process receivedFrameData:(NSData *)data;
|
|
27
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process receivedOutputData:(NSData *)data
|
|
28
|
+
stream:(ArcaneCoreOutputStream)stream;
|
|
29
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process didEncounterError:(NSError *)error;
|
|
30
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process didExitWithStatus:(int)status
|
|
31
|
+
reason:(NSTaskTerminationReason)reason;
|
|
32
|
+
/**
|
|
33
|
+
* Last callback: launch/exit observation, accepted writes and both streams have
|
|
34
|
+
* settled, successfully or with reported errors. Launch failure has no exit
|
|
35
|
+
* callback; an exit-observation failure is not proof that the child exited.
|
|
36
|
+
*/
|
|
37
|
+
- (void)coreProcess:(ArcaneCoreProcess *)process didCompleteWithErrors:(NSArray<NSError *> *)errors;
|
|
38
|
+
@end
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Foundation-only ownership of one explicit Core executable and framed pipes.
|
|
42
|
+
* Compile with Objective-C ARC and blocks; no third-party runtime is linked.
|
|
43
|
+
*/
|
|
44
|
+
API_AVAILABLE(macos(11.0))
|
|
45
|
+
@interface ArcaneCoreProcess : NSObject
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Starts asynchronously without shell execution, PATH lookup, environment
|
|
49
|
+
* mutation or a foreground wait. Arguments exclude argv[0]; the selected child
|
|
50
|
+
* inherits the host environment. Launch failure is reported through the
|
|
51
|
+
* delegate and completion. Callbacks can begin before this method returns.
|
|
52
|
+
*/
|
|
53
|
+
+ (instancetype)startExecutableURL:(NSURL *)executableURL
|
|
54
|
+
arguments:(NSArray<NSString *> *)arguments
|
|
55
|
+
workingDirectoryURL:(NSURL *)workingDirectoryURL
|
|
56
|
+
delegate:(id<ArcaneCoreProcessDelegate>)delegate;
|
|
57
|
+
|
|
58
|
+
/** Zero until a successful launch; the original child PID remains after exit. */
|
|
59
|
+
@property(nonatomic, readonly) pid_t processIdentifier;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Copies and queues the complete opaque JSON body. YES accepts the write and
|
|
63
|
+
* calls completion exactly once on the input queue. nil completion-error means
|
|
64
|
+
* the full frame reached child stdin, not that Core accepted or completed its
|
|
65
|
+
* operation. NO supplies a synchronous error and never calls completion.
|
|
66
|
+
* A partial/failed write is never retried. Payload length is transport framing
|
|
67
|
+
* only, never an application limit or policy.
|
|
68
|
+
*/
|
|
69
|
+
- (BOOL)sendJSONData:(NSData *)data
|
|
70
|
+
completion:(void (^)(NSError * _Nullable error))completion
|
|
71
|
+
error:(NSError * _Nullable * _Nullable)error;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Idempotent and nonblocking: reject new sends, finish accepted writes, then
|
|
75
|
+
* close stdin. Keep reading stdout/stderr and observe child exit. There is no
|
|
76
|
+
* forced termination or deadline. Accepted service work can outlive a renderer;
|
|
77
|
+
* renderer cancellation belongs to the public Core control protocol.
|
|
78
|
+
*/
|
|
79
|
+
- (void)closeInput;
|
|
80
|
+
|
|
81
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
82
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
|
83
|
+
@end
|
|
84
|
+
|
|
85
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#import "ArcaneCoreProcess.h"
|
|
2
|
+
#import <dispatch/dispatch.h>
|
|
3
|
+
#import <errno.h>
|
|
4
|
+
#import <fcntl.h>
|
|
5
|
+
|
|
6
|
+
static NSError *ArcaneProcessError(NSString *code, NSString *message,
|
|
7
|
+
NSString *operation, NSError *underlying) {
|
|
8
|
+
NSMutableDictionary *details = [@{NSLocalizedDescriptionKey: message,
|
|
9
|
+
@"code": code, @"operation": operation} mutableCopy];
|
|
10
|
+
if (underlying) details[NSUnderlyingErrorKey] = underlying;
|
|
11
|
+
return [NSError errorWithDomain:@"ArcaneCoreProcess" code:1 userInfo:details];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static NSError *ArcaneProcessException(NSException *exception, NSString *operation) {
|
|
15
|
+
return [NSError errorWithDomain:@"ArcaneCoreProcess" code:1 userInfo:@{
|
|
16
|
+
NSLocalizedDescriptionKey: exception.reason ?: exception.name,
|
|
17
|
+
@"code": @"CORE_PROCESS_EXCEPTION", @"operation": operation,
|
|
18
|
+
@"exceptionName": exception.name, @"exceptionInfo": exception.userInfo ?: @{},
|
|
19
|
+
@"callStackSymbols": exception.callStackSymbols
|
|
20
|
+
}];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@interface ArcaneCoreProcess () {
|
|
24
|
+
NSURL *_executableURL;
|
|
25
|
+
NSArray<NSString *> *_arguments;
|
|
26
|
+
NSURL *_workingDirectoryURL;
|
|
27
|
+
id<ArcaneCoreProcessDelegate> _delegate;
|
|
28
|
+
NSLock *_state;
|
|
29
|
+
NSMutableArray<NSError *> *_errors;
|
|
30
|
+
dispatch_queue_t _inputQueue;
|
|
31
|
+
dispatch_group_t _lifetime;
|
|
32
|
+
NSTask *_task;
|
|
33
|
+
NSFileHandle *_input;
|
|
34
|
+
NSError *_inputFailure;
|
|
35
|
+
BOOL _accepting;
|
|
36
|
+
BOOL _closeQueued;
|
|
37
|
+
pid_t _processIdentifier;
|
|
38
|
+
}
|
|
39
|
+
- (instancetype)initExecutableURL:(NSURL *)executableURL
|
|
40
|
+
arguments:(NSArray<NSString *> *)arguments
|
|
41
|
+
workingDirectoryURL:(NSURL *)workingDirectoryURL
|
|
42
|
+
delegate:(id<ArcaneCoreProcessDelegate>)delegate;
|
|
43
|
+
@end
|
|
44
|
+
|
|
45
|
+
@implementation ArcaneCoreProcess
|
|
46
|
+
|
|
47
|
+
+ (instancetype)startExecutableURL:(NSURL *)executableURL
|
|
48
|
+
arguments:(NSArray<NSString *> *)arguments
|
|
49
|
+
workingDirectoryURL:(NSURL *)workingDirectoryURL
|
|
50
|
+
delegate:(id<ArcaneCoreProcessDelegate>)delegate {
|
|
51
|
+
if (!executableURL || !arguments || !workingDirectoryURL || !delegate) {
|
|
52
|
+
[NSException raise:NSInvalidArgumentException
|
|
53
|
+
format:@"Select a Core executable, arguments, working directory and delegate."];
|
|
54
|
+
}
|
|
55
|
+
ArcaneCoreProcess *process = [[self alloc] initExecutableURL:executableURL
|
|
56
|
+
arguments:arguments workingDirectoryURL:workingDirectoryURL delegate:delegate];
|
|
57
|
+
[process start];
|
|
58
|
+
return process;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- (instancetype)initExecutableURL:(NSURL *)executableURL
|
|
62
|
+
arguments:(NSArray<NSString *> *)arguments
|
|
63
|
+
workingDirectoryURL:(NSURL *)workingDirectoryURL
|
|
64
|
+
delegate:(id<ArcaneCoreProcessDelegate>)delegate {
|
|
65
|
+
self = [super init];
|
|
66
|
+
if (self) {
|
|
67
|
+
_executableURL = [executableURL copy];
|
|
68
|
+
_arguments = [[NSArray alloc] initWithArray:arguments copyItems:YES];
|
|
69
|
+
_workingDirectoryURL = [workingDirectoryURL copy];
|
|
70
|
+
_delegate = delegate;
|
|
71
|
+
_state = [[NSLock alloc] init];
|
|
72
|
+
_errors = [NSMutableArray array];
|
|
73
|
+
_inputQueue = dispatch_queue_create("arcane.core.macos.input", DISPATCH_QUEUE_SERIAL);
|
|
74
|
+
_lifetime = dispatch_group_create();
|
|
75
|
+
_accepting = YES;
|
|
76
|
+
}
|
|
77
|
+
return self;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
- (pid_t)processIdentifier {
|
|
81
|
+
[_state lock];
|
|
82
|
+
pid_t result = _processIdentifier;
|
|
83
|
+
[_state unlock];
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (void)reportError:(NSError *)error {
|
|
88
|
+
[_state lock];
|
|
89
|
+
[_errors addObject:error];
|
|
90
|
+
[_state unlock];
|
|
91
|
+
[_delegate coreProcess:self didEncounterError:error];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (void)start {
|
|
95
|
+
// Startup and stdin each retain ownership until their actual completion.
|
|
96
|
+
dispatch_group_enter(_lifetime);
|
|
97
|
+
dispatch_group_enter(_lifetime);
|
|
98
|
+
dispatch_async(_inputQueue, ^{ [self launch]; });
|
|
99
|
+
dispatch_group_notify(_lifetime, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
100
|
+
[self complete];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)launch {
|
|
105
|
+
@autoreleasepool {
|
|
106
|
+
NSPipe *inputPipe = nil;
|
|
107
|
+
NSPipe *outputPipe = nil;
|
|
108
|
+
NSPipe *errorPipe = nil;
|
|
109
|
+
NSError *launchError = nil;
|
|
110
|
+
BOOL launched = NO;
|
|
111
|
+
@try {
|
|
112
|
+
inputPipe = [NSPipe pipe];
|
|
113
|
+
outputPipe = [NSPipe pipe];
|
|
114
|
+
errorPipe = [NSPipe pipe];
|
|
115
|
+
_input = inputPipe.fileHandleForWriting;
|
|
116
|
+
_task = [[NSTask alloc] init];
|
|
117
|
+
_task.executableURL = _executableURL;
|
|
118
|
+
_task.arguments = _arguments;
|
|
119
|
+
_task.currentDirectoryURL = _workingDirectoryURL;
|
|
120
|
+
_task.standardInput = inputPipe;
|
|
121
|
+
_task.standardOutput = outputPipe;
|
|
122
|
+
_task.standardError = errorPipe;
|
|
123
|
+
// A closed child pipe must produce an observable EPIPE write error,
|
|
124
|
+
// not terminate the host. This affects this descriptor only.
|
|
125
|
+
if (fcntl(_input.fileDescriptor, F_SETNOSIGPIPE, 1) == -1) {
|
|
126
|
+
launchError = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
|
|
127
|
+
} else {
|
|
128
|
+
launched = [_task launchAndReturnError:&launchError];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
@catch (NSException *exception) { launchError = ArcaneProcessException(exception, @"launch"); }
|
|
132
|
+
if (!launched) {
|
|
133
|
+
_inputFailure = ArcaneProcessError(@"CORE_PROCESS_START_FAILED",
|
|
134
|
+
@"The selected Core executable did not start.", @"launch", launchError);
|
|
135
|
+
[self reportError:_inputFailure];
|
|
136
|
+
[self closeInput];
|
|
137
|
+
// No child owns these handles after a failed launch. Their pipe
|
|
138
|
+
// objects release unused ends; stdin still follows the input queue.
|
|
139
|
+
_task = nil;
|
|
140
|
+
dispatch_group_leave(_lifetime);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
[_state lock];
|
|
144
|
+
_processIdentifier = _task.processIdentifier;
|
|
145
|
+
[_state unlock];
|
|
146
|
+
[_delegate coreProcess:self didLaunch:_processIdentifier];
|
|
147
|
+
|
|
148
|
+
// Exactly two stream readers and one exit observer per child. Each
|
|
149
|
+
// reader invokes its consumer before reading again, applying stream
|
|
150
|
+
// backpressure without a second unbounded callback queue.
|
|
151
|
+
dispatch_group_async(_lifetime, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
152
|
+
[self readOutput:outputPipe.fileHandleForReading stream:ArcaneCoreStandardOutput];
|
|
153
|
+
});
|
|
154
|
+
dispatch_group_async(_lifetime, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
155
|
+
[self readOutput:errorPipe.fileHandleForReading stream:ArcaneCoreStandardError];
|
|
156
|
+
});
|
|
157
|
+
dispatch_group_async(_lifetime, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
158
|
+
[self observeExit];
|
|
159
|
+
});
|
|
160
|
+
dispatch_group_leave(_lifetime);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (BOOL)sendJSONData:(NSData *)data
|
|
165
|
+
completion:(void (^)(NSError * _Nullable error))completion
|
|
166
|
+
error:(NSError * _Nullable * _Nullable)error {
|
|
167
|
+
if (!data || !completion) {
|
|
168
|
+
if (error) *error = ArcaneProcessError(@"CORE_PROCESS_SEND_INVALID",
|
|
169
|
+
@"Supply complete JSON data and a write completion callback.", @"write", nil);
|
|
170
|
+
return NO;
|
|
171
|
+
}
|
|
172
|
+
NSData *body = [data copy];
|
|
173
|
+
[_state lock];
|
|
174
|
+
if (!_accepting) {
|
|
175
|
+
[_state unlock];
|
|
176
|
+
if (error) *error = ArcaneProcessError(@"CORE_PROCESS_INPUT_CLOSED",
|
|
177
|
+
@"Core input no longer accepts requests.", @"write", nil);
|
|
178
|
+
return NO;
|
|
179
|
+
}
|
|
180
|
+
dispatch_group_enter(_lifetime);
|
|
181
|
+
// Enqueue under the same lock as closeInput: every accepted frame precedes
|
|
182
|
+
// the close, even when callers submit and close from different threads.
|
|
183
|
+
dispatch_async(_inputQueue, ^{
|
|
184
|
+
@autoreleasepool {
|
|
185
|
+
NSError *writeError = self->_inputFailure;
|
|
186
|
+
if (!writeError) {
|
|
187
|
+
NSString *header = [NSString stringWithFormat:@"Content-Length: %lu\r\n\r\n",
|
|
188
|
+
(unsigned long)body.length];
|
|
189
|
+
@try {
|
|
190
|
+
if (![self->_input writeData:[header dataUsingEncoding:NSASCIIStringEncoding] error:&writeError]
|
|
191
|
+
|| ![self->_input writeData:body error:&writeError]) {
|
|
192
|
+
writeError = ArcaneProcessError(@"CORE_PROCESS_WRITE_FAILED",
|
|
193
|
+
@"The complete Core frame could not be written.", @"write", writeError);
|
|
194
|
+
}
|
|
195
|
+
} @catch (NSException *exception) {
|
|
196
|
+
writeError = ArcaneProcessException(exception, @"write");
|
|
197
|
+
}
|
|
198
|
+
if (writeError) {
|
|
199
|
+
self->_inputFailure = writeError;
|
|
200
|
+
[self reportError:writeError];
|
|
201
|
+
[self closeInput];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
completion(writeError);
|
|
205
|
+
dispatch_group_leave(self->_lifetime);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
[_state unlock];
|
|
209
|
+
return YES;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
- (void)closeInput {
|
|
213
|
+
[_state lock];
|
|
214
|
+
if (_closeQueued) { [_state unlock]; return; }
|
|
215
|
+
_accepting = NO;
|
|
216
|
+
_closeQueued = YES;
|
|
217
|
+
dispatch_async(_inputQueue, ^{
|
|
218
|
+
[self closeHandle:self->_input operation:@"stdin.close"];
|
|
219
|
+
self->_input = nil;
|
|
220
|
+
dispatch_group_leave(self->_lifetime);
|
|
221
|
+
});
|
|
222
|
+
[_state unlock];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (void)closeHandle:(NSFileHandle *)handle operation:(NSString *)operation {
|
|
226
|
+
if (!handle) return;
|
|
227
|
+
NSError *error = nil;
|
|
228
|
+
@try {
|
|
229
|
+
if (![handle closeAndReturnError:&error]) {
|
|
230
|
+
error = ArcaneProcessError(@"CORE_PROCESS_CLOSE_FAILED",
|
|
231
|
+
@"A Core pipe could not be closed.", operation, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
@catch (NSException *exception) { error = ArcaneProcessException(exception, operation); }
|
|
235
|
+
if (error) [self reportError:error];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
- (BOOL)readContentLength:(NSData *)header result:(NSUInteger *)result {
|
|
239
|
+
NSString *text = [[NSString alloc] initWithData:header encoding:NSASCIIStringEncoding];
|
|
240
|
+
if (!text) return NO;
|
|
241
|
+
BOOL found = NO;
|
|
242
|
+
for (NSString *line in [text componentsSeparatedByString:@"\r\n"]) {
|
|
243
|
+
NSRange colon = [line rangeOfString:@":"];
|
|
244
|
+
if (colon.location == NSNotFound) continue;
|
|
245
|
+
if ([[line substringToIndex:colon.location] caseInsensitiveCompare:@"Content-Length"] != NSOrderedSame) continue;
|
|
246
|
+
if (found) return NO;
|
|
247
|
+
NSString *value = [[line substringFromIndex:colon.location + 1]
|
|
248
|
+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
249
|
+
if (!value.length) return NO;
|
|
250
|
+
NSUInteger length = 0;
|
|
251
|
+
for (NSUInteger index = 0; index < value.length; index++) {
|
|
252
|
+
unichar digit = [value characterAtIndex:index];
|
|
253
|
+
if (digit < '0' || digit > '9') return NO;
|
|
254
|
+
NSUInteger next = (NSUInteger)(digit - '0');
|
|
255
|
+
// This is representability of the unavoidable framing field.
|
|
256
|
+
if (length > (NSUIntegerMax - next) / 10) return NO;
|
|
257
|
+
length = length * 10 + next;
|
|
258
|
+
}
|
|
259
|
+
*result = length;
|
|
260
|
+
found = YES;
|
|
261
|
+
}
|
|
262
|
+
return found;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
- (void)readOutput:(NSFileHandle *)handle stream:(ArcaneCoreOutputStream)stream {
|
|
266
|
+
@autoreleasepool {
|
|
267
|
+
NSMutableData *pending = [NSMutableData data];
|
|
268
|
+
NSData *separator = [@"\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding];
|
|
269
|
+
BOOL readingBody = NO;
|
|
270
|
+
BOOL decoding = YES;
|
|
271
|
+
NSUInteger expected = 0;
|
|
272
|
+
BOOL reachedEOF = NO;
|
|
273
|
+
while (!reachedEOF) {
|
|
274
|
+
@autoreleasepool {
|
|
275
|
+
NSError *readError = nil;
|
|
276
|
+
NSData *chunk = nil;
|
|
277
|
+
@try { chunk = [handle readDataUpToLength:65536 error:&readError]; }
|
|
278
|
+
@catch (NSException *exception) { readError = ArcaneProcessException(exception, @"read"); }
|
|
279
|
+
if (!chunk) {
|
|
280
|
+
[self reportError:ArcaneProcessError(@"CORE_PROCESS_READ_FAILED",
|
|
281
|
+
@"A Core output stream could not be read.",
|
|
282
|
+
stream == ArcaneCoreStandardOutput ? @"stdout.read" : @"stderr.read", readError)];
|
|
283
|
+
[self closeInput];
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (!chunk.length) { reachedEOF = YES; break; }
|
|
287
|
+
[_delegate coreProcess:self receivedOutputData:chunk stream:stream];
|
|
288
|
+
if (stream != ArcaneCoreStandardOutput || !decoding) continue;
|
|
289
|
+
[pending appendData:chunk];
|
|
290
|
+
for (;;) {
|
|
291
|
+
if (!readingBody) {
|
|
292
|
+
NSRange marker = [pending rangeOfData:separator options:0
|
|
293
|
+
range:NSMakeRange(0, pending.length)];
|
|
294
|
+
if (marker.location == NSNotFound) break;
|
|
295
|
+
NSData *header = [pending subdataWithRange:NSMakeRange(0, marker.location)];
|
|
296
|
+
if (![self readContentLength:header result:&expected]) {
|
|
297
|
+
decoding = NO;
|
|
298
|
+
[self reportError:ArcaneProcessError(@"IPC_LENGTH_INVALID",
|
|
299
|
+
@"Core stdout contains an unreadable Content-Length frame header.", @"stdout.frame", nil)];
|
|
300
|
+
[self closeInput];
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
[pending replaceBytesInRange:NSMakeRange(0, NSMaxRange(marker)) withBytes:NULL length:0];
|
|
304
|
+
readingBody = YES;
|
|
305
|
+
}
|
|
306
|
+
if (pending.length < expected) break;
|
|
307
|
+
NSData *frame = [pending subdataWithRange:NSMakeRange(0, expected)];
|
|
308
|
+
[pending replaceBytesInRange:NSMakeRange(0, expected) withBytes:NULL length:0];
|
|
309
|
+
readingBody = NO;
|
|
310
|
+
[_delegate coreProcess:self receivedFrameData:frame];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (stream == ArcaneCoreStandardOutput && reachedEOF && decoding && (readingBody || pending.length)) {
|
|
315
|
+
[self reportError:ArcaneProcessError(@"IPC_FRAME_INCOMPLETE",
|
|
316
|
+
@"Core stdout ended during a frame.", @"stdout.frame", nil)];
|
|
317
|
+
[self closeInput];
|
|
318
|
+
}
|
|
319
|
+
[self closeHandle:handle operation:stream == ArcaneCoreStandardOutput ? @"stdout.close" : @"stderr.close"];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
- (void)observeExit {
|
|
324
|
+
@autoreleasepool {
|
|
325
|
+
// Only this background owner waits. Output readers remain independent.
|
|
326
|
+
int status;
|
|
327
|
+
NSTaskTerminationReason reason;
|
|
328
|
+
@try {
|
|
329
|
+
[_task waitUntilExit];
|
|
330
|
+
status = _task.terminationStatus;
|
|
331
|
+
reason = _task.terminationReason;
|
|
332
|
+
} @catch (NSException *exception) {
|
|
333
|
+
[self reportError:ArcaneProcessException(exception, @"exit.observe")];
|
|
334
|
+
[self closeInput];
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
[self closeInput];
|
|
338
|
+
if (status != 0 || reason != NSTaskTerminationReasonExit) {
|
|
339
|
+
[self reportError:[NSError errorWithDomain:@"ArcaneCoreProcess" code:status userInfo:@{
|
|
340
|
+
NSLocalizedDescriptionKey: @"Core exited without a successful normal completion.",
|
|
341
|
+
@"code": @"CORE_PROCESS_EXIT_FAILED", @"operation": @"exit",
|
|
342
|
+
@"terminationStatus": @(status), @"terminationReason": @(reason)
|
|
343
|
+
}]];
|
|
344
|
+
}
|
|
345
|
+
[_delegate coreProcess:self didExitWithStatus:status reason:reason];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
- (void)complete {
|
|
350
|
+
[_state lock];
|
|
351
|
+
NSArray<NSError *> *errors = [_errors copy];
|
|
352
|
+
[_state unlock];
|
|
353
|
+
[_delegate coreProcess:self didCompleteWithErrors:errors];
|
|
354
|
+
_delegate = nil;
|
|
355
|
+
_task = nil;
|
|
356
|
+
}
|
|
357
|
+
@end
|
|
@@ -94,8 +94,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
94
94
|
throw failure;
|
|
95
95
|
}
|
|
96
96
|
// All callbacks and exit observation exist before either reader starts.
|
|
97
|
-
host.outputReader = Task.Run(host.ReadOutputAsync);
|
|
98
|
-
host.diagnosticReader = Task.Run(host.ReadDiagnosticsAsync);
|
|
97
|
+
host.outputReader = Task.Run(new Func<Task>(host.ReadOutputAsync));
|
|
98
|
+
host.diagnosticReader = Task.Run(new Func<Task>(host.ReadDiagnosticsAsync));
|
|
99
99
|
host.lifetime = host.CompleteAsync();
|
|
100
100
|
host.lifetime.ContinueWith(host.ObserveLifetimeFailure, CancellationToken.None,
|
|
101
101
|
TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
|
|
@@ -164,7 +164,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
164
164
|
{
|
|
165
165
|
if (inputClosing != null) return inputClosing;
|
|
166
166
|
accepting = false;
|
|
167
|
-
inputClosing = writes.ContinueWith(
|
|
167
|
+
inputClosing = writes.ContinueWith<Task>(
|
|
168
168
|
CloseInputAfterWritesAsync, CancellationToken.None,
|
|
169
169
|
TaskContinuationOptions.None, TaskScheduler.Default).Unwrap();
|
|
170
170
|
return inputClosing;
|