@expo/build-tools 20.5.1 → 21.0.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/dist/android/gradle.d.ts +5 -0
- package/dist/android/gradle.js +13 -33
- package/dist/common/installDependencies.js +21 -18
- package/dist/context.js +31 -1
- package/dist/steps/easFunctions.js +18 -0
- package/dist/steps/functions/capturePosthogEvent.d.ts +2 -0
- package/dist/steps/functions/capturePosthogEvent.js +79 -0
- package/dist/steps/functions/createPosthogAnnotation.d.ts +2 -0
- package/dist/steps/functions/createPosthogAnnotation.js +75 -0
- package/dist/steps/functions/finishIosSimulatorRecordings.d.ts +2 -0
- package/dist/steps/functions/finishIosSimulatorRecordings.js +24 -0
- package/dist/steps/functions/rolloutPosthogFlag.d.ts +2 -0
- package/dist/steps/functions/rolloutPosthogFlag.js +208 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +21 -8
- package/dist/steps/functions/startArgentRemoteSession.d.ts +14 -0
- package/dist/steps/functions/startArgentRemoteSession.js +105 -44
- package/dist/steps/functions/startIosSimulatorRecordings.d.ts +2 -0
- package/dist/steps/functions/startIosSimulatorRecordings.js +20 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +7 -5
- package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.d.ts +3 -0
- package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.js +91 -0
- package/dist/steps/functions/uploadPosthogSourcemaps.d.ts +2 -0
- package/dist/steps/functions/uploadPosthogSourcemaps.js +83 -0
- package/dist/steps/functions/waitForPosthogMetric.d.ts +2 -0
- package/dist/steps/functions/waitForPosthogMetric.js +135 -0
- package/dist/steps/functions/waitForPosthogQuery.d.ts +2 -0
- package/dist/steps/functions/waitForPosthogQuery.js +95 -0
- package/dist/steps/utils/IosSimulatorRecordingUtils.d.ts +17 -0
- package/dist/steps/utils/IosSimulatorRecordingUtils.js +218 -0
- package/dist/steps/utils/PosthogClient.d.ts +46 -0
- package/dist/steps/utils/PosthogClient.js +156 -0
- package/dist/steps/utils/PosthogUtils.d.ts +41 -0
- package/dist/steps/utils/PosthogUtils.js +65 -0
- package/dist/steps/utils/agentDeviceArtifacts.d.ts +27 -0
- package/dist/steps/utils/agentDeviceArtifacts.js +133 -0
- package/dist/steps/utils/android/gradle.d.ts +5 -0
- package/dist/steps/utils/android/gradle.js +13 -62
- package/dist/steps/utils/argentArtifacts.d.ts +5 -3
- package/dist/steps/utils/argentArtifacts.js +95 -23
- package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +3 -1
- package/dist/steps/utils/deviceRunSessionArtifacts.js +6 -2
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +8 -0
- package/dist/steps/utils/remoteDeviceRunSession.js +62 -1
- package/dist/utils/AndroidEmulatorUtils.js +3 -0
- package/dist/utils/IosSimulatorUtils.d.ts +1 -0
- package/dist/utils/IosSimulatorUtils.js +41 -2
- package/dist/utils/expoUpdatesEmbedded.js +4 -0
- package/dist/utils/processes.d.ts +1 -0
- package/dist/utils/processes.js +23 -0
- package/package.json +9 -8
- package/resources/record-sim/Package.swift +28 -0
- package/resources/record-sim/README.md +79 -0
- package/resources/record-sim/Sources/RecordSim/FramebufferDisplaySource.swift +192 -0
- package/resources/record-sim/Sources/RecordSim/PrivateSimulatorServices.swift +96 -0
- package/resources/record-sim/Sources/RecordSim/RecordingModels.swift +70 -0
- package/resources/record-sim/Sources/RecordSim/RecordingOutputWriter.swift +136 -0
- package/resources/record-sim/Sources/RecordSim/SimulatorRecorder.swift +632 -0
- package/resources/record-sim/Sources/RecordSim/Utilities.swift +60 -0
- package/resources/record-sim/Sources/record-sim/main.swift +143 -0
|
@@ -13,12 +13,14 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
|
13
13
|
const node_os_1 = __importDefault(require("node:os"));
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const promises_2 = require("node:stream/promises");
|
|
16
|
+
const promises_3 = require("node:timers/promises");
|
|
16
17
|
const zod_1 = require("zod");
|
|
17
18
|
const sentry_1 = require("../../sentry");
|
|
18
19
|
const artifacts_1 = require("../../utils/artifacts");
|
|
19
|
-
const retry_1 = require("../../utils/retry");
|
|
20
20
|
const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
|
|
21
21
|
const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
|
|
22
|
+
const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
|
|
23
|
+
const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
|
|
22
24
|
const ArgentArtifactSchema = zod_1.z.object({
|
|
23
25
|
id: zod_1.z.string(),
|
|
24
26
|
filename: zod_1.z.string(),
|
|
@@ -28,46 +30,73 @@ const ArgentArtifactSchema = zod_1.z.object({
|
|
|
28
30
|
const ArgentArtifactsListResponseSchema = zod_1.z.object({
|
|
29
31
|
artifacts: zod_1.z.array(ArgentArtifactSchema),
|
|
30
32
|
});
|
|
31
|
-
async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, }) {
|
|
33
|
+
async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, signal, }) {
|
|
32
34
|
logger.info('Started polling Argent tool-server for artifacts.');
|
|
33
35
|
const seenArtifactIds = new Set();
|
|
36
|
+
const pendingUploads = new Set();
|
|
34
37
|
let listArtifactsErrorCount = 0;
|
|
35
|
-
|
|
38
|
+
const queueArtifactUpload = (artifact) => {
|
|
39
|
+
if (seenArtifactIds.has(artifact.id)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
seenArtifactIds.add(artifact.id);
|
|
43
|
+
const uploadPromise = uploadArgentArtifactAsync(ctx, {
|
|
44
|
+
deviceRunSessionId,
|
|
45
|
+
toolsUrl,
|
|
46
|
+
toolsAuthToken,
|
|
47
|
+
artifact,
|
|
48
|
+
logger,
|
|
49
|
+
}).catch(err => {
|
|
50
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
51
|
+
sentry_1.Sentry.capture('Could not upload Argent remote session artifact', error);
|
|
52
|
+
logger.warn({ err: error }, 'Could not upload Argent remote session artifact.');
|
|
53
|
+
});
|
|
54
|
+
pendingUploads.add(uploadPromise);
|
|
55
|
+
void uploadPromise.finally(() => {
|
|
56
|
+
pendingUploads.delete(uploadPromise);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const listArtifactsForUploadAsync = async ({ signal, } = {}) => {
|
|
36
60
|
try {
|
|
37
|
-
const artifacts = await listArgentArtifactsAsync({ toolsUrl, toolsAuthToken });
|
|
61
|
+
const artifacts = await listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, signal });
|
|
38
62
|
listArtifactsErrorCount = 0;
|
|
39
|
-
|
|
40
|
-
if (seenArtifactIds.has(artifact.id)) {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
seenArtifactIds.add(artifact.id);
|
|
44
|
-
void uploadArgentArtifactAsync(ctx, {
|
|
45
|
-
deviceRunSessionId,
|
|
46
|
-
toolsUrl,
|
|
47
|
-
toolsAuthToken,
|
|
48
|
-
artifact,
|
|
49
|
-
logger,
|
|
50
|
-
}).catch(err => {
|
|
51
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
52
|
-
sentry_1.Sentry.capture('Could not upload Argent remote session artifact', error);
|
|
53
|
-
logger.warn({ err: error }, 'Could not upload Argent remote session artifact.');
|
|
54
|
-
});
|
|
55
|
-
}
|
|
63
|
+
return artifacts;
|
|
56
64
|
}
|
|
57
65
|
catch (err) {
|
|
66
|
+
if (signal?.aborted) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
58
69
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
59
70
|
listArtifactsErrorCount += 1;
|
|
60
71
|
if (listArtifactsErrorCount === 1 || listArtifactsErrorCount % 5 === 0) {
|
|
61
72
|
sentry_1.Sentry.capture('Could not list Argent remote session artifacts', error);
|
|
62
73
|
logger.warn({ err: error, failedArtifactListCount: listArtifactsErrorCount }, 'Could not list Argent remote session artifacts.');
|
|
63
74
|
}
|
|
75
|
+
return [];
|
|
64
76
|
}
|
|
65
|
-
|
|
77
|
+
};
|
|
78
|
+
const listAndQueueArtifactUploadsAsync = async ({ signal, } = {}) => {
|
|
79
|
+
const artifacts = await listArtifactsForUploadAsync({ signal });
|
|
80
|
+
for (const artifact of artifacts) {
|
|
81
|
+
queueArtifactUpload(artifact);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
while (!signal.aborted) {
|
|
85
|
+
await listAndQueueArtifactUploadsAsync({ signal });
|
|
86
|
+
await sleepUntilAbortedAsync(ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS, signal);
|
|
66
87
|
}
|
|
88
|
+
logger.info('Argent artifact polling stopped; draining remaining artifacts.');
|
|
89
|
+
await listAndQueueArtifactUploadsAsync();
|
|
90
|
+
await waitForPendingUploadsAsync({
|
|
91
|
+
pendingUploads,
|
|
92
|
+
timeoutMs: ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS,
|
|
93
|
+
logger,
|
|
94
|
+
});
|
|
67
95
|
}
|
|
68
|
-
async function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, }) {
|
|
96
|
+
async function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, signal, }) {
|
|
69
97
|
const response = await (0, node_fetch_1.default)(new URL('/artifacts', toolsUrl).toString(), {
|
|
70
98
|
headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
|
|
99
|
+
signal: createTimeoutSignal({ signal, timeoutMs: ARGENT_ARTIFACT_FETCH_TIMEOUT_MS }),
|
|
71
100
|
});
|
|
72
101
|
if (!response.ok) {
|
|
73
102
|
throw new eas_build_job_1.SystemError(`Failed to list Argent artifacts: ${response.status} ${response.statusText}`);
|
|
@@ -97,6 +126,7 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
|
|
|
97
126
|
artifactId: artifact.id,
|
|
98
127
|
name: `${filename} (${artifact.id})`,
|
|
99
128
|
filename,
|
|
129
|
+
kind: undefined,
|
|
100
130
|
size,
|
|
101
131
|
stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
|
|
102
132
|
});
|
|
@@ -108,6 +138,10 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
|
|
|
108
138
|
async function downloadArgentArtifactToFileAsync({ artifact, toolsUrl, toolsAuthToken, destinationPath, }) {
|
|
109
139
|
const response = await (0, node_fetch_1.default)(new URL(`/artifacts/${artifact.id}`, toolsUrl).toString(), {
|
|
110
140
|
headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
|
|
141
|
+
signal: createTimeoutSignal({
|
|
142
|
+
signal: undefined,
|
|
143
|
+
timeoutMs: ARGENT_ARTIFACT_FETCH_TIMEOUT_MS,
|
|
144
|
+
}),
|
|
111
145
|
});
|
|
112
146
|
if (!response.ok) {
|
|
113
147
|
throw new eas_build_job_1.SystemError(`Failed to download Argent artifact ${artifact.id}: ${response.status} ${response.statusText}`);
|
|
@@ -117,3 +151,41 @@ async function downloadArgentArtifactToFileAsync({ artifact, toolsUrl, toolsAuth
|
|
|
117
151
|
}
|
|
118
152
|
await (0, promises_2.pipeline)(response.body, (0, node_fs_1.createWriteStream)(destinationPath));
|
|
119
153
|
}
|
|
154
|
+
async function sleepUntilAbortedAsync(timeoutMs, signal) {
|
|
155
|
+
try {
|
|
156
|
+
await (0, promises_3.setTimeout)(timeoutMs, undefined, { signal });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (!signal.aborted) {
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function createTimeoutSignal({ signal, timeoutMs, }) {
|
|
165
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
166
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
167
|
+
}
|
|
168
|
+
async function waitForPendingUploadsAsync({ pendingUploads, timeoutMs, logger, }) {
|
|
169
|
+
if (pendingUploads.size === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
logger.info(`Waiting for ${pendingUploads.size} Argent artifact upload(s) to finish.`);
|
|
173
|
+
let timeout;
|
|
174
|
+
try {
|
|
175
|
+
await Promise.race([
|
|
176
|
+
Promise.allSettled([...pendingUploads]),
|
|
177
|
+
new Promise((_resolve, reject) => {
|
|
178
|
+
timeout = setTimeout(() => {
|
|
179
|
+
reject(new eas_build_job_1.SystemError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for Argent artifact uploads.`, {
|
|
180
|
+
trackingCode: 'SIMULATOR_ARTIFACT_SLOW_UPLOAD',
|
|
181
|
+
}));
|
|
182
|
+
}, timeoutMs);
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (timeout) {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { CustomBuildContext } from '../../customBuildContext';
|
|
2
|
-
export declare function uploadDeviceRunSessionArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, artifactId, name, filename, size, stream, }: {
|
|
2
|
+
export declare function uploadDeviceRunSessionArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, stream, }: {
|
|
3
3
|
deviceRunSessionId: string;
|
|
4
4
|
artifactId: string;
|
|
5
5
|
name: string;
|
|
6
6
|
filename: string;
|
|
7
|
+
kind: string | undefined;
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
7
9
|
size: number;
|
|
8
10
|
stream: NodeJS.ReadableStream;
|
|
9
11
|
}): Promise<void>;
|
|
@@ -52,12 +52,14 @@ const CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION = (0, gql_tada_
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
`);
|
|
55
|
-
async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, stream, }) {
|
|
55
|
+
async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, stream, }) {
|
|
56
56
|
const uploadSession = await createDeviceRunSessionArtifactUploadSessionAsync(ctx, {
|
|
57
57
|
deviceRunSessionId,
|
|
58
58
|
artifactId,
|
|
59
59
|
name,
|
|
60
60
|
filename,
|
|
61
|
+
kind,
|
|
62
|
+
metadata,
|
|
61
63
|
size,
|
|
62
64
|
});
|
|
63
65
|
const response = await (0, node_fetch_1.default)(uploadSession.url, {
|
|
@@ -69,13 +71,15 @@ async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, ar
|
|
|
69
71
|
throw new eas_build_job_1.SystemError(`Failed to upload device run session artifact ${artifactId}: ${response.status} ${response.statusText}`, { cause: response });
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
|
-
async function createDeviceRunSessionArtifactUploadSessionAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, }) {
|
|
74
|
+
async function createDeviceRunSessionArtifactUploadSessionAsync(ctx, { deviceRunSessionId, artifactId, name, filename, kind, metadata, size, }) {
|
|
73
75
|
const result = await ctx.graphqlClient
|
|
74
76
|
.mutation(CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION, {
|
|
75
77
|
deviceRunSessionId,
|
|
76
78
|
input: {
|
|
77
79
|
name,
|
|
78
80
|
filename,
|
|
81
|
+
...(kind !== undefined ? { kind } : {}),
|
|
82
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
79
83
|
size,
|
|
80
84
|
},
|
|
81
85
|
})
|
|
@@ -15,6 +15,12 @@ export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
|
|
|
15
15
|
env: BuildStepEnv;
|
|
16
16
|
logger: bunyan;
|
|
17
17
|
}): Promise<void>;
|
|
18
|
+
export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }: {
|
|
19
|
+
ctx: CustomBuildContext;
|
|
20
|
+
deviceRunSessionId: string;
|
|
21
|
+
logger: bunyan;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): Promise<void>;
|
|
18
24
|
/**
|
|
19
25
|
* Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
|
|
20
26
|
* credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
|
|
@@ -42,6 +48,8 @@ export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId
|
|
|
42
48
|
logger: bunyan;
|
|
43
49
|
}): Promise<void>;
|
|
44
50
|
export type DetachedProcessHandle = {
|
|
51
|
+
/** PID of the directly spawned process, if the OS assigned one. */
|
|
52
|
+
pid: number | undefined;
|
|
45
53
|
getOutput: () => string;
|
|
46
54
|
};
|
|
47
55
|
export declare function spawnDetached({ command, args, cwd, env, }: {
|
|
@@ -40,6 +40,7 @@ exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
|
|
|
40
40
|
exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
|
|
41
41
|
exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
|
|
42
42
|
exports.selectXcodeDeveloperDirectoryAsync = selectXcodeDeveloperDirectoryAsync;
|
|
43
|
+
exports.waitForDeviceRunSessionStoppedAsync = waitForDeviceRunSessionStoppedAsync;
|
|
43
44
|
exports.turnIceServersToServeSimArgs = turnIceServersToServeSimArgs;
|
|
44
45
|
exports.fetchServeSimTurnArgsAsync = fetchServeSimTurnArgsAsync;
|
|
45
46
|
exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
|
|
@@ -56,6 +57,7 @@ const nullthrows_1 = __importDefault(require("nullthrows"));
|
|
|
56
57
|
const zod_1 = require("zod");
|
|
57
58
|
const node_crypto_1 = require("node:crypto");
|
|
58
59
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
60
|
+
const promises_1 = require("node:timers/promises");
|
|
59
61
|
const sentry_1 = require("../../sentry");
|
|
60
62
|
const retry_1 = require("../../utils/retry");
|
|
61
63
|
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
@@ -73,6 +75,17 @@ const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
`);
|
|
78
|
+
const DEVICE_RUN_SESSION_STATUS_QUERY = (0, gql_tada_1.graphql)(`
|
|
79
|
+
query DeviceRunSessionStatus($deviceRunSessionId: ID!) {
|
|
80
|
+
deviceRunSessions {
|
|
81
|
+
byId(deviceRunSessionId: $deviceRunSessionId) {
|
|
82
|
+
id
|
|
83
|
+
status
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`);
|
|
88
|
+
const DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS = 5_000;
|
|
76
89
|
function getDeviceRunSessionIdOrThrow(env) {
|
|
77
90
|
const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
|
|
78
91
|
if (!deviceRunSessionId) {
|
|
@@ -117,6 +130,54 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
|
|
|
117
130
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
131
|
});
|
|
119
132
|
}
|
|
133
|
+
async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }) {
|
|
134
|
+
logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
|
|
135
|
+
let pollErrorCount = 0;
|
|
136
|
+
while (!signal?.aborted) {
|
|
137
|
+
try {
|
|
138
|
+
const result = await ctx.graphqlClient
|
|
139
|
+
.query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
|
|
140
|
+
.toPromise();
|
|
141
|
+
if (result.error) {
|
|
142
|
+
throw result.error;
|
|
143
|
+
}
|
|
144
|
+
const status = result.data?.deviceRunSessions?.byId?.status;
|
|
145
|
+
if (!status) {
|
|
146
|
+
throw new Error(`Device run session ${deviceRunSessionId} status response was missing.`);
|
|
147
|
+
}
|
|
148
|
+
pollErrorCount = 0;
|
|
149
|
+
if (status === 'STOPPED') {
|
|
150
|
+
logger.info(`Device run session ${deviceRunSessionId} was stopped.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (status === 'ERRORED') {
|
|
154
|
+
throw new eas_build_job_1.SystemError(`Device run session ${deviceRunSessionId} errored.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (err instanceof eas_build_job_1.SystemError) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
pollErrorCount += 1;
|
|
163
|
+
if (pollErrorCount === 1 || pollErrorCount % 5 === 0) {
|
|
164
|
+
sentry_1.Sentry.capture('Could not poll device run session status', error, { level: 'warning' });
|
|
165
|
+
logger.warn({ err: error, failedStatusPollCount: pollErrorCount }, 'Could not poll device run session status; will retry.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function sleepUntilAbortedAsync(timeoutMs, signal) {
|
|
172
|
+
try {
|
|
173
|
+
await (0, promises_1.setTimeout)(timeoutMs, undefined, signal ? { signal } : undefined);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (!signal?.aborted) {
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
120
181
|
const TurnIceServersResponseSchema = zod_1.z.object({
|
|
121
182
|
data: zod_1.z.object({
|
|
122
183
|
iceServers: TurnIceServersSchema,
|
|
@@ -204,7 +265,7 @@ function spawnDetached({ command, args, cwd, env, }) {
|
|
|
204
265
|
};
|
|
205
266
|
promise.child.stdout?.on('data', appendChunk);
|
|
206
267
|
promise.child.stderr?.on('data', appendChunk);
|
|
207
|
-
return { getOutput: () => output };
|
|
268
|
+
return { pid: promise.child.pid, getOutput: () => output };
|
|
208
269
|
}
|
|
209
270
|
async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
|
|
210
271
|
logger.info('Launching serve-sim with tunnel.');
|
|
@@ -240,6 +240,9 @@ var AndroidEmulatorUtils;
|
|
|
240
240
|
await emulatorPromise;
|
|
241
241
|
}
|
|
242
242
|
emulatorPromise.child.unref();
|
|
243
|
+
// The emulator is detached and managed through adb. Observe its process promise
|
|
244
|
+
// immediately so expected exits during retry cleanup do not become unhandled rejections.
|
|
245
|
+
void (0, results_1.asyncResult)(emulatorPromise);
|
|
243
246
|
const serialId = await (0, retry_1.retryAsync)(async () => {
|
|
244
247
|
const serialId = await getSerialIdAsync({ deviceName, env });
|
|
245
248
|
(0, assert_1.default)(serialId, `Failed to configure emulator (${serialId}): emulator with required ID not found.`);
|
|
@@ -21,6 +21,7 @@ var IosSimulatorUtils;
|
|
|
21
21
|
allAvailableDevices.push(...devices.map(device => ({
|
|
22
22
|
...device,
|
|
23
23
|
runtime,
|
|
24
|
+
runtimeDisplayName: formatRuntimeDisplayName(runtime),
|
|
24
25
|
displayName: `${device.name} (${device.udid}) on ${runtime}`,
|
|
25
26
|
})));
|
|
26
27
|
}
|
|
@@ -118,8 +119,30 @@ var IosSimulatorUtils;
|
|
|
118
119
|
}
|
|
119
120
|
IosSimulatorUtils.waitForReadyAsync = waitForReadyAsync;
|
|
120
121
|
async function disableApsdAsync({ udid, env, }) {
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const launchctlDomains = ['user/foreground', 'system'];
|
|
123
|
+
let lastError;
|
|
124
|
+
for (const domain of launchctlDomains) {
|
|
125
|
+
const service = `${domain}/com.apple.apsd`;
|
|
126
|
+
try {
|
|
127
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'disable', service], { env });
|
|
128
|
+
try {
|
|
129
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'bootout', service], {
|
|
130
|
+
env,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// bootout can fail when apsd is already gone; verify the service state below.
|
|
135
|
+
lastError = err;
|
|
136
|
+
}
|
|
137
|
+
if (!(await isLaunchctlServiceLoadedAsync({ udid, env, serviceLabel: 'com.apple.apsd' }))) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
lastError = err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw lastError ?? new eas_build_job_1.SystemError('Unable to disable apsd in the Simulator.');
|
|
123
146
|
}
|
|
124
147
|
IosSimulatorUtils.disableApsdAsync = disableApsdAsync;
|
|
125
148
|
async function collectLogsAsync({ deviceIdentifier, env, }) {
|
|
@@ -196,6 +219,22 @@ var IosSimulatorUtils;
|
|
|
196
219
|
}
|
|
197
220
|
IosSimulatorUtils.isDataMigratorProcessRunning = isDataMigratorProcessRunning;
|
|
198
221
|
})(IosSimulatorUtils || (exports.IosSimulatorUtils = IosSimulatorUtils = {}));
|
|
222
|
+
function formatRuntimeDisplayName(runtimeIdentifier) {
|
|
223
|
+
const match = /^com\.apple\.CoreSimulator\.SimRuntime\.([^-]+)-(.+)$/.exec(runtimeIdentifier);
|
|
224
|
+
return match ? `${match[1]} ${match[2].replaceAll('-', '.')}` : runtimeIdentifier;
|
|
225
|
+
}
|
|
226
|
+
async function isLaunchctlServiceLoadedAsync({ udid, env, serviceLabel, }) {
|
|
227
|
+
try {
|
|
228
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'list', serviceLabel], { env });
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (err instanceof Error && 'status' in err && err.status === 113) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
199
238
|
function parseUdidFromBootstatusStdout(stdout) {
|
|
200
239
|
const matches = stdout.match(/^Monitoring boot status for .+ \((.+)\)\.$/m);
|
|
201
240
|
if (!matches) {
|
|
@@ -20,6 +20,10 @@ async function uploadEmbeddedBundleAsync(ctx) {
|
|
|
20
20
|
ctx.markBuildPhaseSkipped();
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
+
if (ctx.job.developmentClient) {
|
|
24
|
+
ctx.markBuildPhaseSkipped();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
23
27
|
const { platform } = ctx.job;
|
|
24
28
|
if (platform === eas_build_job_1.Platform.IOS && ctx.job.simulator) {
|
|
25
29
|
ctx.markBuildPhaseSkipped();
|
package/dist/utils/processes.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getParentAndDescendantProcessPidsAsync = getParentAndDescendantProcessPidsAsync;
|
|
7
|
+
exports.isProcessDescendantOfAsync = isProcessDescendantOfAsync;
|
|
7
8
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
8
9
|
async function getChildrenPidsAsync(parentPids) {
|
|
9
10
|
try {
|
|
@@ -35,3 +36,25 @@ async function getParentAndDescendantProcessPidsAsync(ppid) {
|
|
|
35
36
|
}
|
|
36
37
|
return [...children];
|
|
37
38
|
}
|
|
39
|
+
async function isProcessDescendantOfAsync(pid, ancestorPid) {
|
|
40
|
+
let currentPid = pid;
|
|
41
|
+
while (currentPid > 0) {
|
|
42
|
+
if (currentPid === ancestorPid) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const result = await (0, turtle_spawn_1.default)('ps', ['-p', String(currentPid), '-o', 'ppid='], {
|
|
47
|
+
stdio: 'pipe',
|
|
48
|
+
});
|
|
49
|
+
const parentPid = Number(result.stdout.toString().trim());
|
|
50
|
+
if (!Number.isInteger(parentPid) || parentPid <= 0 || parentPid === currentPid) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
currentPid = parentPid;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "21.0.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"watch": "tsc --watch --preserveWatchOutput",
|
|
24
24
|
"prebuild": "yarn gql",
|
|
25
25
|
"build": "tsc",
|
|
26
|
+
"build:record-sim": "mkdir -p bin && record_sim_bin_path=$(swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build --show-bin-path) && swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build && cp \"$record_sim_bin_path/record-sim\" bin/record-sim && chmod +x bin/record-sim",
|
|
26
27
|
"typecheck": "tsc",
|
|
27
28
|
"prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
|
|
28
29
|
"jest-unit": "jest --config jest/unit-config.ts",
|
|
@@ -37,17 +38,17 @@
|
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@expo/config": "55.0.10",
|
|
39
40
|
"@expo/config-plugins": "55.0.7",
|
|
40
|
-
"@expo/downloader": "
|
|
41
|
-
"@expo/eas-build-job": "
|
|
41
|
+
"@expo/downloader": "21.0.0",
|
|
42
|
+
"@expo/eas-build-job": "21.0.0",
|
|
42
43
|
"@expo/env": "^0.4.0",
|
|
43
|
-
"@expo/logger": "
|
|
44
|
+
"@expo/logger": "21.0.0",
|
|
44
45
|
"@expo/package-manager": "1.9.10",
|
|
45
46
|
"@expo/plist": "^0.2.0",
|
|
46
47
|
"@expo/results": "^1.0.0",
|
|
47
48
|
"@expo/spawn-async": "1.7.2",
|
|
48
|
-
"@expo/steps": "
|
|
49
|
-
"@expo/template-file": "
|
|
50
|
-
"@expo/turtle-spawn": "
|
|
49
|
+
"@expo/steps": "21.0.0",
|
|
50
|
+
"@expo/template-file": "21.0.0",
|
|
51
|
+
"@expo/turtle-spawn": "21.0.0",
|
|
51
52
|
"@expo/xcpretty": "^4.3.1",
|
|
52
53
|
"@google-cloud/storage": "^7.11.2",
|
|
53
54
|
"@ngrok/ngrok": "1.7.0",
|
|
@@ -100,5 +101,5 @@
|
|
|
100
101
|
"typescript": "^5.5.4",
|
|
101
102
|
"uuid": "^9.0.1"
|
|
102
103
|
},
|
|
103
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "c72cee372f2d45a252acb014493311de85776497"
|
|
104
105
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
|
|
3
|
+
import PackageDescription
|
|
4
|
+
|
|
5
|
+
let package = Package(
|
|
6
|
+
name: "record-sim",
|
|
7
|
+
platforms: [
|
|
8
|
+
.macOS(.v14),
|
|
9
|
+
],
|
|
10
|
+
products: [
|
|
11
|
+
.library(name: "RecordSim", targets: ["RecordSim"]),
|
|
12
|
+
.executable(name: "record-sim", targets: ["record-sim"]),
|
|
13
|
+
],
|
|
14
|
+
targets: [
|
|
15
|
+
.target(
|
|
16
|
+
name: "RecordSim",
|
|
17
|
+
linkerSettings: [
|
|
18
|
+
.linkedFramework("AVFoundation"),
|
|
19
|
+
.linkedFramework("CoreGraphics"),
|
|
20
|
+
.linkedFramework("CoreMedia"),
|
|
21
|
+
.linkedFramework("CoreVideo"),
|
|
22
|
+
.linkedFramework("IOSurface"),
|
|
23
|
+
.linkedFramework("UniformTypeIdentifiers"),
|
|
24
|
+
]
|
|
25
|
+
),
|
|
26
|
+
.executableTarget(name: "record-sim", dependencies: ["RecordSim"]),
|
|
27
|
+
]
|
|
28
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# RecordSim
|
|
2
|
+
|
|
3
|
+
Small Swift library for non-exclusive iOS Simulator screen recording.
|
|
4
|
+
|
|
5
|
+
It implements one recording path:
|
|
6
|
+
|
|
7
|
+
1. Listen to SimRenderServer framebuffer callbacks.
|
|
8
|
+
2. Copy the live `IOSurface` immediately into an owned `CVPixelBuffer`.
|
|
9
|
+
3. Feed one continuous `AVAssetWriter`.
|
|
10
|
+
4. Emit Apple HLS/CMAF-style fragmented MP4 segments.
|
|
11
|
+
|
|
12
|
+
The output is suitable for uploading while recording. `session.json` contains
|
|
13
|
+
the ordered segment metadata needed to build a playlist later.
|
|
14
|
+
|
|
15
|
+
## Output
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
session/
|
|
19
|
+
init.mp4
|
|
20
|
+
session.json
|
|
21
|
+
segments/
|
|
22
|
+
segment-000000.m4s
|
|
23
|
+
segment-000001.m4s
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`init.mp4` is the fMP4 initialization segment. Individual `.m4s` files are not
|
|
27
|
+
standalone MP4 files. `session.json` stores the wall-clock timestamp for the
|
|
28
|
+
first video frame plus the metadata needed to build a playlist: HLS version,
|
|
29
|
+
target duration, media sequence, `initSegment`, and the ordered `segments` array
|
|
30
|
+
with file path and duration for each media segment.
|
|
31
|
+
|
|
32
|
+
## CLI
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
swift run record-sim \
|
|
36
|
+
--udid <BOOTED_SIMULATOR_UDID> \
|
|
37
|
+
--output /tmp/sim-recording \
|
|
38
|
+
--segment-duration 120 \
|
|
39
|
+
--fps 30 \
|
|
40
|
+
--bitrate 30000000 \
|
|
41
|
+
--codec h264
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For sparse 2h sessions, `--segment-duration 120` is a reasonable default: about
|
|
45
|
+
60 media objects plus the init segment. The recorder appends a single duplicate
|
|
46
|
+
hold frame near each segment boundary when the simulator is idle, so long idle
|
|
47
|
+
periods still produce uploadable segments without encoding idle frames at 60fps.
|
|
48
|
+
Use `--segment-duration 0` to write one continuous `recording.mp4` instead of
|
|
49
|
+
`init.mp4` plus media segments.
|
|
50
|
+
|
|
51
|
+
Frame timestamps use monotonic host time, not wall-clock time. The recorder also
|
|
52
|
+
probes the framebuffer seed once per second; if the surface changes without
|
|
53
|
+
callbacks for 5 seconds, it captures that frame and rewires the private callback
|
|
54
|
+
registration.
|
|
55
|
+
|
|
56
|
+
## Library
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
let recorder = SimulatorRecorder(
|
|
60
|
+
configuration: SimulatorRecordingConfiguration(
|
|
61
|
+
deviceUDID: udid,
|
|
62
|
+
outputDirectory: sessionDirectory,
|
|
63
|
+
segmentDuration: 120
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
recorder.onSegment = { segment in
|
|
68
|
+
// Enqueue upload of segment.url.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try recorder.start()
|
|
72
|
+
try recorder.waitUntilFirstFrame()
|
|
73
|
+
// ...
|
|
74
|
+
let manifest = try recorder.stop()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`session.json` contains the precise wall-clock time for video PTS zero. In
|
|
78
|
+
segmented mode it also contains the init segment and ordered media segment
|
|
79
|
+
file/duration entries; in single-file mode it points at `recording.mp4`.
|