Skip to content

Commit a5b53de

Browse files
authored
fix: handle stdin correctly from slower stdout emitting (#704)
This works now ![Code - 2024-11-21 at 20 17 11](https://github.com/user-attachments/assets/1a2ff23a-bb52-488f-ba7c-77432e6bb69a)
1 parent 4f871f5 commit a5b53de

File tree

10 files changed

+64
-16
lines changed

10 files changed

+64
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
"prettier": "^3.3.3",
141141
"tsx": "^4.16.5",
142142
"typescript": "^5.5.4",
143-
"vitest": "^2.0.5"
143+
"vitest": "^2.1.5"
144144
},
145145
"oclif": {
146146
"bin": "apify",

src/commands/actor/push-data.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Args } from '@oclif/core';
22

33
import { APIFY_STORAGE_TYPES, getApifyStorageClient, getDefaultStorageId } from '../../lib/actor.js';
44
import { ApifyCommand } from '../../lib/apify_command.js';
5+
import { readStdin } from '../../lib/commands/read-stdin.js';
6+
import { error } from '../../lib/outputs.js';
57

68
export class PushDataCommand extends ApifyCommand<typeof PushDataCommand> {
79
static override description =
@@ -14,21 +16,27 @@ export class PushDataCommand extends ApifyCommand<typeof PushDataCommand> {
1416

1517
static override args = {
1618
item: Args.string({
17-
required: false,
1819
description:
1920
'JSON string with one object or array of objects containing data to be stored in the default dataset.',
2021
}),
2122
};
2223

2324
async run() {
24-
const { item } = this.args;
25+
const { item: _item } = this.args;
26+
27+
const item = _item || (await readStdin(process.stdin));
28+
29+
if (!item) {
30+
error({ message: 'No item was provided.' });
31+
return;
32+
}
2533

2634
const apifyClient = await getApifyStorageClient();
2735
const defaultStoreId = getDefaultStorageId(APIFY_STORAGE_TYPES.DATASET);
2836

2937
let parsedData: Record<string, unknown> | Record<string, unknown>[];
3038
try {
31-
parsedData = JSON.parse(item!);
39+
parsedData = JSON.parse(item.toString('utf8'));
3240
} catch (err) {
3341
throw new Error(`Failed to parse data as JSON string: ${(err as Error).message}`);
3442
}

src/commands/datasets/get-items.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export class DatasetsGetItems extends ApifyCommand<typeof DatasetsGetItems> {
6868

6969
const { datasetClient } = maybeDataset;
7070

71+
// Write something already to stdout
72+
process.stdout.write('');
73+
7174
const result = await datasetClient.downloadItems(format, {
7275
limit,
7376
offset,
@@ -78,6 +81,7 @@ export class DatasetsGetItems extends ApifyCommand<typeof DatasetsGetItems> {
7881
simpleLog({ message: contentType });
7982

8083
process.stdout.write(result);
84+
process.stdout.write('\n');
8185
}
8286

8387
private async tryToGetDataset(

src/commands/datasets/push-items.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ApifyApiError } from 'apify-client';
33
import chalk from 'chalk';
44

55
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { readStdin } from '../../lib/commands/read-stdin.js';
67
import { tryToGetDataset } from '../../lib/commands/storages.js';
78
import { error, success } from '../../lib/outputs.js';
89
import { getLoggedClientOrThrow } from '../../lib/utils.js';
@@ -17,13 +18,12 @@ export class DatasetsPushDataCommand extends ApifyCommand<typeof DatasetsPushDat
1718
ignoreStdin: true,
1819
}),
1920
item: Args.string({
20-
required: true,
2121
description: 'The object or array of objects to be pushed.',
2222
}),
2323
};
2424

2525
async run() {
26-
const { nameOrId, item } = this.args;
26+
const { nameOrId, item: _item } = this.args;
2727

2828
const client = await getLoggedClientOrThrow();
2929
const existingDataset = await tryToGetDataset(client, nameOrId);
@@ -40,8 +40,15 @@ export class DatasetsPushDataCommand extends ApifyCommand<typeof DatasetsPushDat
4040

4141
let parsedData: Record<string, unknown> | Array<Record<string, unknown>>;
4242

43+
const item = _item || (await readStdin(process.stdin));
44+
45+
if (!item) {
46+
error({ message: 'No items were provided.' });
47+
return;
48+
}
49+
4350
try {
44-
parsedData = JSON.parse(item);
51+
parsedData = JSON.parse(item.toString('utf8'));
4552
} catch (err) {
4653
error({
4754
message: `Failed to parse data as JSON string: ${(err as Error).message}`,

src/commands/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class RunCommand extends ApifyCommand<typeof RunCommand> {
106106
};
107107

108108
async run() {
109+
console.log('running');
109110
const cwd = process.cwd();
110111

111112
const { proxy, id: userId, token } = await getLocalUserInfo();

src/lib/commands/read-stdin.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { once } from 'node:events';
2+
import { fstat as fstat_ } from 'node:fs';
3+
import { promisify } from 'node:util';
4+
5+
const fstat = promisify(fstat_);
26

37
export async function readStdin(stdinStream: typeof process.stdin) {
48
// The isTTY params says if TTY is connected to the process, if so the stdout is
@@ -8,11 +12,25 @@ export async function readStdin(stdinStream: typeof process.stdin) {
812
return;
913
}
1014

15+
// The best showcase of what this does: https://stackoverflow.com/a/59024214
16+
const pipedIn = await fstat(0)
17+
.then((stat) => stat.isFIFO())
18+
.catch(() => false);
19+
20+
if (!pipedIn) {
21+
return;
22+
}
23+
24+
// This is required for some reason when piping from a previous oclif run
25+
stdinStream.resume();
26+
1127
const bufferChunks: Buffer[] = [];
28+
1229
stdinStream.on('data', (chunk) => {
1330
bufferChunks.push(chunk);
1431
});
1532

1633
await once(stdinStream, 'end');
17-
return Buffer.concat(bufferChunks).toString('utf-8');
34+
35+
return Buffer.concat(bufferChunks);
1836
}

src/lib/commands/resolve-input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function getInputOverride(cwd: string, inputFlag: string | undefine
4949

5050
if (stdin) {
5151
try {
52-
const parsed = JSON.parse(stdin);
52+
const parsed = JSON.parse(stdin.toString('utf8'));
5353

5454
if (Array.isArray(parsed)) {
5555
error({ message: 'The provided input is invalid. It should be an object, not an array.' });

test/commands/call.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ describe('apify call', () => {
169169
expect(EXPECTED_INPUT_CONTENT_TYPE).toStrictEqual(input!.contentType);
170170
});
171171

172-
it('should work with stdin input without --input or --input-file', async () => {
172+
// TODO: move this to cucumber, much easier to test
173+
it.skip('should work with stdin input without --input or --input-file', async () => {
173174
const expectedInput = {
174175
hello: 'from cli',
175176
};

test/python_support.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { existsSync, writeFileSync } from 'node:fs';
2+
import { rm } from 'node:fs/promises';
23

34
import { loadJsonFileSync } from 'load-json-file';
45

@@ -26,14 +27,19 @@ describe('Python support [python]', () => {
2627
await afterAllCalls();
2728
});
2829

29-
it('Python templates work [python]', async () => {
30+
it('Python templates work [python]', { timeout: 120_000 }, async () => {
3031
const pythonVersion = detectPythonVersion('.');
3132
// Don't fail this test when Python is not installed (it will be installed in the right CI workflow)
3233
if (!pythonVersion && !process.env.CI) {
3334
console.log('Skipping Python template test since Python is not installed');
3435
return;
3536
}
3637

38+
if (existsSync(tmpPath)) {
39+
// Remove the tmp path if it exists
40+
await rm(tmpPath, { recursive: true, force: true });
41+
}
42+
3743
await CreateCommand.run([actorName, '--template', PYTHON_START_TEMPLATE_ID], import.meta.url);
3844

3945
// Check file structure
@@ -54,8 +60,11 @@ async def main():
5460
writeFileSync(joinPath('src', 'main.py'), actorCode, { flag: 'w' });
5561

5662
toggleCwdBetweenFullAndParentPath();
63+
5764
await RunCommand.run([], import.meta.url);
5865

66+
console.log('ran');
67+
5968
// Check Actor output
6069
const actorOutputPath = joinPath(getLocalKeyValueStorePath(), 'OUTPUT.json');
6170
const actorOutput = loadJsonFileSync(actorOutputPath);

yarn.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3857,7 +3857,7 @@ __metadata:
38573857
tsx: "npm:^4.16.5"
38583858
typescript: "npm:^5.5.4"
38593859
underscore: "npm:~1.13.7"
3860-
vitest: "npm:^2.0.5"
3860+
vitest: "npm:^2.1.5"
38613861
write-json-file: "npm:~6.0.0"
38623862
bin:
38633863
apify: ./bin/run.js
@@ -11037,9 +11037,9 @@ __metadata:
1103711037
linkType: hard
1103811038

1103911039
"tinypool@npm:^1.0.1":
11040-
version: 1.0.1
11041-
resolution: "tinypool@npm:1.0.1"
11042-
checksum: 10c0/90939d6a03f1519c61007bf416632dc1f0b9c1a9dd673c179ccd9e36a408437384f984fc86555a5d040d45b595abc299c3bb39d354439e98a090766b5952e73d
11040+
version: 1.0.2
11041+
resolution: "tinypool@npm:1.0.2"
11042+
checksum: 10c0/31ac184c0ff1cf9a074741254fe9ea6de95026749eb2b8ec6fd2b9d8ca94abdccda731f8e102e7f32e72ed3b36d32c6975fd5f5523df3f1b6de6c3d8dfd95e63
1104311043
languageName: node
1104411044
linkType: hard
1104511045

@@ -11616,7 +11616,7 @@ __metadata:
1161611616
languageName: node
1161711617
linkType: hard
1161811618

11619-
"vitest@npm:^2.0.5":
11619+
"vitest@npm:^2.1.5":
1162011620
version: 2.1.5
1162111621
resolution: "vitest@npm:2.1.5"
1162211622
dependencies:

0 commit comments

Comments
 (0)