@expo/build-tools 21.0.1 → 21.0.2
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/context.js +1 -31
- package/dist/gcs/LoggerStream.d.ts +1 -5
- package/dist/gcs/LoggerStream.js +5 -19
- package/dist/gcs/client.d.ts +8 -51
- package/dist/gcs/client.js +9 -110
- package/dist/gcs/retry.d.ts +4 -3
- package/dist/gcs/retry.js +11 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/steps/functions/startServeSimRemoteSession.js +2 -3
- package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.js +20 -1
- package/dist/steps/utils/IosSimulatorRecordingUtils.js +6 -2
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +0 -1
- package/dist/steps/utils/remoteDeviceRunSession.js +7 -8
- package/package.json +7 -8
- package/resources/record-sim/Sources/RecordSim/RecordingModels.swift +7 -0
- package/resources/record-sim/Sources/RecordSim/RecordingOutputWriter.swift +5 -1
- package/resources/record-sim/Sources/RecordSim/SimulatorRecorder.swift +13 -1
- package/resources/record-sim/Sources/record-sim/main.swift +20 -4
- package/dist/gcs/__unit__/gcs.test.d.ts +0 -1
- package/dist/gcs/__unit__/gcs.test.js +0 -335
package/dist/context.js
CHANGED
|
@@ -17,26 +17,6 @@ const packageManager_1 = require("./utils/packageManager");
|
|
|
17
17
|
class SkipNativeBuildError extends Error {
|
|
18
18
|
}
|
|
19
19
|
exports.SkipNativeBuildError = SkipNativeBuildError;
|
|
20
|
-
function logEnvComparison({ oldMergeOrderEnv, newMergeOrderEnv, }) {
|
|
21
|
-
const envNames = new Set([...Object.keys(oldMergeOrderEnv), ...Object.keys(newMergeOrderEnv)]);
|
|
22
|
-
const sameEnvNames = [];
|
|
23
|
-
const differentEnvNames = [];
|
|
24
|
-
for (const key of envNames) {
|
|
25
|
-
const hasOldValue = key in oldMergeOrderEnv;
|
|
26
|
-
const hasNewValue = key in newMergeOrderEnv;
|
|
27
|
-
if (hasOldValue === hasNewValue && oldMergeOrderEnv[key] === newMergeOrderEnv[key]) {
|
|
28
|
-
sameEnvNames.push(key);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
differentEnvNames.push(key);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (differentEnvNames.length === 0) {
|
|
35
|
-
datadog_1.Datadog.log('BuildContext.updateEnv merge order produced same env');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
datadog_1.Datadog.log(`BuildContext.updateEnv merge order produced different env: compared_env_count=${envNames.size} different_env_count=${differentEnvNames.length} different_env_names=${differentEnvNames.join(',')} same_env_count=${sameEnvNames.length} same_env_names=${sameEnvNames.join(',')}`);
|
|
39
|
-
}
|
|
40
20
|
class BuildContext {
|
|
41
21
|
workingdir;
|
|
42
22
|
logger;
|
|
@@ -177,21 +157,11 @@ class BuildContext {
|
|
|
177
157
|
if (this._job.triggeredBy !== common_1.BuildTrigger.GIT_BASED_INTEGRATION) {
|
|
178
158
|
throw new Error('Updating environment variables is only allowed when build was triggered by a git-based integration.');
|
|
179
159
|
}
|
|
180
|
-
|
|
181
|
-
...env,
|
|
182
|
-
...this._env,
|
|
183
|
-
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
|
184
|
-
};
|
|
185
|
-
const newMergeOrderEnv = {
|
|
160
|
+
this._env = {
|
|
186
161
|
...this._env,
|
|
187
162
|
...env,
|
|
188
163
|
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
|
189
164
|
};
|
|
190
|
-
logEnvComparison({
|
|
191
|
-
oldMergeOrderEnv,
|
|
192
|
-
newMergeOrderEnv,
|
|
193
|
-
});
|
|
194
|
-
this._env = newMergeOrderEnv;
|
|
195
165
|
this._env.PATH = this._env.PATH
|
|
196
166
|
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
|
|
197
167
|
: this.buildExecutablesDirectory;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
2
|
import { Writable } from 'stream';
|
|
3
|
-
import GCS from './client';
|
|
3
|
+
import { GCS } from './client';
|
|
4
4
|
declare class GCSLoggerStream extends Writable {
|
|
5
5
|
writable: boolean;
|
|
6
6
|
private readonly logger;
|
|
@@ -37,10 +37,6 @@ declare namespace GCSLoggerStream {
|
|
|
37
37
|
compress?: CompressionMethod;
|
|
38
38
|
}
|
|
39
39
|
type UploadMethod = {
|
|
40
|
-
client: GCS;
|
|
41
|
-
key: string;
|
|
42
|
-
customTime: Date | null;
|
|
43
|
-
} | {
|
|
44
40
|
signedUrl: GCS.SignedUrl;
|
|
45
41
|
};
|
|
46
42
|
interface Config {
|
package/dist/gcs/LoggerStream.js
CHANGED
|
@@ -11,7 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
const stream_1 = require("stream");
|
|
12
12
|
const util_1 = require("util");
|
|
13
13
|
const zlib_1 = __importDefault(require("zlib"));
|
|
14
|
-
const client_1 =
|
|
14
|
+
const client_1 = require("./client");
|
|
15
15
|
const pipe = (0, util_1.promisify)(stream_1.pipeline);
|
|
16
16
|
class GCSLoggerStream extends stream_1.Writable {
|
|
17
17
|
writable = true;
|
|
@@ -38,7 +38,7 @@ class GCSLoggerStream extends stream_1.Writable {
|
|
|
38
38
|
this.temporaryCompressedLogsPath = `${this.temporaryLogsPath}.compressed`;
|
|
39
39
|
}
|
|
40
40
|
findNormalizedHeader(name) {
|
|
41
|
-
if (!this.uploadMethod
|
|
41
|
+
if (!this.uploadMethod) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
const normalizedName = name.toLowerCase().replace('-', '');
|
|
@@ -145,24 +145,10 @@ class GCSLoggerStream extends stream_1.Writable {
|
|
|
145
145
|
const srcGeneratorAsync = async () => {
|
|
146
146
|
return await this.createCompressedStream(fs_extra_1.default.createReadStream(this.temporaryLogsPath, { end: size }));
|
|
147
147
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
srcGeneratorAsync,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
const { Location } = await this.uploadMethod.client.uploadFile({
|
|
155
|
-
key: this.uploadMethod.key,
|
|
156
|
-
src: await srcGeneratorAsync(),
|
|
157
|
-
streamOptions: {
|
|
158
|
-
metadata: {
|
|
159
|
-
contentType: 'text/plain;charset=utf-8',
|
|
160
|
-
...(this.compress !== null ? { contentEncoding: this.compress } : {}),
|
|
161
|
-
customTime: this.uploadMethod.customTime,
|
|
162
|
-
},
|
|
163
|
-
},
|
|
148
|
+
return await client_1.GCS.uploadWithSignedUrl({
|
|
149
|
+
signedUrl: this.uploadMethod.signedUrl,
|
|
150
|
+
srcGeneratorAsync,
|
|
164
151
|
});
|
|
165
|
-
return Location;
|
|
166
152
|
}
|
|
167
153
|
async createCompressedStream(src) {
|
|
168
154
|
if (!this.compress) {
|
package/dist/gcs/client.d.ts
CHANGED
|
@@ -1,58 +1,15 @@
|
|
|
1
|
-
import { CreateWriteStreamOptions, File } from '@google-cloud/storage';
|
|
2
1
|
import { Readable } from 'stream';
|
|
3
|
-
import { RetryOptions } from './retry';
|
|
4
|
-
interface SignedUrlParams {
|
|
5
|
-
key: string;
|
|
6
|
-
expirationTime: number;
|
|
7
|
-
contentType?: string;
|
|
8
|
-
extensionHeaders?: {
|
|
9
|
-
[key: string]: number | string | string[];
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
2
|
interface UploadWithSignedUrlParams {
|
|
13
3
|
signedUrl: GCS.SignedUrl;
|
|
14
4
|
srcGeneratorAsync: () => Promise<Readable>;
|
|
15
|
-
retryIntervalMs?:
|
|
16
|
-
retries?:
|
|
5
|
+
retryIntervalMs?: number;
|
|
6
|
+
retries?: number;
|
|
17
7
|
}
|
|
18
|
-
declare
|
|
19
|
-
|
|
20
|
-
private readonly client;
|
|
21
|
-
constructor(bucket: string);
|
|
22
|
-
static uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries, retryIntervalMs, }: UploadWithSignedUrlParams): Promise<string>;
|
|
23
|
-
formatHttpUrl(key: string): string;
|
|
24
|
-
uploadFile({ key, src, streamOptions, }: {
|
|
25
|
-
key: string;
|
|
26
|
-
src: Readable;
|
|
27
|
-
streamOptions?: CreateWriteStreamOptions;
|
|
28
|
-
}): Promise<{
|
|
29
|
-
Location: string;
|
|
30
|
-
}>;
|
|
31
|
-
deleteFile(key: string): Promise<void>;
|
|
32
|
-
createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders, }: SignedUrlParams): Promise<GCS.SignedUrl>;
|
|
33
|
-
createSignedDownloadUrl({ key, expirationTime, }: {
|
|
34
|
-
key: string;
|
|
35
|
-
expirationTime: number;
|
|
36
|
-
}): Promise<string>;
|
|
37
|
-
checkIfFileExists(key: string, fileHash?: string): Promise<boolean>;
|
|
38
|
-
listDirectory(prefix: string): Promise<string[]>;
|
|
39
|
-
moveFile(src: string, dest: string): Promise<void>;
|
|
40
|
-
deleteFiles(keys: string[]): Promise<void>;
|
|
41
|
-
downloadFile(key: string, destinationPath: string): Promise<void>;
|
|
42
|
-
getFile(key: string): File;
|
|
43
|
-
}
|
|
44
|
-
declare namespace GCS {
|
|
45
|
-
interface SignedUrl {
|
|
8
|
+
export declare namespace GCS {
|
|
9
|
+
type SignedUrl = {
|
|
46
10
|
url: string;
|
|
47
|
-
headers:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
interface Config {
|
|
52
|
-
accessKeyId: string;
|
|
53
|
-
secretAccessKey: string;
|
|
54
|
-
region: string;
|
|
55
|
-
bucket: string;
|
|
56
|
-
}
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
function uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries, retryIntervalMs, }: UploadWithSignedUrlParams): Promise<string>;
|
|
57
14
|
}
|
|
58
|
-
export
|
|
15
|
+
export {};
|
package/dist/gcs/client.js
CHANGED
|
@@ -32,26 +32,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
|
|
40
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
36
|
+
exports.GCS = void 0;
|
|
41
37
|
const node_fetch_1 = __importStar(require("node-fetch"));
|
|
42
|
-
const node_stream_1 = __importDefault(require("node:stream"));
|
|
43
|
-
const node_util_1 = require("node:util");
|
|
44
38
|
const url_1 = require("url");
|
|
45
39
|
const retry_1 = require("./retry");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
client = new storage_1.Storage();
|
|
50
|
-
constructor(bucket) {
|
|
51
|
-
this.bucket = bucket;
|
|
52
|
-
this.bucket = bucket;
|
|
53
|
-
}
|
|
54
|
-
static async uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries = 2, retryIntervalMs = 30_000, }) {
|
|
40
|
+
var GCS;
|
|
41
|
+
(function (GCS) {
|
|
42
|
+
async function uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries = 2, retryIntervalMs = 30_000, }) {
|
|
55
43
|
let resp;
|
|
56
44
|
try {
|
|
57
45
|
resp = await (0, retry_1.retryOnGCSUploadFailure)(async () => {
|
|
@@ -61,18 +49,13 @@ class GCS {
|
|
|
61
49
|
headers: signedUrl.headers,
|
|
62
50
|
body: src,
|
|
63
51
|
});
|
|
64
|
-
}, {
|
|
65
|
-
retries,
|
|
66
|
-
retryIntervalMs,
|
|
67
|
-
});
|
|
52
|
+
}, { retries, retryIntervalMs });
|
|
68
53
|
}
|
|
69
54
|
catch (err) {
|
|
70
55
|
if (err instanceof node_fetch_1.FetchError) {
|
|
71
56
|
throw new Error(`Failed to upload the file, reason: ${err.code}`);
|
|
72
57
|
}
|
|
73
|
-
|
|
74
|
-
throw err;
|
|
75
|
-
}
|
|
58
|
+
throw err;
|
|
76
59
|
}
|
|
77
60
|
if (!resp.ok) {
|
|
78
61
|
let body;
|
|
@@ -83,91 +66,7 @@ class GCS {
|
|
|
83
66
|
throw new Error(`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`);
|
|
84
67
|
}
|
|
85
68
|
const url = new url_1.URL(signedUrl.url);
|
|
86
|
-
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
87
|
-
}
|
|
88
|
-
formatHttpUrl(key) {
|
|
89
|
-
return this.client.bucket(this.bucket).file(key).publicUrl();
|
|
90
|
-
}
|
|
91
|
-
async uploadFile({ key, src, streamOptions, }) {
|
|
92
|
-
const file = this.client.bucket(this.bucket).file(key);
|
|
93
|
-
await new Promise((res, rej) => {
|
|
94
|
-
src.pipe(file
|
|
95
|
-
.createWriteStream(streamOptions)
|
|
96
|
-
.on('error', err => {
|
|
97
|
-
rej(err);
|
|
98
|
-
})
|
|
99
|
-
.on('finish', () => {
|
|
100
|
-
res();
|
|
101
|
-
}));
|
|
102
|
-
});
|
|
103
|
-
return { Location: file.publicUrl() };
|
|
104
|
-
}
|
|
105
|
-
async deleteFile(key) {
|
|
106
|
-
try {
|
|
107
|
-
await this.client.bucket(this.bucket).file(key).delete();
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
if (err.response?.statusCode === 404) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
throw err;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
async createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders = {}, }) {
|
|
117
|
-
const config = {
|
|
118
|
-
version: 'v4',
|
|
119
|
-
action: 'write',
|
|
120
|
-
expires: Date.now() + expirationTime,
|
|
121
|
-
contentType,
|
|
122
|
-
extensionHeaders,
|
|
123
|
-
};
|
|
124
|
-
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
|
|
125
|
-
return {
|
|
126
|
-
url,
|
|
127
|
-
headers: {
|
|
128
|
-
...(contentType ? { 'content-type': contentType } : {}),
|
|
129
|
-
...extensionHeaders,
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
async createSignedDownloadUrl({ key, expirationTime, }) {
|
|
134
|
-
const options = {
|
|
135
|
-
version: 'v4',
|
|
136
|
-
action: 'read',
|
|
137
|
-
expires: Date.now() + expirationTime,
|
|
138
|
-
};
|
|
139
|
-
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
|
|
140
|
-
return url;
|
|
141
|
-
}
|
|
142
|
-
async checkIfFileExists(key, fileHash) {
|
|
143
|
-
let metadata;
|
|
144
|
-
try {
|
|
145
|
-
[metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
if (error.code === 404) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
return fileHash ? metadata.etag === fileHash : true;
|
|
154
|
-
}
|
|
155
|
-
async listDirectory(prefix) {
|
|
156
|
-
const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
|
|
157
|
-
return files.map(x => this.formatHttpUrl(x.name));
|
|
158
|
-
}
|
|
159
|
-
async moveFile(src, dest) {
|
|
160
|
-
await this.client.bucket(this.bucket).file(src).move(dest);
|
|
161
|
-
}
|
|
162
|
-
async deleteFiles(keys) {
|
|
163
|
-
await Promise.all(keys.map(key => this.deleteFile(key)));
|
|
164
|
-
}
|
|
165
|
-
async downloadFile(key, destinationPath) {
|
|
166
|
-
const stream = this.client.bucket(this.bucket).file(key).createReadStream();
|
|
167
|
-
await pipeline(stream, fs_extra_1.default.createWriteStream(destinationPath));
|
|
168
|
-
}
|
|
169
|
-
getFile(key) {
|
|
170
|
-
return this.client.bucket(this.bucket).file(key);
|
|
69
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
171
70
|
}
|
|
172
|
-
|
|
173
|
-
exports.
|
|
71
|
+
GCS.uploadWithSignedUrl = uploadWithSignedUrl;
|
|
72
|
+
})(GCS || (exports.GCS = GCS = {}));
|
package/dist/gcs/retry.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Response } from 'node-fetch';
|
|
2
|
-
|
|
2
|
+
type RetryOptions = {
|
|
3
3
|
retries: number;
|
|
4
4
|
retryIntervalMs: number;
|
|
5
|
-
shouldRetryOnError: (error:
|
|
5
|
+
shouldRetryOnError: (error: unknown) => boolean;
|
|
6
6
|
shouldRetryOnResponse: (response: Response) => boolean;
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
8
|
export declare function retryOnGCSUploadFailure(fn: (attemptCount: number) => Promise<Response>, { retries, retryIntervalMs, }: {
|
|
9
9
|
retries: RetryOptions['retries'];
|
|
10
10
|
retryIntervalMs: RetryOptions['retryIntervalMs'];
|
|
11
11
|
}): Promise<Response>;
|
|
12
|
+
export {};
|
package/dist/gcs/retry.js
CHANGED
|
@@ -6,20 +6,24 @@ async function retryOnGCSUploadFailure(fn, { retries, retryIntervalMs, }) {
|
|
|
6
6
|
return await retry(fn, {
|
|
7
7
|
retries,
|
|
8
8
|
retryIntervalMs,
|
|
9
|
-
shouldRetryOnError:
|
|
10
|
-
return (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
shouldRetryOnError: err => {
|
|
10
|
+
return (isErrorWithCode(err) &&
|
|
11
|
+
(err.code === 'ENOTFOUND' ||
|
|
12
|
+
err.code === 'EAI_AGAIN' ||
|
|
13
|
+
err.code === 'ECONNRESET' ||
|
|
14
|
+
err.code === 'ETIMEDOUT' ||
|
|
15
|
+
err.code === 'EPIPE'));
|
|
15
16
|
},
|
|
16
17
|
shouldRetryOnResponse: resp => {
|
|
17
18
|
return [408, 429, 500, 502, 503, 504].includes(resp.status);
|
|
18
19
|
},
|
|
19
20
|
});
|
|
20
21
|
}
|
|
22
|
+
function isErrorWithCode(error) {
|
|
23
|
+
return typeof error === 'object' && error !== null && 'code' in error;
|
|
24
|
+
}
|
|
21
25
|
/**
|
|
22
|
-
* Wrapper used to execute an inner function and possibly retry it if it throws
|
|
26
|
+
* Wrapper used to execute an inner function and possibly retry it if it throws an error
|
|
23
27
|
* @param fn Function to be executed and retried in case of error
|
|
24
28
|
* @param retries How many times at most should the function be retried
|
|
25
29
|
* @param retryIntervalMs Time interval between the retries
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Builders from './builders';
|
|
2
2
|
import GCSLoggerStream from './gcs/LoggerStream';
|
|
3
|
-
import GCS from './gcs/client';
|
|
3
|
+
import { GCS } from './gcs/client';
|
|
4
4
|
export { Builders, GCS, GCSLoggerStream };
|
|
5
5
|
export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
|
|
6
6
|
export { PackageManager } from './utils/packageManager';
|
package/dist/index.js
CHANGED
|
@@ -44,8 +44,8 @@ const Builders = __importStar(require("./builders"));
|
|
|
44
44
|
exports.Builders = Builders;
|
|
45
45
|
const LoggerStream_1 = __importDefault(require("./gcs/LoggerStream"));
|
|
46
46
|
exports.GCSLoggerStream = LoggerStream_1.default;
|
|
47
|
-
const client_1 =
|
|
48
|
-
exports
|
|
47
|
+
const client_1 = require("./gcs/client");
|
|
48
|
+
Object.defineProperty(exports, "GCS", { enumerable: true, get: function () { return client_1.GCS; } });
|
|
49
49
|
var context_1 = require("./context");
|
|
50
50
|
Object.defineProperty(exports, "BuildContext", { enumerable: true, get: function () { return context_1.BuildContext; } });
|
|
51
51
|
Object.defineProperty(exports, "SkipNativeBuildError", { enumerable: true, get: function () { return context_1.SkipNativeBuildError; } });
|
|
@@ -16,18 +16,17 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
16
16
|
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
17
17
|
logger.info('Starting serve-sim remote session.');
|
|
18
18
|
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
19
|
-
const { previewUrl
|
|
19
|
+
const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
|
|
20
20
|
baseDomain: ngrokTunnelDomain,
|
|
21
21
|
env,
|
|
22
22
|
logger,
|
|
23
23
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
24
24
|
});
|
|
25
25
|
logger.info(`Preview URL: ${previewUrl}`);
|
|
26
|
-
logger.info(`Stream URL: ${streamUrl}`);
|
|
27
26
|
await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
|
|
28
27
|
ctx,
|
|
29
28
|
deviceRunSessionId,
|
|
30
|
-
remoteConfig: { previewUrl
|
|
29
|
+
remoteConfig: { previewUrl },
|
|
31
30
|
logger,
|
|
32
31
|
});
|
|
33
32
|
await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
|
|
@@ -25,8 +25,22 @@ const RecordingManifestSchema = zod_1.z.object({
|
|
|
25
25
|
firstFrameWallClock: zod_1.z.object({
|
|
26
26
|
iso8601: zod_1.z.string(),
|
|
27
27
|
}),
|
|
28
|
+
width: zod_1.z.number().int().positive(),
|
|
29
|
+
height: zod_1.z.number().int().positive(),
|
|
28
30
|
recording: zod_1.z.string(),
|
|
29
31
|
});
|
|
32
|
+
const recordingStartTimeFormatter = new Intl.DateTimeFormat('en-US', {
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
month: 'short',
|
|
35
|
+
day: 'numeric',
|
|
36
|
+
hour: '2-digit',
|
|
37
|
+
minute: '2-digit',
|
|
38
|
+
second: '2-digit',
|
|
39
|
+
fractionalSecondDigits: 3,
|
|
40
|
+
hourCycle: 'h23',
|
|
41
|
+
timeZone: 'UTC',
|
|
42
|
+
timeZoneName: 'short',
|
|
43
|
+
});
|
|
30
44
|
function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
|
|
31
45
|
return new steps_1.BuildFunction({
|
|
32
46
|
namespace: 'eas',
|
|
@@ -56,9 +70,11 @@ function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
|
|
|
56
70
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
57
71
|
const limit = (0, promise_limit_1.default)(5);
|
|
58
72
|
await Promise.all(recordings.map(recording => limit(async () => {
|
|
59
|
-
const displayName = `${recording.deviceName} screen recording`;
|
|
60
73
|
try {
|
|
61
74
|
const metadata = RecordingManifestSchema.parse(JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(recording.directory, 'session.json'), 'utf-8')));
|
|
75
|
+
const startedAt = recordingStartTimeFormatter.format(new Date(metadata.firstFrameWallClock.iso8601));
|
|
76
|
+
const shortUdid = `${recording.udid.slice(0, 8)}-…`;
|
|
77
|
+
const displayName = `${recording.deviceName} screen recording (${shortUdid}, started at ${startedAt})`;
|
|
62
78
|
const recordingPath = node_path_1.default.join(recording.directory, metadata.recording);
|
|
63
79
|
const { size } = await (0, promises_1.stat)(recordingPath);
|
|
64
80
|
const recordingId = node_path_1.default.basename(recording.directory);
|
|
@@ -70,11 +86,14 @@ function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
|
|
|
70
86
|
filename: `${recordingId}.mp4`,
|
|
71
87
|
kind: 'screen-recording',
|
|
72
88
|
metadata: {
|
|
89
|
+
__eas_type: 'screen-recording',
|
|
73
90
|
__eas_screen_recording: '1',
|
|
74
91
|
udid: recording.udid,
|
|
75
92
|
deviceName: recording.deviceName,
|
|
76
93
|
runtimeDisplayName: recording.runtimeDisplayName,
|
|
77
94
|
firstFrameAt: metadata.firstFrameWallClock.iso8601,
|
|
95
|
+
width: metadata.width,
|
|
96
|
+
height: metadata.height,
|
|
78
97
|
},
|
|
79
98
|
size,
|
|
80
99
|
stream: (0, node_fs_1.createReadStream)(recordingPath),
|
|
@@ -13,7 +13,7 @@ const promises_2 = require("node:timers/promises");
|
|
|
13
13
|
const sentry_1 = require("../../sentry");
|
|
14
14
|
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
15
15
|
const IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS = 2_000;
|
|
16
|
-
const RECORD_SIM_FINISH_TIMEOUT_MS =
|
|
16
|
+
const RECORD_SIM_FINISH_TIMEOUT_MS = 70_000;
|
|
17
17
|
const RECORD_SIM_FORCE_STOP_TIMEOUT_MS = 5_000;
|
|
18
18
|
const RECORD_SIM_MAX_ATTEMPTS_PER_BOOT = 3;
|
|
19
19
|
const RECORD_SIM_COMMAND = 'record-sim';
|
|
@@ -70,7 +70,11 @@ var IosSimulatorRecordingUtils;
|
|
|
70
70
|
if (finished) {
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
const recordSimOutput = recording.getOutput().trim();
|
|
74
|
+
const finishTimeoutSeconds = Math.round(RECORD_SIM_FINISH_TIMEOUT_MS / 1_000);
|
|
75
|
+
logger.warn({ recordSimOutput }, `Screen recording for ${recording.deviceName} did not finish within ${finishTimeoutSeconds} seconds and will be stopped.${recordSimOutput
|
|
76
|
+
? `\nRecent recorder messages:\n${recordSimOutput}`
|
|
77
|
+
: '\nNo recorder messages were captured.'}`);
|
|
74
78
|
recording.recordingProcess.kill('SIGKILL');
|
|
75
79
|
const killed = await Promise.race([
|
|
76
80
|
recording.completionPromise.then(() => true),
|
|
@@ -65,7 +65,6 @@ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, {
|
|
|
65
65
|
timeoutMs: number;
|
|
66
66
|
}): Promise<{
|
|
67
67
|
previewUrl: string;
|
|
68
|
-
streamUrl: string;
|
|
69
68
|
}>;
|
|
70
69
|
export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, rewriteHostHeader, logger, }: {
|
|
71
70
|
port: number;
|
|
@@ -289,21 +289,20 @@ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, time
|
|
|
289
289
|
],
|
|
290
290
|
env,
|
|
291
291
|
});
|
|
292
|
-
logger.info('Waiting for serve-sim to report tunnel
|
|
292
|
+
logger.info('Waiting for serve-sim to report tunnel URL.');
|
|
293
293
|
const deadline = Date.now() + timeoutMs;
|
|
294
294
|
while (Date.now() < deadline) {
|
|
295
295
|
const output = serveSim.getOutput();
|
|
296
|
-
const previewUrl =
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return { previewUrl, streamUrl };
|
|
296
|
+
const previewUrl = matchTunnelUrl({ output, baseDomain });
|
|
297
|
+
if (previewUrl) {
|
|
298
|
+
return { previewUrl };
|
|
300
299
|
}
|
|
301
300
|
await (0, retry_1.sleepAsync)(1_000);
|
|
302
301
|
}
|
|
303
|
-
throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim to report Tunnel
|
|
302
|
+
throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim to report Tunnel URL. Last output:\n${serveSim.getOutput() || '<empty>'}`);
|
|
304
303
|
}
|
|
305
|
-
function
|
|
306
|
-
const labelPattern = new RegExp(
|
|
304
|
+
function matchTunnelUrl({ output, baseDomain, }) {
|
|
305
|
+
const labelPattern = new RegExp(`Tunnel:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`);
|
|
307
306
|
const match = labelPattern.exec(output);
|
|
308
307
|
return match ? match[1] : null;
|
|
309
308
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "21.0.
|
|
3
|
+
"version": "21.0.2",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -39,18 +39,17 @@
|
|
|
39
39
|
"@expo/config": "55.0.10",
|
|
40
40
|
"@expo/config-plugins": "55.0.7",
|
|
41
41
|
"@expo/downloader": "21.0.0",
|
|
42
|
-
"@expo/eas-build-job": "21.0.
|
|
42
|
+
"@expo/eas-build-job": "21.0.2",
|
|
43
43
|
"@expo/env": "^0.4.0",
|
|
44
44
|
"@expo/logger": "21.0.0",
|
|
45
45
|
"@expo/package-manager": "1.9.10",
|
|
46
|
-
"@expo/plist": "^0.
|
|
46
|
+
"@expo/plist": "^0.3.5",
|
|
47
47
|
"@expo/results": "^1.0.0",
|
|
48
48
|
"@expo/spawn-async": "1.7.2",
|
|
49
|
-
"@expo/steps": "21.0.
|
|
50
|
-
"@expo/template-file": "21.0.
|
|
49
|
+
"@expo/steps": "21.0.2",
|
|
50
|
+
"@expo/template-file": "21.0.2",
|
|
51
51
|
"@expo/turtle-spawn": "21.0.0",
|
|
52
52
|
"@expo/xcpretty": "^4.3.1",
|
|
53
|
-
"@google-cloud/storage": "^7.11.2",
|
|
54
53
|
"@ngrok/ngrok": "1.7.0",
|
|
55
54
|
"@sentry/node": "7.77.0",
|
|
56
55
|
"@urql/core": "^6.0.1",
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"gql.tada": "^1.8.13",
|
|
62
61
|
"joi": "^17.13.1",
|
|
63
62
|
"jose": "5.10.0",
|
|
64
|
-
"lodash": "^4.
|
|
63
|
+
"lodash": "^4.18.1",
|
|
65
64
|
"node-fetch": "^2.7.0",
|
|
66
65
|
"node-forge": "^1.3.1",
|
|
67
66
|
"node-stream-zip": "1.15.0",
|
|
@@ -101,5 +100,5 @@
|
|
|
101
100
|
"typescript": "^5.5.4",
|
|
102
101
|
"uuid": "^9.0.1"
|
|
103
102
|
},
|
|
104
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "e9ebc41413ae2b5c7b4b92e91f45e9f65e3dbd23"
|
|
105
104
|
}
|
|
@@ -59,8 +59,15 @@ public struct FirstFrameWallClock: Codable, Sendable {
|
|
|
59
59
|
public let iso8601: String
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
public enum RecordingFinalizationStage: Sendable {
|
|
63
|
+
case captureStopped
|
|
64
|
+
case videoSaved
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
public struct RecordingManifest: Codable, Sendable {
|
|
63
68
|
public let firstFrameWallClock: FirstFrameWallClock
|
|
69
|
+
public let width: Int
|
|
70
|
+
public let height: Int
|
|
64
71
|
public let hlsVersion: Int?
|
|
65
72
|
public let hlsTargetDurationSeconds: Int?
|
|
66
73
|
public let hlsMediaSequence: Int?
|
|
@@ -100,7 +100,9 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
|
|
|
100
100
|
@discardableResult
|
|
101
101
|
func writeManifest(
|
|
102
102
|
configuration: SimulatorRecordingConfiguration,
|
|
103
|
-
firstFrameWallClock: Date
|
|
103
|
+
firstFrameWallClock: Date,
|
|
104
|
+
width: Int,
|
|
105
|
+
height: Int
|
|
104
106
|
) throws -> RecordingManifest {
|
|
105
107
|
try stateQueue.sync {
|
|
106
108
|
if let firstError {
|
|
@@ -121,6 +123,8 @@ final class RecordingOutputWriter: NSObject, AVAssetWriterDelegate {
|
|
|
121
123
|
unixMs: unixMs(firstFrameWallClock),
|
|
122
124
|
iso8601: iso8601(firstFrameWallClock)
|
|
123
125
|
),
|
|
126
|
+
width: width,
|
|
127
|
+
height: height,
|
|
124
128
|
hlsVersion: segmented ? 7 : nil,
|
|
125
129
|
hlsTargetDurationSeconds: targetDuration,
|
|
126
130
|
hlsMediaSequence: segmented ? 0 : nil,
|
|
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
|
|
|
7
7
|
public final class SimulatorRecorder {
|
|
8
8
|
public var onSegment: ((SegmentOutput) -> Void)?
|
|
9
9
|
public var onSimulatorStopped: ((String) -> Void)?
|
|
10
|
+
public var onFinalizationStage: ((RecordingFinalizationStage) -> Void)?
|
|
10
11
|
|
|
11
12
|
private static let callbackStalenessTimeout: TimeInterval = 5
|
|
12
13
|
private static let firstFrameRewireInterval: TimeInterval = 1
|
|
@@ -26,6 +27,8 @@ public final class SimulatorRecorder {
|
|
|
26
27
|
private var monotonicClock = MonotonicClock()
|
|
27
28
|
private var firstAcceptedCaptureTime: CMTime?
|
|
28
29
|
private var firstAcceptedWallClock: Date?
|
|
30
|
+
private var recordingWidth: Int?
|
|
31
|
+
private var recordingHeight: Int?
|
|
29
32
|
private var lastPTS: CMTime?
|
|
30
33
|
private var lastSeed: UInt32?
|
|
31
34
|
private var lastFrameCallbackElapsed: TimeInterval?
|
|
@@ -113,6 +116,7 @@ public final class SimulatorRecorder {
|
|
|
113
116
|
displaySource?.stop()
|
|
114
117
|
displaySource = nil
|
|
115
118
|
}
|
|
119
|
+
onFinalizationStage?(.captureStopped)
|
|
116
120
|
|
|
117
121
|
return try writerQueue.sync {
|
|
118
122
|
if let firstError {
|
|
@@ -139,12 +143,18 @@ public final class SimulatorRecorder {
|
|
|
139
143
|
if let error = outputWriter.error {
|
|
140
144
|
throw error
|
|
141
145
|
}
|
|
146
|
+
onFinalizationStage?(.videoSaved)
|
|
142
147
|
guard let firstAcceptedWallClock else {
|
|
143
148
|
throw RecorderError.make(25, "Missing first frame wall-clock timestamp")
|
|
144
149
|
}
|
|
150
|
+
guard let recordingWidth, let recordingHeight else {
|
|
151
|
+
throw RecorderError.make(27, "Missing recording dimensions")
|
|
152
|
+
}
|
|
145
153
|
return try outputWriter.writeManifest(
|
|
146
154
|
configuration: configuration,
|
|
147
|
-
firstFrameWallClock: firstAcceptedWallClock
|
|
155
|
+
firstFrameWallClock: firstAcceptedWallClock,
|
|
156
|
+
width: recordingWidth,
|
|
157
|
+
height: recordingHeight
|
|
148
158
|
)
|
|
149
159
|
}
|
|
150
160
|
}
|
|
@@ -590,6 +600,8 @@ public final class SimulatorRecorder {
|
|
|
590
600
|
self.writer = writer
|
|
591
601
|
self.input = input
|
|
592
602
|
self.adaptor = adaptor
|
|
603
|
+
recordingWidth = width
|
|
604
|
+
recordingHeight = height
|
|
593
605
|
}
|
|
594
606
|
|
|
595
607
|
private func assetWriterSettings(width: Int, height: Int) -> [String: Any] {
|
|
@@ -10,15 +10,15 @@ struct CLIOptions {
|
|
|
10
10
|
var codec: RecorderCodec = .h264
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
private var
|
|
13
|
+
private var requestedStopSignal: CInt = 0
|
|
14
14
|
private var simulatorStopped: CInt = 0
|
|
15
15
|
|
|
16
|
-
private func handleStopSignal(_: CInt) {
|
|
17
|
-
|
|
16
|
+
private func handleStopSignal(_ signal: CInt) {
|
|
17
|
+
requestedStopSignal = signal
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
private func isStopRequested() -> Bool {
|
|
21
|
-
|
|
21
|
+
requestedStopSignal != 0 || simulatorStopped != 0
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
func usage() -> String {
|
|
@@ -112,6 +112,14 @@ do {
|
|
|
112
112
|
simulatorStopped = 1
|
|
113
113
|
fputs("[record-sim] simulator stopped state=\(state); finalizing recording\n", stderr)
|
|
114
114
|
}
|
|
115
|
+
recorder.onFinalizationStage = { stage in
|
|
116
|
+
switch stage {
|
|
117
|
+
case .captureStopped:
|
|
118
|
+
fputs("[record-sim] screen capture stopped; saving final video\n", stderr)
|
|
119
|
+
case .videoSaved:
|
|
120
|
+
fputs("[record-sim] final video saved; writing recording details\n", stderr)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
115
123
|
recorder.onSegment = { segment in
|
|
116
124
|
switch segment.kind {
|
|
117
125
|
case .initialization:
|
|
@@ -130,6 +138,14 @@ do {
|
|
|
130
138
|
Thread.sleep(forTimeInterval: 0.1)
|
|
131
139
|
}
|
|
132
140
|
|
|
141
|
+
if requestedStopSignal != 0 {
|
|
142
|
+
let signalName = switch requestedStopSignal {
|
|
143
|
+
case SIGINT: "SIGINT"
|
|
144
|
+
case SIGTERM: "SIGTERM"
|
|
145
|
+
default: "signal \(requestedStopSignal)"
|
|
146
|
+
}
|
|
147
|
+
fputs("[record-sim] received \(signalName); finishing recording\n", stderr)
|
|
148
|
+
}
|
|
133
149
|
let manifest = try recorder.stop()
|
|
134
150
|
if let recording = manifest.recording {
|
|
135
151
|
fputs("[record-sim] done recording=\(recording)\n", stderr)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const crypto_1 = require("crypto");
|
|
7
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
-
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const stream_1 = require("stream");
|
|
11
|
-
const client_1 = __importDefault(require("../client"));
|
|
12
|
-
jest.mock('node-fetch');
|
|
13
|
-
class ErrorWithCode extends Error {
|
|
14
|
-
_code;
|
|
15
|
-
constructor(message, code) {
|
|
16
|
-
super(message);
|
|
17
|
-
if (code) {
|
|
18
|
-
this._code = code;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
get code() {
|
|
22
|
-
return this._code;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
class DNSError extends ErrorWithCode {
|
|
26
|
-
_code = 'ENOTFOUND';
|
|
27
|
-
}
|
|
28
|
-
const TEST_BUCKET = 'turtle-v2-test';
|
|
29
|
-
let googleApplicationCredentials;
|
|
30
|
-
beforeAll(() => {
|
|
31
|
-
googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
32
|
-
process.env.GOOGLE_APPLICATION_CREDENTIALS = path_1.default.join(__dirname, 'mock-credentials.json');
|
|
33
|
-
});
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
|
|
36
|
-
});
|
|
37
|
-
describe('GCS client', () => {
|
|
38
|
-
describe('uploadWithPresignedURL function', () => {
|
|
39
|
-
it('should throw an error if upload fails', async () => {
|
|
40
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
41
|
-
const res = {
|
|
42
|
-
ok: false,
|
|
43
|
-
status: 500,
|
|
44
|
-
};
|
|
45
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
46
|
-
const key = 'cat.jpg';
|
|
47
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
48
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
49
|
-
key,
|
|
50
|
-
expirationTime: 30000,
|
|
51
|
-
contentType: 'image/jpeg',
|
|
52
|
-
});
|
|
53
|
-
fetchMock.mockImplementation(async () => res);
|
|
54
|
-
await expect(client_1.default.uploadWithSignedUrl({
|
|
55
|
-
signedUrl,
|
|
56
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
57
|
-
retryIntervalMs: 500,
|
|
58
|
-
})).rejects.toThrow();
|
|
59
|
-
});
|
|
60
|
-
it('should return stripped URL if successful', async () => {
|
|
61
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
62
|
-
const res = {
|
|
63
|
-
ok: true,
|
|
64
|
-
status: 200,
|
|
65
|
-
};
|
|
66
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
67
|
-
const key = 'cat.jpg';
|
|
68
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
69
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
70
|
-
key,
|
|
71
|
-
expirationTime: 30000,
|
|
72
|
-
contentType: 'image/jpeg',
|
|
73
|
-
});
|
|
74
|
-
fetchMock.mockImplementation(async () => res);
|
|
75
|
-
const result = await client_1.default.uploadWithSignedUrl({
|
|
76
|
-
signedUrl,
|
|
77
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
78
|
-
retryIntervalMs: 500,
|
|
79
|
-
});
|
|
80
|
-
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
81
|
-
});
|
|
82
|
-
it('should retry upload on DNS error up to 2 times', async () => {
|
|
83
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
84
|
-
const res = {
|
|
85
|
-
ok: true,
|
|
86
|
-
status: 200,
|
|
87
|
-
};
|
|
88
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
89
|
-
const key = 'cat.jpg';
|
|
90
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
91
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
92
|
-
key,
|
|
93
|
-
expirationTime: 30000,
|
|
94
|
-
contentType: 'image/jpeg',
|
|
95
|
-
});
|
|
96
|
-
fetchMock
|
|
97
|
-
.mockImplementationOnce(async () => {
|
|
98
|
-
throw new DNSError('failed once');
|
|
99
|
-
})
|
|
100
|
-
.mockImplementationOnce(async () => {
|
|
101
|
-
throw new DNSError('failed twice', 'EAI_AGAIN');
|
|
102
|
-
})
|
|
103
|
-
.mockImplementation(async () => res);
|
|
104
|
-
const result = await client_1.default.uploadWithSignedUrl({
|
|
105
|
-
signedUrl,
|
|
106
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
107
|
-
retryIntervalMs: 500,
|
|
108
|
-
});
|
|
109
|
-
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
110
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
111
|
-
});
|
|
112
|
-
it('should retry upload on retriable status codes error up to 2 times', async () => {
|
|
113
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
114
|
-
const res = {
|
|
115
|
-
ok: true,
|
|
116
|
-
status: 200,
|
|
117
|
-
};
|
|
118
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
119
|
-
const key = 'cat.jpg';
|
|
120
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
121
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
122
|
-
key,
|
|
123
|
-
expirationTime: 30000,
|
|
124
|
-
contentType: 'image/jpeg',
|
|
125
|
-
});
|
|
126
|
-
fetchMock
|
|
127
|
-
.mockImplementationOnce(async () => {
|
|
128
|
-
return {
|
|
129
|
-
ok: false,
|
|
130
|
-
status: 503,
|
|
131
|
-
};
|
|
132
|
-
})
|
|
133
|
-
.mockImplementationOnce(async () => {
|
|
134
|
-
return {
|
|
135
|
-
ok: false,
|
|
136
|
-
status: 408,
|
|
137
|
-
};
|
|
138
|
-
})
|
|
139
|
-
.mockImplementation(async () => res);
|
|
140
|
-
const result = await client_1.default.uploadWithSignedUrl({
|
|
141
|
-
signedUrl,
|
|
142
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
143
|
-
retryIntervalMs: 500,
|
|
144
|
-
});
|
|
145
|
-
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
146
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
147
|
-
});
|
|
148
|
-
it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {
|
|
149
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
150
|
-
const res = {
|
|
151
|
-
ok: true,
|
|
152
|
-
status: 200,
|
|
153
|
-
};
|
|
154
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
155
|
-
const key = 'cat.jpg';
|
|
156
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
157
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
158
|
-
key,
|
|
159
|
-
expirationTime: 30000,
|
|
160
|
-
contentType: 'image/jpeg',
|
|
161
|
-
});
|
|
162
|
-
fetchMock
|
|
163
|
-
.mockImplementationOnce(async () => {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
status: 503,
|
|
167
|
-
};
|
|
168
|
-
})
|
|
169
|
-
.mockImplementationOnce(async () => {
|
|
170
|
-
throw new DNSError('failed once');
|
|
171
|
-
})
|
|
172
|
-
.mockImplementation(async () => res);
|
|
173
|
-
const result = await client_1.default.uploadWithSignedUrl({
|
|
174
|
-
signedUrl,
|
|
175
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
176
|
-
retryIntervalMs: 500,
|
|
177
|
-
});
|
|
178
|
-
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
179
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
180
|
-
});
|
|
181
|
-
it('should not retry upload on DNS error more than 2 times', async () => {
|
|
182
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
183
|
-
const res = {
|
|
184
|
-
ok: true,
|
|
185
|
-
status: 200,
|
|
186
|
-
};
|
|
187
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
188
|
-
const key = 'cat.jpg';
|
|
189
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
190
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
191
|
-
key,
|
|
192
|
-
expirationTime: 30000,
|
|
193
|
-
contentType: 'image/jpeg',
|
|
194
|
-
});
|
|
195
|
-
const lastDNSError = new DNSError('failed thrice');
|
|
196
|
-
fetchMock
|
|
197
|
-
.mockImplementationOnce(async () => {
|
|
198
|
-
throw new DNSError('failed once');
|
|
199
|
-
})
|
|
200
|
-
.mockImplementationOnce(async () => {
|
|
201
|
-
throw new DNSError('failed twice', 'EAI_AGAIN');
|
|
202
|
-
})
|
|
203
|
-
.mockImplementationOnce(async () => {
|
|
204
|
-
throw lastDNSError;
|
|
205
|
-
})
|
|
206
|
-
.mockImplementation(async () => res);
|
|
207
|
-
await expect(client_1.default.uploadWithSignedUrl({
|
|
208
|
-
signedUrl,
|
|
209
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
210
|
-
retryIntervalMs: 500,
|
|
211
|
-
})).rejects.toThrow(lastDNSError);
|
|
212
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
213
|
-
});
|
|
214
|
-
it('should not retry upload on retriable status code more than 2 times', async () => {
|
|
215
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
216
|
-
const res = {
|
|
217
|
-
ok: true,
|
|
218
|
-
status: 200,
|
|
219
|
-
};
|
|
220
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
221
|
-
const key = 'cat.jpg';
|
|
222
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
223
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
224
|
-
key,
|
|
225
|
-
expirationTime: 30000,
|
|
226
|
-
contentType: 'image/jpeg',
|
|
227
|
-
});
|
|
228
|
-
fetchMock
|
|
229
|
-
.mockImplementationOnce(async () => {
|
|
230
|
-
return {
|
|
231
|
-
ok: false,
|
|
232
|
-
status: 503,
|
|
233
|
-
};
|
|
234
|
-
})
|
|
235
|
-
.mockImplementationOnce(async () => {
|
|
236
|
-
return {
|
|
237
|
-
ok: false,
|
|
238
|
-
status: 408,
|
|
239
|
-
};
|
|
240
|
-
})
|
|
241
|
-
.mockImplementationOnce(async () => {
|
|
242
|
-
return {
|
|
243
|
-
ok: false,
|
|
244
|
-
status: 504,
|
|
245
|
-
};
|
|
246
|
-
})
|
|
247
|
-
.mockImplementation(async () => res);
|
|
248
|
-
await expect(client_1.default.uploadWithSignedUrl({
|
|
249
|
-
signedUrl,
|
|
250
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
251
|
-
retryIntervalMs: 500,
|
|
252
|
-
})).rejects.toThrow();
|
|
253
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
254
|
-
});
|
|
255
|
-
it('should not retry upload on other error codes', async () => {
|
|
256
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
257
|
-
const res = {
|
|
258
|
-
ok: true,
|
|
259
|
-
status: 200,
|
|
260
|
-
};
|
|
261
|
-
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
262
|
-
const key = 'cat.jpg';
|
|
263
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
264
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
265
|
-
key,
|
|
266
|
-
expirationTime: 30000,
|
|
267
|
-
contentType: 'image/jpeg',
|
|
268
|
-
});
|
|
269
|
-
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
|
|
270
|
-
fetchMock
|
|
271
|
-
.mockImplementationOnce(async () => {
|
|
272
|
-
throw nonDNSError;
|
|
273
|
-
})
|
|
274
|
-
.mockImplementation(async () => res);
|
|
275
|
-
await expect(client_1.default.uploadWithSignedUrl({
|
|
276
|
-
signedUrl,
|
|
277
|
-
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
278
|
-
retryIntervalMs: 500,
|
|
279
|
-
})).rejects.toThrow(nonDNSError);
|
|
280
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
281
|
-
});
|
|
282
|
-
it('retries upload with full stream', async () => {
|
|
283
|
-
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
284
|
-
const receivedBodies = [];
|
|
285
|
-
const recordBody = async (request) => {
|
|
286
|
-
let body = Buffer.from([]);
|
|
287
|
-
for await (const chunk of request.body) {
|
|
288
|
-
body = Buffer.concat([body, chunk]);
|
|
289
|
-
}
|
|
290
|
-
receivedBodies.push(body);
|
|
291
|
-
};
|
|
292
|
-
fetchMock
|
|
293
|
-
.mockImplementationOnce(async (_path, request) => {
|
|
294
|
-
await recordBody(request);
|
|
295
|
-
return {
|
|
296
|
-
ok: false,
|
|
297
|
-
status: 503,
|
|
298
|
-
};
|
|
299
|
-
})
|
|
300
|
-
.mockImplementationOnce(async (_path, request) => {
|
|
301
|
-
await recordBody(request);
|
|
302
|
-
throw new DNSError('failed once');
|
|
303
|
-
})
|
|
304
|
-
.mockImplementation(async (_path, request) => {
|
|
305
|
-
await recordBody(request);
|
|
306
|
-
return {
|
|
307
|
-
ok: true,
|
|
308
|
-
status: 201,
|
|
309
|
-
};
|
|
310
|
-
});
|
|
311
|
-
const gcs = new client_1.default(TEST_BUCKET);
|
|
312
|
-
const signedUrl = await gcs.createSignedUploadUrl({
|
|
313
|
-
key: 'text.txt',
|
|
314
|
-
expirationTime: 30000,
|
|
315
|
-
contentType: 'text/plain',
|
|
316
|
-
});
|
|
317
|
-
const bufferToUpload = (0, crypto_1.randomBytes)(16);
|
|
318
|
-
const result = await client_1.default.uploadWithSignedUrl({
|
|
319
|
-
signedUrl,
|
|
320
|
-
srcGeneratorAsync: async () => stream_1.Readable.from(bufferToUpload),
|
|
321
|
-
retryIntervalMs: 500,
|
|
322
|
-
});
|
|
323
|
-
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
|
|
324
|
-
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
325
|
-
expect(receivedBodies.length).toBe(3);
|
|
326
|
-
for (const body of receivedBodies) {
|
|
327
|
-
// Here we're testing that each body received in every retry is the same.
|
|
328
|
-
// If we passed the same body instance to each of the retries
|
|
329
|
-
// each retry would consume a chunk of the same stream,
|
|
330
|
-
// causing the uploaded file to be corrupted (missing first bytes).
|
|
331
|
-
expect(body).toStrictEqual(bufferToUpload);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
});
|