@nitpicker/crawler 0.9.0 → 0.11.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/lib/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +147 -24
- package/lib/archive/archive.js +252 -86
- package/lib/archive/database.d.ts +209 -25
- package/lib/archive/database.js +928 -108
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
- package/lib/archive/init-schema.d.ts +53 -2
- package/lib/archive/init-schema.js +247 -15
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +135 -26
- package/lib/crawler/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +4 -1
- package/lib/crawler/crawler.js +290 -32
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- package/lib/crawler/derive-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +23 -0
- package/lib/crawler/derive-resource-source.js +26 -0
- package/lib/crawler/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.js +38 -2
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- package/lib/crawler/is-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -0
- package/lib/crawler/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.js +2 -1
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- package/lib/crawler/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -0
- package/lib/crawler/types.d.ts +122 -1
- package/lib/crawler-orchestrator.d.ts +93 -1
- package/lib/crawler-orchestrator.js +389 -12
- package/lib/crawler.d.ts +5 -0
- package/lib/crawler.js +3 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +11 -1
- package/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -0
- package/package.json +4 -4
package/lib/archive/types.d.ts
CHANGED
|
@@ -46,6 +46,28 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
|
|
|
46
46
|
/** Whether robots.txt restrictions were ignored during crawling. */
|
|
47
47
|
ignoreRobots: boolean;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Provenance of a page or resource row — which crawler channel originally
|
|
51
|
+
* inserted it. Stored as `pages.source` / `resources.source` in the
|
|
52
|
+
* SQLite schema (NOT NULL DEFAULT `'crawled'`).
|
|
53
|
+
*
|
|
54
|
+
* - `'crawled'` — discovered via the recursive crawl rooted at `info.roots`.
|
|
55
|
+
* Default for pre-`--inventory` archives after the
|
|
56
|
+
* `migratePagesResourcesSource` runtime migration.
|
|
57
|
+
* - `'inventory-seed'` — supplied directly by a `crawl --inventory` URL
|
|
58
|
+
* list. For pages this is the HTML URL that was rendered; for resources
|
|
59
|
+
* this is a non-HTML URL handed in by the list (HEAD-fetched without
|
|
60
|
+
* rendering).
|
|
61
|
+
* - `'inventory-discovered'` — found by following links from an
|
|
62
|
+
* `inventory-seed` page, OR (for resources) loaded by puppeteer while
|
|
63
|
+
* rendering one of those pages.
|
|
64
|
+
*
|
|
65
|
+
* Used by the viewer as a badge and to indicate why a row was added.
|
|
66
|
+
* Isolation queries (`listIsolatedPages` / `listUnusedResources`) judge
|
|
67
|
+
* orphans by `referrer = 0`, NOT by this value — `source` only labels
|
|
68
|
+
* the row.
|
|
69
|
+
*/
|
|
70
|
+
export type PageSource = 'crawled' | 'inventory-seed' | 'inventory-discovered';
|
|
49
71
|
/**
|
|
50
72
|
* Filter type for querying pages from the database.
|
|
51
73
|
*
|
|
@@ -59,7 +81,13 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
|
|
|
59
81
|
*/
|
|
60
82
|
export type PageFilter = 'page' | 'page-included-no-target' | 'external-page' | 'internal-page' | 'no-page' | 'external-no-page' | 'internal-no-page';
|
|
61
83
|
/**
|
|
62
|
-
* Raw database row representing a crawled page in the `pages` table.
|
|
84
|
+
* Raw database row representing a crawled page in the `pages` table (v2 schema).
|
|
85
|
+
*
|
|
86
|
+
* Maps 1:1 to the columns defined by `archive/init-schema.ts`. Most meta
|
|
87
|
+
* fields are derived from beholder 3.0.0's nested {@link import('@d-zero/beholder').Meta}
|
|
88
|
+
* via `archive/meta/derive-flat-from-meta.ts` and are stored as plain
|
|
89
|
+
* scalars for SQL-level filter / projection. The catch-all `meta_extras`
|
|
90
|
+
* JSON column preserves nested sub-objects not flattened above.
|
|
63
91
|
*/
|
|
64
92
|
export interface DB_Page {
|
|
65
93
|
/** Auto-incremented primary key. */
|
|
@@ -84,48 +112,118 @@ export interface DB_Page {
|
|
|
84
112
|
contentLength: number | null;
|
|
85
113
|
/** JSON-serialized HTTP response headers. */
|
|
86
114
|
responseHeaders: string;
|
|
87
|
-
/** The `lang` attribute value from the HTML element, or null
|
|
115
|
+
/** The `lang` attribute value from the HTML element, or null. */
|
|
88
116
|
lang: string | null;
|
|
89
|
-
/** The
|
|
117
|
+
/** The `dir` attribute value, or null. */
|
|
118
|
+
dir: string | null;
|
|
119
|
+
/** The `<meta charset>` value, or null. */
|
|
120
|
+
charset: string | null;
|
|
121
|
+
/** Absolutised `<base href>`, or null. */
|
|
122
|
+
baseHref: string | null;
|
|
123
|
+
/** The raw `<meta name="viewport">` content, or null. */
|
|
124
|
+
viewport_raw: string | null;
|
|
125
|
+
/** The primary `<meta name="theme-color">` (no media), or null. */
|
|
126
|
+
themeColor: string | null;
|
|
127
|
+
/** `<meta name="application-name">`, or null. */
|
|
128
|
+
applicationName: string | null;
|
|
129
|
+
/** `<meta name="author">`, or null. */
|
|
130
|
+
author: string | null;
|
|
131
|
+
/** `<meta name="generator">`, or null. */
|
|
132
|
+
generator: string | null;
|
|
133
|
+
/** `<meta name="publisher">`, or null. */
|
|
134
|
+
publisher: string | null;
|
|
135
|
+
/** The page title from the `<title>` element, or null. */
|
|
90
136
|
title: string | null;
|
|
91
|
-
/** The meta description content, or null
|
|
137
|
+
/** The meta description content, or null. */
|
|
92
138
|
description: string | null;
|
|
93
|
-
/** The meta keywords content, or null
|
|
139
|
+
/** The meta keywords content, or null. */
|
|
94
140
|
keywords: string | null;
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
/** Whether the
|
|
98
|
-
|
|
99
|
-
/** Whether the
|
|
100
|
-
|
|
101
|
-
/**
|
|
141
|
+
/** The raw `<meta name="robots">` content, or null. */
|
|
142
|
+
robots_raw: string | null;
|
|
143
|
+
/** Whether the noindex directive is set (SQLite INTEGER 0/1). */
|
|
144
|
+
robots_noindex: number | null;
|
|
145
|
+
/** Whether the nofollow directive is set (SQLite INTEGER 0/1). */
|
|
146
|
+
robots_nofollow: number | null;
|
|
147
|
+
/** Whether the noarchive directive is set (SQLite INTEGER 0/1). */
|
|
148
|
+
robots_noarchive: number | null;
|
|
149
|
+
/** Whether the noimageindex directive is set (SQLite INTEGER 0/1). */
|
|
150
|
+
robots_noimageindex: number | null;
|
|
151
|
+
/** `<meta name="googlebot">` content, or null. */
|
|
152
|
+
googlebot: string | null;
|
|
153
|
+
/** Absolutised `<link rel="canonical">` href, or null. */
|
|
102
154
|
canonical: string | null;
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
/**
|
|
155
|
+
/** Absolutised `<link rel="amphtml">` href, or null. */
|
|
156
|
+
amphtml: string | null;
|
|
157
|
+
/** Absolutised `<link rel="manifest">` href, or null. */
|
|
158
|
+
manifest: string | null;
|
|
159
|
+
/** Absolutised `<link rel="icon">` href, or null. */
|
|
160
|
+
icon_href: string | null;
|
|
161
|
+
/** Absolutised `<link rel="apple-touch-icon">` href, or null. */
|
|
162
|
+
appleTouchIcon_href: string | null;
|
|
163
|
+
/** og:type, or null. */
|
|
106
164
|
og_type: string | null;
|
|
107
|
-
/**
|
|
165
|
+
/** og:title, or null. */
|
|
108
166
|
og_title: string | null;
|
|
109
|
-
/**
|
|
167
|
+
/** Absolutised og:url, or null. */
|
|
168
|
+
og_url: string | null;
|
|
169
|
+
/** og:site_name, or null. */
|
|
110
170
|
og_site_name: string | null;
|
|
111
|
-
/**
|
|
171
|
+
/** og:description, or null. */
|
|
112
172
|
og_description: string | null;
|
|
113
|
-
/**
|
|
114
|
-
og_url: string | null;
|
|
115
|
-
/** The Open Graph image URL (`og:image`), or null if not present. */
|
|
173
|
+
/** Absolutised og:image (first if multiple), or null. */
|
|
116
174
|
og_image: string | null;
|
|
117
|
-
/**
|
|
175
|
+
/** og:image:alt, or null. */
|
|
176
|
+
og_image_alt: string | null;
|
|
177
|
+
/** og:image:width as a string (per spec), or null. */
|
|
178
|
+
og_image_width: string | null;
|
|
179
|
+
/** og:image:height as a string (per spec), or null. */
|
|
180
|
+
og_image_height: string | null;
|
|
181
|
+
/** og:locale, or null. */
|
|
182
|
+
og_locale: string | null;
|
|
183
|
+
/** og:article:published_time, or null. */
|
|
184
|
+
og_article_published_time: string | null;
|
|
185
|
+
/** og:article:modified_time, or null. */
|
|
186
|
+
og_article_modified_time: string | null;
|
|
187
|
+
/** twitter:card, or null. */
|
|
118
188
|
twitter_card: string | null;
|
|
189
|
+
/** twitter:site, or null. */
|
|
190
|
+
twitter_site: string | null;
|
|
191
|
+
/** twitter:creator, or null. */
|
|
192
|
+
twitter_creator: string | null;
|
|
193
|
+
/** twitter:title, or null. */
|
|
194
|
+
twitter_title: string | null;
|
|
195
|
+
/** twitter:description, or null. */
|
|
196
|
+
twitter_description: string | null;
|
|
197
|
+
/** Absolutised twitter:image (or twitter:image:src fallback), or null. */
|
|
198
|
+
twitter_image: string | null;
|
|
199
|
+
/** Facebook app id (`fb:app_id`), or null. */
|
|
200
|
+
fb_app_id: string | null;
|
|
201
|
+
/** Google site verification token, or null. */
|
|
202
|
+
verification_google: string | null;
|
|
203
|
+
/** `format-detection` telephone (SQLite INTEGER 0/1), or null. */
|
|
204
|
+
formatDetection_telephone: number | null;
|
|
205
|
+
/** First time this page row was inserted (UNIX ms), or null on legacy rows. */
|
|
206
|
+
firstCrawledAt: number | null;
|
|
207
|
+
/** Last successful re-scrape time (UNIX ms), or null on legacy rows. */
|
|
208
|
+
lastCrawledAt: number | null;
|
|
209
|
+
/** Number of `page_tags` rows belonging to this page. */
|
|
210
|
+
tag_count: number | null;
|
|
211
|
+
/** `meta.jsonLd.length + meta.speculationRules.length` at scrape time. */
|
|
212
|
+
jsonld_count: number | null;
|
|
213
|
+
/** Sorted unique provider names, comma-separated (empty string when none). */
|
|
214
|
+
tags_providers_csv: string | null;
|
|
215
|
+
/** JSON-serialised nested Meta sub-objects not flattened above. */
|
|
216
|
+
meta_extras: string | null;
|
|
119
217
|
/** JSON-serialized network logs captured during scraping, or null if not collected. */
|
|
120
218
|
networkLogs: string | null;
|
|
121
|
-
/** Relative file path to the saved HTML snapshot, or null if not saved. */
|
|
122
|
-
html: string | null;
|
|
123
219
|
/** Whether the page was skipped during crawling (1) or processed normally (0). */
|
|
124
220
|
isSkipped: 0 | 1;
|
|
125
221
|
/** The reason the page was skipped, or null if it was not skipped. */
|
|
126
222
|
skipReason: string | null;
|
|
127
223
|
/** The natural URL sort order index, or null if not yet assigned. */
|
|
128
224
|
order: number | null;
|
|
225
|
+
/** Provenance of the row — see {@link PageSource}. */
|
|
226
|
+
source: PageSource;
|
|
129
227
|
}
|
|
130
228
|
/**
|
|
131
229
|
* Raw database row representing a redirect relationship.
|
|
@@ -283,13 +381,24 @@ export interface DB_Resource {
|
|
|
283
381
|
cdn: string | 0;
|
|
284
382
|
/** JSON-serialized HTTP response headers, or null if not available. */
|
|
285
383
|
responseHeaders: string | null;
|
|
384
|
+
/** Provenance of the row — see {@link PageSource}. */
|
|
385
|
+
source: PageSource;
|
|
286
386
|
}
|
|
287
387
|
/**
|
|
288
388
|
* Connection options for the archive's libsql-backed database.
|
|
289
389
|
*/
|
|
290
390
|
export interface DatabaseOption {
|
|
291
|
-
/** The working directory for the database (used for resolving relative paths). */
|
|
292
|
-
workingDir: string;
|
|
293
391
|
/** The absolute file path to the SQLite database file. */
|
|
294
392
|
filename: string;
|
|
393
|
+
/**
|
|
394
|
+
* When `true`, open the database for read-only inspection:
|
|
395
|
+
*
|
|
396
|
+
* - Skip schema migrations (no `ALTER TABLE` / `UPDATE` on the user's file).
|
|
397
|
+
* - Refuse to mkdir/create the parent dir or the db file — fail loudly if
|
|
398
|
+
* either is missing instead of resurrecting them.
|
|
399
|
+
*
|
|
400
|
+
* Used by the viewer / MCP server when attaching to an in-progress crawl's
|
|
401
|
+
* tmpDir, where any write would race the live crawler.
|
|
402
|
+
*/
|
|
403
|
+
readOnly?: boolean;
|
|
295
404
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural subset of a Puppeteer `Browser` required to close it safely.
|
|
3
|
+
*
|
|
4
|
+
* Declared structurally (rather than importing Puppeteer's `Browser`) so the
|
|
5
|
+
* function stays unit-testable with a lightweight stub and free of a Puppeteer
|
|
6
|
+
* import.
|
|
7
|
+
*/
|
|
8
|
+
export interface ClosableBrowser {
|
|
9
|
+
/** Gracefully closes the browser and all of its pages over the CDP connection. */
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the underlying Chromium child process, or `null` when the browser
|
|
13
|
+
* was connected to (rather than launched) and therefore owns no process.
|
|
14
|
+
*/
|
|
15
|
+
process(): {
|
|
16
|
+
/** PID of the Chromium parent process, or `undefined` before spawn settles. */
|
|
17
|
+
readonly pid?: number;
|
|
18
|
+
/** Sends a signal to the process; returns whether it was delivered. */
|
|
19
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
20
|
+
/** Whether a signal has already been successfully sent to the process. */
|
|
21
|
+
readonly killed: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Dependency overrides for {@link closeBrowserSafely}. Used only by tests
|
|
26
|
+
* to substitute the tree-kill orchestration.
|
|
27
|
+
*/
|
|
28
|
+
export interface CloseBrowserSafelyDeps {
|
|
29
|
+
/**
|
|
30
|
+
* Kills a process and every descendant. Defaults to {@link killProcessTree}.
|
|
31
|
+
*/
|
|
32
|
+
killTree?: (pid: number, signal: NodeJS.Signals | number) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Closes a Puppeteer browser, falling back to a hard tree-kill if the graceful
|
|
36
|
+
* close hangs.
|
|
37
|
+
*
|
|
38
|
+
* WHY: When a page's Chromium session dies mid-scrape (e.g. a viewport change
|
|
39
|
+
* detaches the frame, surfacing `Attempted to use detached Frame` or
|
|
40
|
+
* `Session closed`), the CDP connection can be left wedged. A bare
|
|
41
|
+
* `await browser.close()` then never settles, stalling the `deal()` worker and
|
|
42
|
+
* hanging the whole crawl. Racing the close against a timeout and SIGKILLing
|
|
43
|
+
* the Chromium process tree (parent + renderer/network/zygote children) on
|
|
44
|
+
* expiry guarantees the worker always completes and no orphan subprocesses are
|
|
45
|
+
* left behind.
|
|
46
|
+
*
|
|
47
|
+
* The losing timer is cleared explicitly in `.finally()` so it never keeps the
|
|
48
|
+
* event loop alive after the race settles (a plain `delay()` in `Promise.race`
|
|
49
|
+
* would leak the timer until it fires).
|
|
50
|
+
*
|
|
51
|
+
* The tree-kill happens via {@link killProcessTree}, which enumerates
|
|
52
|
+
* descendants through `ps` (POSIX) or delegates to `taskkill /T /F` (Windows).
|
|
53
|
+
* `childProcess.kill('SIGKILL')` is still invoked on the parent up-front
|
|
54
|
+
* because Node's `ChildProcess.killed` flag governs how Node treats the
|
|
55
|
+
* spawn handle (reaping etc.); without it the parent would linger in Node's
|
|
56
|
+
* process table even after the OS-level kill.
|
|
57
|
+
* @param browser - The browser to close.
|
|
58
|
+
* @param timeoutMs - Milliseconds to wait for a graceful close before force-killing.
|
|
59
|
+
* Defaults to {@link DEFAULT_CLOSE_TIMEOUT_MS}.
|
|
60
|
+
* @param deps - Test-time overrides (default-free for production callers).
|
|
61
|
+
* @returns `true` if the graceful close timed out (and a tree-kill was
|
|
62
|
+
* attempted), `false` if `close()` settled in time.
|
|
63
|
+
*/
|
|
64
|
+
export declare function closeBrowserSafely(browser: ClosableBrowser, timeoutMs?: number, deps?: CloseBrowserSafelyDeps): Promise<boolean>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { crawlerLog } from '../debug.js';
|
|
2
|
+
import { killProcessTree } from './kill-process-tree.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default time to wait for a graceful `browser.close()` before force-killing
|
|
5
|
+
* the underlying Chromium process, in milliseconds.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 30 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Closes a Puppeteer browser, falling back to a hard tree-kill if the graceful
|
|
10
|
+
* close hangs.
|
|
11
|
+
*
|
|
12
|
+
* WHY: When a page's Chromium session dies mid-scrape (e.g. a viewport change
|
|
13
|
+
* detaches the frame, surfacing `Attempted to use detached Frame` or
|
|
14
|
+
* `Session closed`), the CDP connection can be left wedged. A bare
|
|
15
|
+
* `await browser.close()` then never settles, stalling the `deal()` worker and
|
|
16
|
+
* hanging the whole crawl. Racing the close against a timeout and SIGKILLing
|
|
17
|
+
* the Chromium process tree (parent + renderer/network/zygote children) on
|
|
18
|
+
* expiry guarantees the worker always completes and no orphan subprocesses are
|
|
19
|
+
* left behind.
|
|
20
|
+
*
|
|
21
|
+
* The losing timer is cleared explicitly in `.finally()` so it never keeps the
|
|
22
|
+
* event loop alive after the race settles (a plain `delay()` in `Promise.race`
|
|
23
|
+
* would leak the timer until it fires).
|
|
24
|
+
*
|
|
25
|
+
* The tree-kill happens via {@link killProcessTree}, which enumerates
|
|
26
|
+
* descendants through `ps` (POSIX) or delegates to `taskkill /T /F` (Windows).
|
|
27
|
+
* `childProcess.kill('SIGKILL')` is still invoked on the parent up-front
|
|
28
|
+
* because Node's `ChildProcess.killed` flag governs how Node treats the
|
|
29
|
+
* spawn handle (reaping etc.); without it the parent would linger in Node's
|
|
30
|
+
* process table even after the OS-level kill.
|
|
31
|
+
* @param browser - The browser to close.
|
|
32
|
+
* @param timeoutMs - Milliseconds to wait for a graceful close before force-killing.
|
|
33
|
+
* Defaults to {@link DEFAULT_CLOSE_TIMEOUT_MS}.
|
|
34
|
+
* @param deps - Test-time overrides (default-free for production callers).
|
|
35
|
+
* @returns `true` if the graceful close timed out (and a tree-kill was
|
|
36
|
+
* attempted), `false` if `close()` settled in time.
|
|
37
|
+
*/
|
|
38
|
+
export async function closeBrowserSafely(browser, timeoutMs = DEFAULT_CLOSE_TIMEOUT_MS, deps = {}) {
|
|
39
|
+
// Capture the process up-front: after a successful close() puppeteer
|
|
40
|
+
// releases its internal reference and process() returns null, so we would
|
|
41
|
+
// have no handle to tree-kill on timeout.
|
|
42
|
+
const childProcess = browser.process();
|
|
43
|
+
let timeoutHandle;
|
|
44
|
+
const timedOut = await Promise.race([
|
|
45
|
+
browser
|
|
46
|
+
.close()
|
|
47
|
+
.then(() => false)
|
|
48
|
+
.catch(() => false),
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
timeoutHandle = setTimeout(() => resolve(true), timeoutMs);
|
|
51
|
+
}),
|
|
52
|
+
]).finally(() => {
|
|
53
|
+
if (timeoutHandle) {
|
|
54
|
+
clearTimeout(timeoutHandle);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (timedOut && childProcess && !childProcess.killed) {
|
|
58
|
+
// Mark the Node ChildProcess as killed so Node's reaping logic treats
|
|
59
|
+
// it correctly; then walk the OS process tree.
|
|
60
|
+
childProcess.kill('SIGKILL');
|
|
61
|
+
// Capture pid once: ChildProcess.pid is technically `number | undefined`
|
|
62
|
+
// (undefined before spawn settles), and reading it twice across the
|
|
63
|
+
// `await` below would force the second read to re-widen back to
|
|
64
|
+
// `number | undefined` regardless of the typeof guard. Snapshotting
|
|
65
|
+
// makes the type and the runtime value match.
|
|
66
|
+
const pid = childProcess.pid;
|
|
67
|
+
if (typeof pid === 'number') {
|
|
68
|
+
const killTree = deps.killTree ?? ((p, sig) => killProcessTree(p, sig, { log: crawlerLog }));
|
|
69
|
+
await killTree(pid, 'SIGKILL');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return timedOut;
|
|
73
|
+
}
|
package/lib/crawler/crawler.d.ts
CHANGED
|
@@ -53,8 +53,11 @@ export default class Crawler extends EventEmitter<CrawlerEventTypes> {
|
|
|
53
53
|
* @param pending - URLs that were pending (not yet scraped) in the previous session.
|
|
54
54
|
* @param scraped - URLs that were already scraped in the previous session.
|
|
55
55
|
* @param resources - Resource URLs that were already captured in the previous session.
|
|
56
|
+
* @param pagesScrapedOffset - Number of HTML pages already rendered in previous
|
|
57
|
+
* sessions, used to seed the session-spanning progress counter. Defaults to 0
|
|
58
|
+
* for callers that don't need cross-session accuracy in the progress display.
|
|
56
59
|
*/
|
|
57
|
-
resume(pending: string[], scraped: string[], resources: string[]): void;
|
|
60
|
+
resume(pending: string[], scraped: string[], resources: string[], pagesScrapedOffset?: number): void;
|
|
58
61
|
/**
|
|
59
62
|
* Start crawling from one or more root URLs.
|
|
60
63
|
*
|