bidi2pdf 0.1.15 → 0.1.17
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +46 -2
- data/README.md +233 -11
- data/docker/Dockerfile +11 -0
- data/docker/Dockerfile.chromedriver +7 -0
- data/docker/Dockerfile.slim +12 -1
- data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +9 -0
- data/lib/bidi2pdf/bidi/session.rb +5 -1
- data/lib/bidi2pdf/cli/json_output.rb +30 -0
- data/lib/bidi2pdf/cli.rb +559 -17
- data/lib/bidi2pdf/diagnose.rb +119 -0
- data/lib/bidi2pdf/error_codes.rb +59 -0
- data/lib/bidi2pdf/exit_codes.rb +44 -0
- data/lib/bidi2pdf/launcher.rb +23 -0
- data/lib/bidi2pdf/manifest.rb +65 -0
- data/lib/bidi2pdf/notifications/json_subscriber.rb +78 -0
- data/lib/bidi2pdf/pdf_inspection.rb +62 -0
- data/lib/bidi2pdf/recipe/loader.rb +44 -0
- data/lib/bidi2pdf/recipe/runner.rb +229 -0
- data/lib/bidi2pdf/recipe/schema_shape.rb +228 -0
- data/lib/bidi2pdf/recipe/validator.rb +138 -0
- data/lib/bidi2pdf/recipe.rb +83 -0
- data/lib/bidi2pdf/result.rb +67 -0
- data/lib/bidi2pdf/result_collector.rb +150 -0
- data/lib/bidi2pdf/schema.rb +382 -0
- data/lib/bidi2pdf/session_registry.rb +97 -0
- data/lib/bidi2pdf/session_runner.rb +42 -0
- data/lib/bidi2pdf/session_sweeper.rb +64 -0
- data/lib/bidi2pdf/session_warmer.rb +51 -1
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +92 -8
- data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +7 -0
- data/sig/bidi2pdf/bidi/session.rbs +6 -0
- data/sig/bidi2pdf/cli/json_output.rbs +19 -0
- data/sig/bidi2pdf/cli.rbs +112 -2
- data/sig/bidi2pdf/diagnose.rbs +30 -0
- data/sig/bidi2pdf/error_codes.rbs +21 -0
- data/sig/bidi2pdf/exit_codes.rbs +21 -0
- data/sig/bidi2pdf/launcher.rbs +9 -0
- data/sig/bidi2pdf/manifest.rbs +38 -0
- data/sig/bidi2pdf/notifications/json_subscriber.rbs +45 -0
- data/sig/bidi2pdf/pdf_inspection.rbs +33 -0
- data/sig/bidi2pdf/recipe/loader.rbs +20 -0
- data/sig/bidi2pdf/recipe/runner.rbs +92 -0
- data/sig/bidi2pdf/recipe/schema_shape.rbs +85 -0
- data/sig/bidi2pdf/recipe/validator.rbs +54 -0
- data/sig/bidi2pdf/recipe.rbs +72 -0
- data/sig/bidi2pdf/result.rbs +71 -0
- data/sig/bidi2pdf/result_collector.rbs +106 -0
- data/sig/bidi2pdf/schema.rbs +45 -0
- data/sig/bidi2pdf/session_registry.rbs +44 -0
- data/sig/bidi2pdf/session_runner.rbs +17 -0
- data/sig/bidi2pdf/session_sweeper.rbs +36 -0
- data/sig/bidi2pdf/session_warmer.rbs +33 -0
- data/sig/bidi2pdf/version.rbs +1 -1
- data/sig/bidi2pdf.rbs +66 -0
- metadata +43 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b879e9c84ccd1584f5b2eb1b406972624cd8b64a50c92466e78f7275ef229441
|
|
4
|
+
data.tar.gz: 50d517dac875995c2a06ca39dcd8c521c0b18fd238b6950d5e0d9674187941dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fca9af905a088093ff4c8ac3a1c78b4f6138068c27ad4cf87616c7ded67f44b3d57aab73b40d83b6c9d8d63097f2851b10f5b0531a91bc22088ec15aab21769
|
|
7
|
+
data.tar.gz: 835727303208ada398c0ec7456e29d49201d2b798a34edac35007c40ae5123e2a382a6e325e978237fd328ef0d58e6f573d61b1447df54bae51f9188775ab4c6
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
# Keep RuboCop's own default excludes (vendor/**, tmp/**, node_modules/**, .git/**) when adding to
|
|
2
|
+
# AllCops/Exclude below - without this, a project-level Exclude replaces them and vendor/bundle gets
|
|
3
|
+
# linted.
|
|
4
|
+
inherit_mode:
|
|
5
|
+
merge:
|
|
6
|
+
- Exclude
|
|
7
|
+
|
|
1
8
|
AllCops:
|
|
2
9
|
NewCops: enable
|
|
3
10
|
TargetRubyVersion: 3.4
|
|
11
|
+
Exclude:
|
|
12
|
+
# Local Claude devbox (gitignored). Its Dockerfile.ruby ends in ".ruby", which RuboCop takes for
|
|
13
|
+
# a Ruby source file and then fails to parse.
|
|
14
|
+
- 'docker-claude/**/*'
|
|
4
15
|
|
|
5
16
|
Style/StringLiterals:
|
|
6
17
|
EnforcedStyle: double_quotes
|
|
@@ -79,6 +90,15 @@ RSpec/DescribeClass:
|
|
|
79
90
|
Exclude:
|
|
80
91
|
- 'spec/acceptance/**/*_spec.rb'
|
|
81
92
|
|
|
93
|
+
# Matches bidi2pdf-rails' own spec/rails_helper.rb: scenario/when_ are custom
|
|
94
|
+
# alias_example_group_to groups there (and here, see spec/spec_helper.rb), not the single example
|
|
95
|
+
# Capybara's own `scenario` conventionally means - this cop doesn't know that and miscounts every
|
|
96
|
+
# then_/and_ nested underneath as belonging to one example.
|
|
97
|
+
RSpec/MultipleExpectations:
|
|
98
|
+
Enabled: true
|
|
99
|
+
Exclude:
|
|
100
|
+
- 'spec/acceptance/**/*_spec.rb'
|
|
101
|
+
|
|
82
102
|
plugins:
|
|
83
103
|
- rubocop-rake
|
|
84
104
|
- rubocop-rspec
|
data/CHANGELOG.md
CHANGED
|
@@ -8,10 +8,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
-
[unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.
|
|
11
|
+
[unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.17..HEAD
|
|
12
12
|
|
|
13
13
|
<!-- generated by git-cliff end -->
|
|
14
14
|
|
|
15
|
+
## [0.1.17] - 2026-09-29
|
|
16
|
+
|
|
17
|
+
### 🐛 Fixed
|
|
18
|
+
- Let Chromium start on a read-only root
|
|
19
|
+
- Do not raise when our own close ends a write
|
|
20
|
+
|
|
21
|
+
### 📝 Docs
|
|
22
|
+
- Describe latest as the newest Chromium build
|
|
23
|
+
|
|
24
|
+
### 🔄 Changed
|
|
25
|
+
- Merge pull request #143 from dieter-medium/feat/chromedriver-sha-tags
|
|
26
|
+
- Merge pull request #140 from dieter-medium/fix/chromium-read-only-root
|
|
27
|
+
- Merge pull request #139 from dieter-medium/fix/websocket-close-during-write
|
|
28
|
+
- Merge pull request #138 from dieter-medium/feat/session-warmer-orphan-sweep
|
|
29
|
+
- Merge pull request #132 from dieter-medium/dependabot/bundler/main/rubyzip-3.6.0
|
|
30
|
+
|
|
31
|
+
### 🔧 Build
|
|
32
|
+
- Update rubyzip requirement from ~> 2.4 to >= 2.4, < 4.0
|
|
33
|
+
|
|
34
|
+
### 🚀 Added
|
|
35
|
+
- Tag every chromedriver image build by commit
|
|
36
|
+
- Close leftover warm sessions on start
|
|
37
|
+
|
|
38
|
+
## [0.1.16] - 2026-09-22
|
|
39
|
+
|
|
40
|
+
### 🐛 Fixed
|
|
41
|
+
- Give NavigationDNSError its own initializer
|
|
42
|
+
- Enforce the complete recipe shape in --validate, not just semantic checks
|
|
43
|
+
- Close schema/validator mismatches in recipe schema
|
|
44
|
+
- Document and address blank PDFs from Chrome's network checks
|
|
45
|
+
- Add browser tab cookie integration specs
|
|
46
|
+
|
|
47
|
+
### 🔄 Changed
|
|
48
|
+
- Merge pull request #134 from dieter-medium/feat/llm-friendly-cli
|
|
49
|
+
- Merge pull request #133 from dieter-medium/feat/llm-friendly-cli
|
|
50
|
+
- Merge pull request #131 from dieter-medium/docs/local-network-access
|
|
51
|
+
- Merge pull request #130 from dieter-medium/fix/host-only-cookie-domain
|
|
52
|
+
- Merge pull request #126 from dieter-medium/chore-fix-release-creds-check
|
|
53
|
+
|
|
54
|
+
### 🚀 Added
|
|
55
|
+
- Agent-friendly CLI - structured JSON, diagnose, and recipes
|
|
56
|
+
|
|
15
57
|
## [0.1.15] - 2026-09-20
|
|
16
58
|
|
|
17
59
|
### 🎨 Refactored
|
|
@@ -453,7 +495,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
453
495
|
|
|
454
496
|
### 🔄 Released
|
|
455
497
|
|
|
456
|
-
- [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.
|
|
498
|
+
- [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.17..HEAD)
|
|
499
|
+
- [0.1.17](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.16..v0.1.17)
|
|
500
|
+
- [0.1.16](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.15..v0.1.16)
|
|
457
501
|
- [0.1.15](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.14..v0.1.15)
|
|
458
502
|
- [0.1.14](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.13..v0.1.14)
|
|
459
503
|
- [0.1.13](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.12..v0.1.13)
|
data/README.md
CHANGED
|
@@ -20,16 +20,17 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
|
|
|
20
20
|
3. [Why BiDi?](#why-bidi-instead-of-cdp)
|
|
21
21
|
4. [Installation](#installation)
|
|
22
22
|
5. [CLI Usage](#cli-usage)
|
|
23
|
-
6. [
|
|
24
|
-
7. [
|
|
25
|
-
8. [
|
|
26
|
-
9. [
|
|
27
|
-
10. [
|
|
28
|
-
11. [
|
|
29
|
-
12. [
|
|
30
|
-
13. [
|
|
31
|
-
14. [
|
|
32
|
-
15. [
|
|
23
|
+
6. [Agent and Automation Usage](#agent-and-automation-usage)
|
|
24
|
+
7. [Library API](#library-api)
|
|
25
|
+
8. [Architecture](#architecture)
|
|
26
|
+
9. [Docker](#docker)
|
|
27
|
+
10. [Configuration Options](#configuration-options)
|
|
28
|
+
11. [Programmatic Configuration](#programmatic-configuration)
|
|
29
|
+
12. [Rails Integration](#rails-integration)
|
|
30
|
+
13. [Test Helpers](#test-helpers)
|
|
31
|
+
14. [Development](#development)
|
|
32
|
+
15. [Contributing](#contributing)
|
|
33
|
+
16. [License](#license)
|
|
33
34
|
|
|
34
35
|
## ✨ Key Features
|
|
35
36
|
|
|
@@ -40,7 +41,9 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
|
|
|
40
41
|
✅ **Docker-ready** – Plug and play with containers
|
|
41
42
|
✅ **Modern architecture** – Built on Chrome's next-gen BiDi protocol
|
|
42
43
|
✅ **Network logging** – Know which requests fail during rendering
|
|
43
|
-
✅ **Console log capture** – See what goes wrong inside the browser
|
|
44
|
+
✅ **Console log capture** – See what goes wrong inside the browser
|
|
45
|
+
✅ **Agent-ready** – Structured JSON output, NDJSON progress streaming, a page diagnostic, and
|
|
46
|
+
declarative recipes, so LLM agents and CI can drive it without parsing logs
|
|
44
47
|
|
|
45
48
|
---
|
|
46
49
|
|
|
@@ -109,6 +112,152 @@ bidi2pdf render \
|
|
|
109
112
|
|
|
110
113
|
---
|
|
111
114
|
|
|
115
|
+
## 🤖 Agent and Automation Usage
|
|
116
|
+
|
|
117
|
+
Every command below also works without `--json` (human-readable output on stdout); with it, stdout
|
|
118
|
+
carries exactly one JSON document and nothing else - safe to pipe into `jq` or parse directly.
|
|
119
|
+
Human-readable logs move to stderr for the duration of any `--json`/`--output -` call, never
|
|
120
|
+
mixing into stdout. `--json-stream` works independently of the two: it always writes progress
|
|
121
|
+
events to stderr, whether or not `--json` is also given - in human mode, that just means
|
|
122
|
+
human-readable output keeps going to stdout as usual, alongside the stream. Full JSON Schema for
|
|
123
|
+
every shape is built into the gem, so an agent can discover it without reading this file:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# 1. Check compatibility first - no browser launched
|
|
127
|
+
bidi2pdf version --json
|
|
128
|
+
|
|
129
|
+
# 2. Discover the shape of each command's own result, a manifest, an NDJSON event, or a recipe file
|
|
130
|
+
bidi2pdf schema render
|
|
131
|
+
bidi2pdf schema diagnose
|
|
132
|
+
bidi2pdf schema run
|
|
133
|
+
bidi2pdf schema manifest
|
|
134
|
+
bidi2pdf schema event
|
|
135
|
+
bidi2pdf schema recipe
|
|
136
|
+
|
|
137
|
+
# 3. Validate a recipe - no browser launched, the cheapest way to iterate on one
|
|
138
|
+
bidi2pdf run recipe.yml --validate
|
|
139
|
+
|
|
140
|
+
# 4. Run it for real
|
|
141
|
+
bidi2pdf run recipe.yml --json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Structured render output
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
bidi2pdf render --url https://example.com/invoice/14432423 --output example.pdf --json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"schema_version": 1,
|
|
153
|
+
"ok": true,
|
|
154
|
+
"command": "render",
|
|
155
|
+
"output": "example.pdf",
|
|
156
|
+
"bytes": 182734,
|
|
157
|
+
"sha256": "abcd...",
|
|
158
|
+
"pages": 2,
|
|
159
|
+
"duration_ms": 842,
|
|
160
|
+
"navigation": { "requested_url": "https://example.com/invoice/14432423", "final_url": "https://example.com/invoice/14432423", "status": 200 },
|
|
161
|
+
"console": [],
|
|
162
|
+
"network_failures": [],
|
|
163
|
+
"warnings": [],
|
|
164
|
+
"error": null
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`pages` is `null`, with a warning explaining why, when the optional `pdf-reader` gem isn't
|
|
169
|
+
installed - see [Docker](#docker) for why the published images always have it. A failed render
|
|
170
|
+
still emits exactly this shape, with `ok: false` and a structured `error` (`code`, `message`,
|
|
171
|
+
`retryable`, `hint`, `details`) instead of a stack trace:
|
|
172
|
+
|
|
173
|
+
| Exit code | Meaning |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `0` | success |
|
|
176
|
+
| `2` | CLI, configuration, or recipe-validation error |
|
|
177
|
+
| `3` | browser or navigation error |
|
|
178
|
+
| `4` | page not as expected (a recipe action/assertion, or a diagnose selector, failed) |
|
|
179
|
+
| `6` | output or PDF generation failure |
|
|
180
|
+
| `70` | unexpected internal error |
|
|
181
|
+
|
|
182
|
+
### stdin/stdout, manifests, and progress streaming
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Render HTML piped on stdin, PDF bytes piped out on stdout - no files touched
|
|
186
|
+
cat page.html | bidi2pdf render --stdin --output - > page.pdf
|
|
187
|
+
|
|
188
|
+
# A render manifest: enough to reproduce and diagnose the render later
|
|
189
|
+
bidi2pdf render --url https://example.com --output example.pdf --manifest render.json
|
|
190
|
+
|
|
191
|
+
# One JSON progress event per stderr line as the render happens - works with human-readable
|
|
192
|
+
# output too, not just --json
|
|
193
|
+
bidi2pdf render --url https://example.com --output example.pdf --json-stream
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Diagnosing a page before trusting its PDF
|
|
197
|
+
|
|
198
|
+
`bidi2pdf diagnose` loads a page like `render` does but produces no PDF - it answers *why does the
|
|
199
|
+
PDF not look like the page* (console errors, failed requests, font-loading status, `@media
|
|
200
|
+
print`/`@page` rules, fixed/sticky elements, Paged.js detection), not what the page's content is:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
bidi2pdf diagnose --url https://example.com/invoice/14432423 --json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Declarative recipes
|
|
207
|
+
|
|
208
|
+
A recipe is a rendering contract - the waits a page needs before it's printed, and the properties
|
|
209
|
+
the resulting PDF must have - not a general browser-automation script:
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
# invoice.yml
|
|
213
|
+
version: 1
|
|
214
|
+
|
|
215
|
+
source:
|
|
216
|
+
url: https://example.com/invoice/123
|
|
217
|
+
|
|
218
|
+
actions:
|
|
219
|
+
- wait_for:
|
|
220
|
+
selector: "#invoice"
|
|
221
|
+
timeout: 10
|
|
222
|
+
- click:
|
|
223
|
+
selector: "#show-details"
|
|
224
|
+
- wait_network_idle:
|
|
225
|
+
timeout: 10
|
|
226
|
+
|
|
227
|
+
assert:
|
|
228
|
+
- selector_exists:
|
|
229
|
+
selector: "#total"
|
|
230
|
+
- no_console_errors: true
|
|
231
|
+
- page_count: 2
|
|
232
|
+
- pdf_text_present:
|
|
233
|
+
text: "Invoice #123"
|
|
234
|
+
|
|
235
|
+
output:
|
|
236
|
+
pdf: invoice.pdf
|
|
237
|
+
manifest: invoice.json
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
bidi2pdf run invoice.yml --validate # schema + known actions/assertions, no browser
|
|
242
|
+
bidi2pdf run invoice.yml --json # actions, then the PDF, then assertions against it
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Actions (`wait_for`, `click`, `evaluate`, `inject_script`, `inject_style`, `set_viewport`,
|
|
246
|
+
`wait_network_idle`) and page assertions (`selector_exists`, `text_present`, `no_console_errors`,
|
|
247
|
+
`no_network_failures`, `fonts_loaded`) are a thin layer over the same `BrowserTab` methods the
|
|
248
|
+
[Programmatic API](#-programmatic-api) below uses directly. PDF assertions (`page_count`,
|
|
249
|
+
`pdf_text_present`, `pdf_not_blank`) need the `pdf-reader` gem - a recipe using one fails
|
|
250
|
+
`--validate` immediately, before any browser launches, when it isn't installed.
|
|
251
|
+
|
|
252
|
+
`no_console_errors`, `no_network_failures`, `fonts_loaded`, and `pdf_not_blank` are presence-only
|
|
253
|
+
assertions - the step is either there or it isn't, nothing reads the value beside it - so they must
|
|
254
|
+
be written as `true` exactly, e.g. `- no_console_errors: true`; `false` (or any other value) is
|
|
255
|
+
rejected by both `bidi2pdf schema recipe` and `--validate` rather than being silently ignored.
|
|
256
|
+
`wait_for` needs exactly one of `selector`, `paged_js`, `script` - zero or more than one is
|
|
257
|
+
rejected the same way, before any browser launches.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
112
261
|
## 🧠 Programmatic API
|
|
113
262
|
|
|
114
263
|
### Classic Approach
|
|
@@ -286,6 +435,30 @@ docker run -it --rm \
|
|
|
286
435
|
|
|
287
436
|
✅ Tip: Mount your local directory (e.g. ./output) to /reports in the container to easily access the generated PDFs.
|
|
288
437
|
|
|
438
|
+
✅ All images run with a read-only root filesystem (`--read-only`) as long as `/tmp` is writable,
|
|
439
|
+
e.g. `--tmpfs /tmp`: they point Chromium's `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` there, without
|
|
440
|
+
which Chromium's crash handler fails to start and every launch aborts with
|
|
441
|
+
`chrome_crashpad_handler: --database is required`.
|
|
442
|
+
|
|
443
|
+
✅ Both published images also install [`pdf-reader`](https://github.com/yob/pdf-reader) - not a
|
|
444
|
+
runtime dependency of the gem itself (see [Agent and Automation Usage](#agent-and-automation-usage)) -
|
|
445
|
+
so `pages` and the `page_count`/`pdf_text_present`/`pdf_not_blank` recipe assertions work out of
|
|
446
|
+
the box in either image, no extra install step needed.
|
|
447
|
+
|
|
448
|
+
### ChromeDriver image tags
|
|
449
|
+
|
|
450
|
+
[`dieters877565/chromedriver`](https://hub.docker.com/r/dieters877565/chromedriver) is built for
|
|
451
|
+
`linux/amd64` and `linux/arm64` with these tags:
|
|
452
|
+
|
|
453
|
+
| Tag | Moves? | Use |
|
|
454
|
+
|---|---|---|
|
|
455
|
+
| `0.1.17` (a release) | no | a fixed Chromium, pinned together with the gem version |
|
|
456
|
+
| `sha-<short commit>` | only if that commit is built again by hand | a fix on `main` not released yet |
|
|
457
|
+
| `latest`, `main` | yes, every push to `main` | the newest build and its Chromium security fixes |
|
|
458
|
+
|
|
459
|
+
Chromium comes from Debian's packages at build time, so every build can carry a different
|
|
460
|
+
Chromium - for a byte-exact pin use the digest (`docker buildx imagetools inspect <image:tag>`).
|
|
461
|
+
|
|
289
462
|
### Docker Compose
|
|
290
463
|
|
|
291
464
|
```bash
|
|
@@ -332,6 +505,15 @@ docker compose -f docker/docker-compose.yml down
|
|
|
332
505
|
| `--log_level` | Log level: debug, info, warn, error, fatal |
|
|
333
506
|
| `--remote_browser_url` | Connect to remote Chrome session |
|
|
334
507
|
| `--default_timeout` | Operation timeout (default: 60s) |
|
|
508
|
+
| `--json` | Emit a single structured JSON result document (see `bidi2pdf schema render`) |
|
|
509
|
+
| `--json_stream` | Emit one JSON progress event per line to stderr (see `bidi2pdf schema event`) - works with or without `--json` |
|
|
510
|
+
| `--stdin` | Read the HTML document from stdin, instead of `--url`/`--html-file` |
|
|
511
|
+
| `--output -` | Write raw PDF bytes to stdout instead of a file |
|
|
512
|
+
| `--manifest FILE` | Write a render manifest (see `bidi2pdf schema manifest`) to `FILE` |
|
|
513
|
+
|
|
514
|
+
See [Agent and Automation Usage](#agent-and-automation-usage) above for `bidi2pdf diagnose`,
|
|
515
|
+
`bidi2pdf run recipe.yml`, `bidi2pdf schema <kind>`, and `bidi2pdf version --json` - a separate
|
|
516
|
+
set of commands with their own options, not additional flags on `render`.
|
|
335
517
|
|
|
336
518
|
---
|
|
337
519
|
|
|
@@ -388,6 +570,24 @@ Bidi2pdf::SessionWarmer.shutdown
|
|
|
388
570
|
| `headless` | `true` | Run Chrome headless. |
|
|
389
571
|
| `chrome_args` | `DEFAULT_CHROME_ARGS` | Chrome launch arguments. |
|
|
390
572
|
| `remote_browser_url` | `nil` | Connect each slot to a remote chromedriver instead of starting a local one. |
|
|
573
|
+
| `orphan_age` | `:auto` | Remote only: on start, close sessions other warmers left behind older than this (`:auto` = 2 × `max_idle_age`, `nil` = off). |
|
|
574
|
+
| `registry_dir` | `Dir.tmpdir` | Where the session registry file lives - every process that should clean up after the others must share it. |
|
|
575
|
+
|
|
576
|
+
#### Leftover sessions on a shared chromedriver
|
|
577
|
+
|
|
578
|
+
A remote chromedriver keeps a session - a whole Chrome - until someone deletes it. A warmer closes
|
|
579
|
+
its own sessions when they idle past `max_idle_age` and when the process shuts down cleanly, but a
|
|
580
|
+
process that is killed or crashes leaves its sessions open, and enough of them stop the container
|
|
581
|
+
from starting any new Chrome. So in remote mode each warmer records the sessions it opens in a small
|
|
582
|
+
registry file (`<registry_dir>/bidi2pdf-sessions-<hash of the URL>.json`, mode 0600), and on start
|
|
583
|
+
closes recorded sessions older than `orphan_age`. The default, twice `max_idle_age`, is beyond the
|
|
584
|
+
point where any live warmer would already have recycled its own spare, so a running process never
|
|
585
|
+
loses one. Sessions nobody recorded (other tools on the same chromedriver) are never touched.
|
|
586
|
+
chromedriver drops custom capabilities, so a session cannot carry a tag of its own - hence the file.
|
|
587
|
+
|
|
588
|
+
Everything here is fail-open: if the registry directory is not writable, the warmer logs one
|
|
589
|
+
warning, instruments `session_warmer.registry_unavailable.bidi2pdf`, and keeps rendering - only the
|
|
590
|
+
cleanup is off. Closed leftovers are reported as `session_warmer.orphans_closed.bidi2pdf`.
|
|
391
591
|
|
|
392
592
|
> **Security note:** an idle warm session is an open, unauthenticated automation endpoint
|
|
393
593
|
> (chromedriver's port, Chrome's debugging port - loopback only for a local chromedriver) for as
|
|
@@ -395,6 +595,28 @@ Bidi2pdf::SessionWarmer.shutdown
|
|
|
395
595
|
> nothing else can reach those ports. With `remote_browser_url`, who can reach that endpoint on the
|
|
396
596
|
> network is what matters, exactly as it does without the warmer.
|
|
397
597
|
|
|
598
|
+
### Customizing Chrome arguments (and blank PDFs from inline HTML)
|
|
599
|
+
|
|
600
|
+
`Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS` already contains one `--disable-features=...` entry,
|
|
601
|
+
and Chrome only honors the **last** occurrence of that switch. To turn another feature off, extend
|
|
602
|
+
that entry instead of appending a second switch, which would silently drop the defaults:
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
chrome_args = Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.map do |arg|
|
|
606
|
+
arg.start_with?("--disable-features=") ? "#{arg},LocalNetworkAccessChecks" : arg
|
|
607
|
+
end
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`LocalNetworkAccessChecks` is the one you are most likely to need. HTML rendered through
|
|
611
|
+
`BrowserTab#render_html_content` is loaded as a `data:` URL, which Chrome treats as a public origin;
|
|
612
|
+
if that HTML references assets on a private address (`localhost`, a Docker hostname, ...), recent
|
|
613
|
+
Chrome versions (confirmed with Chrome 153) block those requests before they are sent. Nothing
|
|
614
|
+
raises - the assets show up as failed network events, the server never sees a request, and the PDF
|
|
615
|
+
comes out unstyled or blank. Disable the check only where the asset host really is private, and keep
|
|
616
|
+
it on when rendering pages you do not control.
|
|
617
|
+
The [bidi2pdf-rails README](https://github.com/dieter-medium/bidi2pdf-rails#readme) has the full
|
|
618
|
+
walkthrough under "Blank PDFs: Chrome's Local Network Access Check".
|
|
619
|
+
|
|
398
620
|
---
|
|
399
621
|
|
|
400
622
|
## 🚂 Rails Integration
|
data/docker/Dockerfile
CHANGED
|
@@ -34,10 +34,21 @@ WORKDIR /app
|
|
|
34
34
|
# Copy your gem into container
|
|
35
35
|
COPY ./pkg/bidi2pdf-*.gem ./
|
|
36
36
|
|
|
37
|
+
# pdf-reader is deliberately not a bidi2pdf runtime dependency - it is installed here, as its own
|
|
38
|
+
# step, so the published image always has `pages`/PDF assertions available, the environment an
|
|
39
|
+
# agent or CI job actually renders in.
|
|
37
40
|
RUN gem install ./bidi2pdf-*.gem && \
|
|
41
|
+
gem install pdf-reader -v "~> 2.14" && \
|
|
38
42
|
chown -R appuser:appuser /app
|
|
39
43
|
|
|
40
44
|
# Switch to non-root user
|
|
45
|
+
# Chromium's crash handler gets its database path from the XDG config dir. On a read-only root
|
|
46
|
+
# (docker run --read-only) that dir cannot be created, the handler refuses to start
|
|
47
|
+
# ("--database is required") and Chrome aborts on every launch. /tmp is the one place such a
|
|
48
|
+
# container is expected to mount writable (--tmpfs /tmp).
|
|
49
|
+
ENV XDG_CONFIG_HOME=/tmp/xdg-config \
|
|
50
|
+
XDG_CACHE_HOME=/tmp/xdg-cache
|
|
51
|
+
|
|
41
52
|
USER appuser
|
|
42
53
|
|
|
43
54
|
CMD ["/usr/bin/bash"]
|
|
@@ -53,6 +53,13 @@ WORKDIR /app
|
|
|
53
53
|
RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
|
|
54
54
|
|
|
55
55
|
# Switch to non-root user
|
|
56
|
+
# Chromium's crash handler gets its database path from the XDG config dir. On a read-only root
|
|
57
|
+
# (docker run --read-only) that dir cannot be created, the handler refuses to start
|
|
58
|
+
# ("--database is required") and Chrome aborts on every launch. /tmp is the one place such a
|
|
59
|
+
# container is expected to mount writable (--tmpfs /tmp).
|
|
60
|
+
ENV XDG_CONFIG_HOME=/tmp/xdg-config \
|
|
61
|
+
XDG_CACHE_HOME=/tmp/xdg-cache
|
|
62
|
+
|
|
56
63
|
USER appuser
|
|
57
64
|
|
|
58
65
|
# RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
|
data/docker/Dockerfile.slim
CHANGED
|
@@ -26,7 +26,11 @@ WORKDIR /app
|
|
|
26
26
|
# Copy your gem into container
|
|
27
27
|
COPY ./pkg/bidi2pdf-*.gem ./
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# pdf-reader is deliberately not a bidi2pdf runtime dependency - it is installed here, as its own
|
|
30
|
+
# step, so the published image always has `pages`/PDF assertions available, the environment an
|
|
31
|
+
# agent or CI job actually renders in.
|
|
32
|
+
RUN gem install ./bidi2pdf-*.gem && \
|
|
33
|
+
gem install pdf-reader -v "~> 2.14"
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
# Stage 2
|
|
@@ -69,6 +73,13 @@ WORKDIR /app
|
|
|
69
73
|
RUN chown -R appuser:appuser /app
|
|
70
74
|
|
|
71
75
|
# Switch to non-root user
|
|
76
|
+
# Chromium's crash handler gets its database path from the XDG config dir. On a read-only root
|
|
77
|
+
# (docker run --read-only) that dir cannot be created, the handler refuses to start
|
|
78
|
+
# ("--database is required") and Chrome aborts on every launch. /tmp is the one place such a
|
|
79
|
+
# container is expected to mount writable (--tmpfs /tmp).
|
|
80
|
+
ENV XDG_CONFIG_HOME=/tmp/xdg-config \
|
|
81
|
+
XDG_CACHE_HOME=/tmp/xdg-cache
|
|
82
|
+
|
|
72
83
|
USER appuser
|
|
73
84
|
|
|
74
85
|
CMD ["/usr/bin/bash"]
|
|
@@ -92,8 +92,17 @@ module Bidi2pdf
|
|
|
92
92
|
@listeners_mutex.synchronize { @listeners[event].dup }.each { |listener| listener.call(*) }
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# The reader closes the socket from its own thread when the peer hangs up, and #close does not
|
|
96
|
+
# wait for a write in flight - so a write can fail with "stream closed in another thread" (or
|
|
97
|
+
# EPIPE) because *we* closed it. By then the connection is over and :close has been emitted;
|
|
98
|
+
# there is nothing left to report. Seen in the handshake write of #connect, which had no
|
|
99
|
+
# rescue: a server that answered and hung up at once made #connect raise although the
|
|
100
|
+
# handshake went through. Any other write failure still raises (#send and #say_goodbye
|
|
101
|
+
# handle it).
|
|
95
102
|
def write(bytes)
|
|
96
103
|
@write_mutex.synchronize { @socket&.write bytes }
|
|
104
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
105
|
+
raise unless @closed
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
def say_goodbye
|
|
@@ -74,6 +74,10 @@ module Bidi2pdf
|
|
|
74
74
|
# @return [Array<String>] The Chrome arguments for the session.
|
|
75
75
|
attr_reader :chrome_args
|
|
76
76
|
|
|
77
|
+
# @return [String, nil] chromedriver's id for this session, once it was created - what
|
|
78
|
+
# SessionWarmer records so a later process can close it if this one dies uncleanly.
|
|
79
|
+
attr_reader :session_id
|
|
80
|
+
|
|
77
81
|
# Initializes a new session.
|
|
78
82
|
#
|
|
79
83
|
# @param [String] session_url The URL for the session.
|
|
@@ -227,7 +231,7 @@ module Bidi2pdf
|
|
|
227
231
|
value = session_data["value"]
|
|
228
232
|
handle_error(value) if value.nil? || value["error"]
|
|
229
233
|
|
|
230
|
-
session_id = value["sessionId"]
|
|
234
|
+
@session_id = value["sessionId"]
|
|
231
235
|
ws_url = value["capabilities"]["webSocketUrl"]
|
|
232
236
|
|
|
233
237
|
Bidi2pdf.logger.info "Created session with ID: #{session_id}"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bidi2pdf
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
# Shared machinery for --json/--json-stream/--output - across render/diagnose/run: reserving
|
|
6
|
+
# stdout for exactly one machine-readable payload, and mapping a Result to a process exit
|
|
7
|
+
# status.
|
|
8
|
+
module JsonOutput
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Bidi2pdf.logger (and friends) default to $stdout (see lib/bidi2pdf.rb), so a
|
|
12
|
+
# --json/--output - render has to redirect them for its duration or every log line would
|
|
13
|
+
# land in the same stream as the JSON document / PDF bytes it is trying to keep pure.
|
|
14
|
+
# Logger#reopen swaps the destination in place, so nothing else holding a reference to
|
|
15
|
+
# these loggers needs to know.
|
|
16
|
+
def reserve_stdout_for_machine_output
|
|
17
|
+
loggers = [Bidi2pdf.logger, Bidi2pdf.network_events_logger, Bidi2pdf.browser_console_logger].compact
|
|
18
|
+
loggers.each { |logger| logger.logger.reopen($stderr) }
|
|
19
|
+
|
|
20
|
+
yield
|
|
21
|
+
ensure
|
|
22
|
+
loggers.each { |logger| logger.logger.reopen($stdout) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def exit_for_result(result)
|
|
26
|
+
exit(result.ok? ? Bidi2pdf::ExitCodes::SUCCESS : Bidi2pdf::ExitCodes.for(result.error[:code]))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|