sghtmltopdf 0.4.0 → 0.5.1
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/Cargo.lock +14 -14
- data/README.md +27 -2
- data/ext/sghtmltopdf/Cargo.toml +14 -8
- data/ext/sghtmltopdf/extconf.rb +7 -7
- data/ext/sghtmltopdf/src/callback_sink.rs +89 -90
- data/ext/sghtmltopdf/src/errors.rs +27 -25
- data/ext/sghtmltopdf/src/gvl.rs +34 -34
- data/ext/sghtmltopdf/src/lib.rs +53 -56
- data/lib/sghtmltopdf/configuration.rb +17 -17
- data/lib/sghtmltopdf/options.rb +37 -40
- data/lib/sghtmltopdf/railtie.rb +5 -5
- data/lib/sghtmltopdf/renderer.rb +22 -22
- data/lib/sghtmltopdf/server_client.rb +18 -19
- data/lib/sghtmltopdf/version.rb +1 -1
- data/lib/sghtmltopdf.rb +33 -34
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1679b41d8745afc3af14b9ea754429a5c9f895b770af172cbbdfba5af87658d
|
|
4
|
+
data.tar.gz: 3a176472e76010d1aa22a365f71b02c0c8ffe1461a1e05518f1a7f7bc8180d7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 401481476fb5d2dd13aadbac6b5d1e4fe2280826bfb38b6d20be486e825a78a938536821b1a8338273ccf9f5d51317e1cdc1eb557fb779b97a9a32400f9190e3
|
|
7
|
+
data.tar.gz: 4d9002823ff97d3b625be63065c5547269eb7ffde84e65b1eabfef57d92ad0f033539be96a66dd1b1e761dc3bfbfc1ae02f71c7549708dc87f1c77c72f62300a
|
data/Cargo.lock
CHANGED
|
@@ -927,9 +927,9 @@ dependencies = [
|
|
|
927
927
|
|
|
928
928
|
[[package]]
|
|
929
929
|
name = "rustls"
|
|
930
|
-
version = "0.23.
|
|
930
|
+
version = "0.23.45"
|
|
931
931
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
932
|
-
checksum = "
|
|
932
|
+
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
|
933
933
|
dependencies = [
|
|
934
934
|
"log",
|
|
935
935
|
"once_cell",
|
|
@@ -951,9 +951,9 @@ dependencies = [
|
|
|
951
951
|
|
|
952
952
|
[[package]]
|
|
953
953
|
name = "rustls-webpki"
|
|
954
|
-
version = "0.103.
|
|
954
|
+
version = "0.103.15"
|
|
955
955
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
956
|
-
checksum = "
|
|
956
|
+
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
|
957
957
|
dependencies = [
|
|
958
958
|
"ring",
|
|
959
959
|
"rustls-pki-types",
|
|
@@ -1050,16 +1050,7 @@ dependencies = [
|
|
|
1050
1050
|
|
|
1051
1051
|
[[package]]
|
|
1052
1052
|
name = "sghtmltopdf"
|
|
1053
|
-
version = "0.
|
|
1054
|
-
dependencies = [
|
|
1055
|
-
"magnus",
|
|
1056
|
-
"rb-sys",
|
|
1057
|
-
"sghtmltopdf-core",
|
|
1058
|
-
]
|
|
1059
|
-
|
|
1060
|
-
[[package]]
|
|
1061
|
-
name = "sghtmltopdf-core"
|
|
1062
|
-
version = "0.4.0"
|
|
1053
|
+
version = "0.5.1"
|
|
1063
1054
|
dependencies = [
|
|
1064
1055
|
"base64",
|
|
1065
1056
|
"clap",
|
|
@@ -1084,6 +1075,15 @@ dependencies = [
|
|
|
1084
1075
|
"ureq",
|
|
1085
1076
|
]
|
|
1086
1077
|
|
|
1078
|
+
[[package]]
|
|
1079
|
+
name = "sghtmltopdf-ruby"
|
|
1080
|
+
version = "0.5.1"
|
|
1081
|
+
dependencies = [
|
|
1082
|
+
"magnus",
|
|
1083
|
+
"rb-sys",
|
|
1084
|
+
"sghtmltopdf",
|
|
1085
|
+
]
|
|
1086
|
+
|
|
1087
1087
|
[[package]]
|
|
1088
1088
|
name = "shell-words"
|
|
1089
1089
|
version = "1.1.1"
|
data/README.md
CHANGED
|
@@ -13,10 +13,10 @@ The engine runs inside your process through a native extension (magnus + rb-sys)
|
|
|
13
13
|
gem "sghtmltopdf"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Precompiled native gems are published for `x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`, `aarch64-linux-musl`, and `
|
|
16
|
+
Precompiled native gems are published for `x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`, and `x86_64-darwin`.
|
|
17
17
|
There is no build step on those platforms.
|
|
18
18
|
|
|
19
|
-
Elsewhere (
|
|
19
|
+
Elsewhere (Windows) the gem cannot run in-process — the source gem does not carry the Rust core and will refuse to build with an explanatory message.
|
|
20
20
|
Point those environments at a separate `sghtmltopdf server` process instead; see [Delegating to a server](#delegating-to-a-server).
|
|
21
21
|
|
|
22
22
|
Requires Ruby >= 3.2.
|
|
@@ -38,6 +38,31 @@ Sghtmltopdf.render_to_file(html, "invoice.pdf", page_size: "A4")
|
|
|
38
38
|
Sghtmltopdf.render(html) { |bytes| io.write(bytes) }
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Header and footer HTML
|
|
42
|
+
|
|
43
|
+
Pass markup directly without creating temporary files:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Sghtmltopdf.render(html,
|
|
47
|
+
header_html_content: '<div>Invoice [title]</div>',
|
|
48
|
+
footer_html_content: '<div>Page [page] of [topage]</div>')
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
These options work with `render`, block output, `render_to_file`, global
|
|
52
|
+
configuration, and server delegation. The shared CLI options are
|
|
53
|
+
`--header-html-content` and `--footer-html-content`.
|
|
54
|
+
|
|
55
|
+
`header_html` and `footer_html` still accept file paths. Supplying both a path
|
|
56
|
+
and content for the same side raises `Sghtmltopdf::UsageError`, including when
|
|
57
|
+
one comes from global configuration. `nil` or `false` disables a configured
|
|
58
|
+
option; an empty string is an explicitly empty header or footer. Either HTML
|
|
59
|
+
form takes precedence over simple text options for that side.
|
|
60
|
+
|
|
61
|
+
Content uses the same placeholder expansion and margin clipping as file input.
|
|
62
|
+
Embedded `data:` images are supported; external resources remain blocked.
|
|
63
|
+
With server delegation, markup travels in URL query parameters, so URL length
|
|
64
|
+
limits may apply and request logs may contain the markup.
|
|
65
|
+
|
|
41
66
|
## Rails
|
|
42
67
|
|
|
43
68
|
Adding the gem is enough; the Railtie wires everything up, and nothing is loaded when Rails is absent.
|
data/ext/sghtmltopdf/Cargo.toml
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
[package]
|
|
2
|
-
name
|
|
3
|
-
|
|
2
|
+
# The package name differs from the library name on purpose: the core crate is published as
|
|
3
|
+
# `sghtmltopdf`, and two packages with the same name cannot be written to one lockfile.
|
|
4
|
+
name = "sghtmltopdf-ruby"
|
|
5
|
+
version = "0.5.1"
|
|
4
6
|
edition = "2021"
|
|
5
7
|
publish = false
|
|
6
8
|
description = "Ruby binding for sghtmltopdf (see bindings/ruby)"
|
|
7
9
|
|
|
8
10
|
[lib]
|
|
9
|
-
# Ruby
|
|
11
|
+
# Loaded as a Ruby extension library (.so/.bundle). The name has to stay `sghtmltopdf`:
|
|
12
|
+
# rb_sys looks for `libsghtmltopdf.so` (it derives that from the gem name) and
|
|
13
|
+
# `Init_sghtmltopdf` is the entry point Ruby calls.
|
|
14
|
+
name = "sghtmltopdf"
|
|
10
15
|
crate-type = ["cdylib"]
|
|
11
16
|
|
|
12
17
|
[dependencies]
|
|
13
|
-
# `rb-sys` feature
|
|
18
|
+
# The `rb-sys` feature is what makes `AsRawValue`/`FromRawValue` available.
|
|
14
19
|
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
15
|
-
# GVL
|
|
20
|
+
# magnus has no way to release the GVL (`rb_thread_call_without_gvl`), so we call it directly.
|
|
16
21
|
rb-sys = "0.9"
|
|
17
|
-
# HTTP
|
|
18
|
-
# `svg
|
|
19
|
-
|
|
22
|
+
# The HTTP server (tiny_http) is not needed here, so the `server` feature is left out.
|
|
23
|
+
# `svg` matches the CLI default, so the gem can render SVG too.
|
|
24
|
+
# Renamed on import so that the `sghtmltopdf_core::` paths in the source stay as they are.
|
|
25
|
+
sghtmltopdf_core = { package = "sghtmltopdf", path = "../../../../core", default-features = false, features = [
|
|
20
26
|
"cli",
|
|
21
27
|
"svg",
|
|
22
28
|
] }
|
data/ext/sghtmltopdf/extconf.rb
CHANGED
|
@@ -6,16 +6,16 @@ require "rb_sys/mkmf"
|
|
|
6
6
|
core = File.expand_path("../../../../core", __dir__)
|
|
7
7
|
unless File.exist?(File.join(core, "Cargo.toml"))
|
|
8
8
|
abort <<~MESSAGE
|
|
9
|
-
sghtmltopdf: Rust
|
|
9
|
+
sghtmltopdf: the Rust core (#{core}) was not found.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
This gem is distributed as a precompiled gem for the supported platforms.
|
|
12
|
+
No prebuilt gem exists for your environment (#{RUBY_PLATFORM} / ruby #{RUBY_VERSION}),
|
|
13
|
+
so a build from source was attempted, but the source gem does not include the
|
|
14
|
+
Rust core and cannot be built.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Supported platforms: x86_64-linux / aarch64-linux / x86_64-linux-musl / aarch64-linux-musl / arm64-darwin / x86_64-darwin
|
|
17
17
|
MESSAGE
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# `lib/sghtmltopdf/sghtmltopdf.so
|
|
20
|
+
# Built as `lib/sghtmltopdf/sghtmltopdf.so`.
|
|
21
21
|
create_rust_makefile("sghtmltopdf/sghtmltopdf")
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! Handing the settled PDF bytes to a Ruby block, chunk by chunk.
|
|
2
2
|
//!
|
|
3
|
-
//! #
|
|
3
|
+
//! # How the threads are split
|
|
4
4
|
//!
|
|
5
|
-
//!
|
|
6
|
-
//! Ruby
|
|
7
|
-
//! `
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
5
|
+
//! Rendering recurses as deep as the DOM (style computation, layout, drawing).
|
|
6
|
+
//! A Ruby thread's machine stack is 1MiB by default (`thread_machine_stack_size` in
|
|
7
|
+
//! `RubyVM::DEFAULT_PARAMS`), so running it directly on a Puma worker thread overflows the
|
|
8
|
+
//! stack at a depth just under 200. Worse, it touches the guard page with the GVL released,
|
|
9
|
+
//! so rather than the process dying the thread hangs.
|
|
10
10
|
//!
|
|
11
|
-
//!
|
|
12
|
-
//!
|
|
13
|
-
//!
|
|
11
|
+
//! So rendering runs on a dedicated thread with a
|
|
12
|
+
//! [`sghtmltopdf_core::STACK_SIZE`] stack allocated explicitly, and the settled
|
|
13
|
+
//! chunks are passed back to the original thread over a channel. Only the original thread ever touches Ruby.
|
|
14
14
|
//!
|
|
15
15
|
//! ```text
|
|
16
|
-
//!
|
|
16
|
+
//! original thread (Ruby's, GVL released) rendering thread (16MiB)
|
|
17
17
|
//! recv(chunk) <---------- chunk ---------- Sink::write
|
|
18
18
|
//! with_gvl { block.call(chunk) }
|
|
19
|
-
//! send(ack) ------------ ack ----------> (
|
|
19
|
+
//! send(ack) ------------ ack ----------> (on to the next chunk)
|
|
20
20
|
//! ```
|
|
21
21
|
//!
|
|
22
|
-
//!
|
|
23
|
-
//! `
|
|
24
|
-
//!
|
|
25
|
-
//!
|
|
22
|
+
//! It only works this way round: [`crate::gvl::with_gvl`]'s `rb_thread_call_with_gvl`
|
|
23
|
+
//! assumes "this thread released the GVL through `rb_thread_call_without_gvl`" and cannot
|
|
24
|
+
//! be called from a thread Ruby does not know about. So the rendering thread never touches
|
|
25
|
+
//! Ruby at all.
|
|
26
26
|
|
|
27
27
|
use std::io;
|
|
28
28
|
use std::sync::mpsc::{Receiver, SyncSender};
|
|
@@ -30,29 +30,28 @@ use std::sync::mpsc::{Receiver, SyncSender};
|
|
|
30
30
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
31
31
|
use magnus::{block::Proc, Error, ExceptionClass, RString, Ruby, Value};
|
|
32
32
|
use rb_sys::VALUE;
|
|
33
|
-
use sghtmltopdf_core::
|
|
33
|
+
use sghtmltopdf_core::Sink;
|
|
34
34
|
|
|
35
35
|
use crate::gvl;
|
|
36
36
|
|
|
37
|
-
///
|
|
37
|
+
/// The error `Sink::write` returns when the block was interrupted.
|
|
38
38
|
///
|
|
39
|
-
/// `convert::render
|
|
40
|
-
///
|
|
41
|
-
///
|
|
39
|
+
/// `convert::render` requires `Sink<Output = (), Error = io::Error>`, so no Ruby-derived
|
|
40
|
+
/// information can ride on the error type. The real reason is put in [`PendingUnwind`] and
|
|
41
|
+
/// this is used purely as "the signal to unwind".
|
|
42
42
|
fn interrupted() -> io::Error {
|
|
43
|
-
io::Error::other("Ruby
|
|
43
|
+
io::Error::other("the Ruby block was interrupted")
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/// `rb_gc_register_address
|
|
46
|
+
/// Somewhere to keep a `VALUE` protected from the GC with `rb_gc_register_address`.
|
|
47
47
|
///
|
|
48
|
-
/// Ruby
|
|
49
|
-
/// GVL
|
|
50
|
-
///
|
|
51
|
-
///
|
|
52
|
-
/// 生かしたいVALUEは、必ずここへ登録する。
|
|
48
|
+
/// Ruby's conservative GC scans the machine stack to find VALUEs, but only as far as the
|
|
49
|
+
/// stack position at the moment the GVL was released (the machine context being saved then).
|
|
50
|
+
/// A value pushed onto the stack inside `without_gvl` lies beyond that and is not scanned.
|
|
51
|
+
/// Any VALUE that must survive across the released region has to be registered here.
|
|
53
52
|
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
53
|
+
/// The registered address is pinned with a `Box`. Even when GC compaction moves the object,
|
|
54
|
+
/// the contents at the registered address are updated, so no stale reference is held.
|
|
56
55
|
pub struct ValueSlot {
|
|
57
56
|
slot: Box<VALUE>,
|
|
58
57
|
}
|
|
@@ -64,12 +63,12 @@ impl ValueSlot {
|
|
|
64
63
|
Self { slot }
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/// GC
|
|
66
|
+
/// The address of the GC-registered slot.
|
|
68
67
|
pub fn addr(&self) -> *mut VALUE {
|
|
69
68
|
&*self.slot as *const VALUE as *mut VALUE
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
///
|
|
71
|
+
/// The current `VALUE`. Call only while holding the GVL.
|
|
73
72
|
pub fn get(&self) -> VALUE {
|
|
74
73
|
*self.slot
|
|
75
74
|
}
|
|
@@ -81,13 +80,13 @@ impl Drop for ValueSlot {
|
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
///
|
|
83
|
+
/// The address of a [`ValueSlot`], for carrying into a GVL-released region.
|
|
85
84
|
#[derive(Clone, Copy)]
|
|
86
85
|
pub struct BlockSlot(*mut VALUE);
|
|
87
86
|
|
|
88
|
-
// SAFETY:
|
|
89
|
-
//
|
|
90
|
-
// `ValueSlot
|
|
87
|
+
// SAFETY: in the released region the address is only carried around as a number; it is read
|
|
88
|
+
// as a `VALUE` only inside `with_gvl` (that is, while holding the GVL). What it points at is
|
|
89
|
+
// a `ValueSlot` registered with the GC and alive until `without_gvl` returns.
|
|
91
90
|
unsafe impl Send for BlockSlot {}
|
|
92
91
|
|
|
93
92
|
impl BlockSlot {
|
|
@@ -95,36 +94,36 @@ impl BlockSlot {
|
|
|
95
94
|
Self(slot.addr())
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
/// GVL
|
|
97
|
+
/// Convert back to a `Proc`, assuming the GVL is held.
|
|
99
98
|
fn proc(self) -> Option<Proc> {
|
|
100
99
|
let value = unsafe { Value::from_raw(*self.0) };
|
|
101
100
|
Proc::from_value(value)
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
///
|
|
104
|
+
/// The receptacle for carrying an exception or non-local exit thrown by the block out of the GVL-released region.
|
|
106
105
|
#[derive(Default)]
|
|
107
106
|
pub struct PendingUnwind {
|
|
108
107
|
unwind: Option<Unwind>,
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
enum Unwind {
|
|
112
|
-
/// Ruby
|
|
111
|
+
/// A Ruby exception object.
|
|
113
112
|
Exception(ValueSlot),
|
|
114
|
-
/// Rust
|
|
113
|
+
/// An error the Rust side (magnus) built. The class and the message are carried separately.
|
|
115
114
|
Raise { class: ValueSlot, message: String },
|
|
116
|
-
/// `break
|
|
115
|
+
/// A `break`, `return`, `throw` and so on. The value is the tag passed to `rb_jump_tag`.
|
|
117
116
|
Jump(i32),
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
impl PendingUnwind {
|
|
121
|
-
///
|
|
120
|
+
/// Whether the block was interrupted.
|
|
122
121
|
pub fn is_pending(&self) -> bool {
|
|
123
122
|
self.unwind.is_some()
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
/// magnus
|
|
127
|
-
/// GVL
|
|
125
|
+
/// Convert a magnus `Error` into a form that can cross the released region and store it.
|
|
126
|
+
/// Call it while holding the GVL (it registers with the GC).
|
|
128
127
|
fn store(&mut self, error: Error) {
|
|
129
128
|
use magnus::error::ErrorType;
|
|
130
129
|
|
|
@@ -141,26 +140,26 @@ impl PendingUnwind {
|
|
|
141
140
|
self.unwind = Some(unwind);
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
///
|
|
143
|
+
/// Return the stored interruption to Ruby. Call it while holding the GVL.
|
|
145
144
|
///
|
|
146
|
-
/// `break
|
|
147
|
-
///
|
|
148
|
-
/// (`ValueSlot
|
|
145
|
+
/// A non-local exit such as `break` is propagated faithfully with `rb_jump_tag`. This
|
|
146
|
+
/// function does not return, so call it once the Rust-side cleanup is done
|
|
147
|
+
/// (deregistering the `ValueSlot` from the GC is already handled inside it).
|
|
149
148
|
pub fn into_error(self) -> Option<Error> {
|
|
150
149
|
match self.unwind? {
|
|
151
150
|
Unwind::Exception(slot) => {
|
|
152
151
|
let value = unsafe { Value::from_raw(slot.get()) };
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
152
|
+
// Deregistration happens after the `Error` is built. From here on the caller
|
|
153
|
+
// returns to Ruby still holding the GVL, so it is within the conservative
|
|
154
|
+
// GC's scanning range.
|
|
156
155
|
let error = magnus::Exception::from_value(value).map(Error::from);
|
|
157
156
|
drop(slot);
|
|
158
157
|
Some(error.unwrap_or_else(|| {
|
|
159
158
|
Error::new(
|
|
160
159
|
Ruby::get()
|
|
161
|
-
.expect("GVL
|
|
160
|
+
.expect("it should be called while holding the GVL")
|
|
162
161
|
.exception_runtime_error(),
|
|
163
|
-
"
|
|
162
|
+
"could not restore the exception the block threw",
|
|
164
163
|
)
|
|
165
164
|
}))
|
|
166
165
|
}
|
|
@@ -171,24 +170,24 @@ impl PendingUnwind {
|
|
|
171
170
|
Some(error.unwrap_or_else(|| {
|
|
172
171
|
Error::new(
|
|
173
172
|
Ruby::get()
|
|
174
|
-
.expect("GVL
|
|
173
|
+
.expect("it should be called while holding the GVL")
|
|
175
174
|
.exception_runtime_error(),
|
|
176
|
-
"
|
|
175
|
+
"could not restore the block's interruption",
|
|
177
176
|
)
|
|
178
177
|
}))
|
|
179
178
|
}
|
|
180
|
-
// `rb_jump_tag
|
|
181
|
-
//
|
|
179
|
+
// `rb_jump_tag` does not return (`-> !`). Every other field of `self` has already
|
|
180
|
+
// been dropped by this point.
|
|
182
181
|
Unwind::Jump(tag) => unsafe { rb_sys::rb_jump_tag(tag) },
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
///
|
|
186
|
+
/// A Sink streaming the settled bytes to a channel in `chunk_size` pieces.
|
|
188
187
|
///
|
|
189
|
-
///
|
|
190
|
-
/// GVL
|
|
191
|
-
/// (rendezvous)
|
|
188
|
+
/// Used on the rendering thread. It never touches Ruby, so it is `Send` and free of the
|
|
189
|
+
/// GVL's constraints. It waits for the receiving side's acknowledgement after every chunk
|
|
190
|
+
/// (a rendezvous), so it cannot run ahead of the block's processing and pile up memory.
|
|
192
191
|
pub struct ChannelSink {
|
|
193
192
|
chunks: SyncSender<Vec<u8>>,
|
|
194
193
|
ack: Receiver<bool>,
|
|
@@ -202,16 +201,16 @@ impl ChannelSink {
|
|
|
202
201
|
chunks,
|
|
203
202
|
ack,
|
|
204
203
|
buf: Vec::new(),
|
|
205
|
-
// 0
|
|
204
|
+
// At 0 the GVL would be reacquired for every byte, hence the lower bound.
|
|
206
205
|
chunk_size: chunk_size.max(1),
|
|
207
206
|
}
|
|
208
207
|
}
|
|
209
208
|
|
|
210
|
-
///
|
|
209
|
+
/// Hand over one chunk and wait until the block has finished receiving it.
|
|
211
210
|
///
|
|
212
|
-
///
|
|
213
|
-
///
|
|
214
|
-
/// [`PendingUnwind`]
|
|
211
|
+
/// Both being unable to send (the receiver went away) and the block returning an
|
|
212
|
+
/// interruption unwind through [`interrupted`]. The real reason for the interruption is
|
|
213
|
+
/// in the receiver's [`PendingUnwind`].
|
|
215
214
|
fn hand_off(&mut self, chunk: Vec<u8>) -> Result<(), io::Error> {
|
|
216
215
|
if self.chunks.send(chunk).is_err() {
|
|
217
216
|
return Err(interrupted());
|
|
@@ -245,18 +244,18 @@ impl Sink for ChannelSink {
|
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
246
|
|
|
248
|
-
///
|
|
247
|
+
/// Hand one chunk to the Ruby block. Returns `false` on an interruption.
|
|
249
248
|
///
|
|
250
|
-
/// GVL
|
|
251
|
-
///
|
|
252
|
-
///
|
|
249
|
+
/// This is the only place the GVL is reacquired. The block call goes through magnus's
|
|
250
|
+
/// `Proc::call`, which wraps it in `rb_protect` internally, so an exception's longjmp cannot
|
|
251
|
+
/// jump over a Rust frame.
|
|
253
252
|
///
|
|
254
|
-
///
|
|
255
|
-
///
|
|
256
|
-
///
|
|
253
|
+
/// Storing the error also happens inside this region. Carrying a Ruby object (an exception)
|
|
254
|
+
/// out of `with_gvl` would put it outside the GC's scope the moment the GVL is released
|
|
255
|
+
/// (see the documentation of `gvl::with_gvl`). Only a boolean is carried out.
|
|
257
256
|
fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) -> bool {
|
|
258
257
|
gvl::with_gvl(move || {
|
|
259
|
-
let ruby = Ruby::get().expect("with_gvl
|
|
258
|
+
let ruby = Ruby::get().expect("inside with_gvl, so the GVL is held");
|
|
260
259
|
let result = match block.proc() {
|
|
261
260
|
Some(proc) => {
|
|
262
261
|
let chunk: RString = ruby.str_from_slice(&bytes);
|
|
@@ -264,13 +263,13 @@ fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) ->
|
|
|
264
263
|
}
|
|
265
264
|
None => Err(Error::new(
|
|
266
265
|
ruby.exception_runtime_error(),
|
|
267
|
-
"
|
|
266
|
+
"the block was lost",
|
|
268
267
|
)),
|
|
269
268
|
};
|
|
270
269
|
match result {
|
|
271
270
|
Ok(()) => true,
|
|
272
271
|
Err(error) => {
|
|
273
|
-
// GC
|
|
272
|
+
// Registering with the GC also happens here, while the GVL is held.
|
|
274
273
|
pending.store(error);
|
|
275
274
|
false
|
|
276
275
|
}
|
|
@@ -278,25 +277,25 @@ fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) ->
|
|
|
278
277
|
})
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
/// `render
|
|
282
|
-
///
|
|
280
|
+
/// Run `render` on a dedicated rendering thread and keep handing the chunks it produces to
|
|
281
|
+
/// the Ruby block from this thread.
|
|
283
282
|
///
|
|
284
|
-
/// GVL
|
|
285
|
-
///
|
|
283
|
+
/// Call it from inside a GVL-released region (inside `without_gvl`), on the thread that
|
|
284
|
+
/// released it. It is the left-hand side of the diagram in the module docs.
|
|
286
285
|
pub fn pump_to_block<F>(
|
|
287
286
|
block: BlockSlot,
|
|
288
287
|
pending: &mut PendingUnwind,
|
|
289
288
|
chunk_size: usize,
|
|
290
289
|
render: F,
|
|
291
|
-
) -> Result<(), sghtmltopdf_core::
|
|
290
|
+
) -> Result<(), sghtmltopdf_core::ConvertError>
|
|
292
291
|
where
|
|
293
|
-
F: FnOnce(ChannelSink) -> Result<(), sghtmltopdf_core::
|
|
292
|
+
F: FnOnce(ChannelSink) -> Result<(), sghtmltopdf_core::ConvertError> + Send + 'static,
|
|
294
293
|
{
|
|
295
|
-
use sghtmltopdf_core::
|
|
296
|
-
use sghtmltopdf_core::
|
|
294
|
+
use sghtmltopdf_core::ConvertError;
|
|
295
|
+
use sghtmltopdf_core::STACK_SIZE;
|
|
297
296
|
|
|
298
|
-
//
|
|
299
|
-
//
|
|
297
|
+
// Both are zero-capacity rendezvous channels. The rendering side waits for the block to
|
|
298
|
+
// finish after every chunk.
|
|
300
299
|
let (chunk_tx, chunk_rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(0);
|
|
301
300
|
let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel::<bool>(0);
|
|
302
301
|
|
|
@@ -304,17 +303,17 @@ where
|
|
|
304
303
|
.name("sghtmltopdf-render".to_string())
|
|
305
304
|
.stack_size(STACK_SIZE)
|
|
306
305
|
.spawn(move || render(ChannelSink::new(chunk_tx, ack_rx, chunk_size)))
|
|
307
|
-
.map_err(|e|
|
|
306
|
+
.map_err(|e| ConvertError::Input(format!("cannot create the rendering thread: {e}")))?;
|
|
308
307
|
|
|
309
308
|
while let Ok(chunk) = chunk_rx.recv() {
|
|
310
309
|
let ok = call_block(block, pending, chunk);
|
|
311
|
-
//
|
|
310
|
+
// Also break out when the acknowledgement cannot be sent (the rendering side already went away).
|
|
312
311
|
if ack_tx.send(ok).is_err() || !ok {
|
|
313
312
|
break;
|
|
314
313
|
}
|
|
315
314
|
}
|
|
316
|
-
//
|
|
317
|
-
//
|
|
315
|
+
// On an interruption, drop the receiving end first so the rendering side errors on its
|
|
316
|
+
// next send and can unwind.
|
|
318
317
|
drop(chunk_rx);
|
|
319
318
|
drop(ack_tx);
|
|
320
319
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//! Ruby
|
|
1
|
+
//! The Ruby exception classes, and the mapping from the core's [`ConvertError`].
|
|
2
2
|
|
|
3
3
|
use std::panic::AssertUnwindSafe;
|
|
4
4
|
|
|
5
5
|
use magnus::{prelude::*, ExceptionClass, RModule, Ruby};
|
|
6
|
-
use sghtmltopdf_core::
|
|
6
|
+
use sghtmltopdf_core::ConvertError;
|
|
7
7
|
|
|
8
8
|
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), magnus::Error> {
|
|
9
9
|
let base = module.define_error("Error", ruby.exception_standard_error())?;
|
|
@@ -15,60 +15,62 @@ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), magnus::Error> {
|
|
|
15
15
|
Ok(())
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/// Rust
|
|
18
|
+
/// Run `f`, converting a Rust panic into `Sghtmltopdf::InternalError`.
|
|
19
19
|
///
|
|
20
|
-
/// #
|
|
20
|
+
/// # Why we catch it ourselves
|
|
21
21
|
///
|
|
22
|
-
/// magnus
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
/// magnus
|
|
22
|
+
/// magnus also guards method calls against panics, so the process does not abort. But what
|
|
23
|
+
/// magnus converts to is Ruby's `fatal`, which not even `rescue Exception` catches and which
|
|
24
|
+
/// terminates the process. Losing a whole worker inside a web application over a bug in one
|
|
25
|
+
/// request is unacceptable, so it is converted to a descendant of `StandardError` here,
|
|
26
|
+
/// before it reaches magnus.
|
|
27
27
|
///
|
|
28
|
-
///
|
|
28
|
+
/// A panic means a bug in the core, so the message is kept rather than swallowed.
|
|
29
29
|
pub fn catch_panic<F, R>(ruby: &Ruby, f: F) -> Result<R, magnus::Error>
|
|
30
30
|
where
|
|
31
31
|
F: FnOnce() -> Result<R, magnus::Error>,
|
|
32
32
|
{
|
|
33
|
-
// AssertUnwindSafe:
|
|
34
|
-
// Rust
|
|
33
|
+
// AssertUnwindSafe: after unwinding from a panic, all that is touched is building the
|
|
34
|
+
// Ruby-side exception; no half-broken Rust state is read back.
|
|
35
35
|
match std::panic::catch_unwind(AssertUnwindSafe(f)) {
|
|
36
36
|
Ok(result) => result,
|
|
37
37
|
Err(payload) => Err(magnus::Error::new(
|
|
38
38
|
class(ruby, "InternalError"),
|
|
39
|
-
format!("
|
|
39
|
+
format!("internal error (panic): {}", panic_message(&payload)),
|
|
40
40
|
)),
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
///
|
|
44
|
+
/// Extract a human-readable message from a panic payload.
|
|
45
45
|
fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
|
|
46
46
|
if let Some(message) = payload.downcast_ref::<&'static str>() {
|
|
47
47
|
(*message).to_string()
|
|
48
48
|
} else if let Some(message) = payload.downcast_ref::<String>() {
|
|
49
49
|
message.clone()
|
|
50
50
|
} else {
|
|
51
|
-
"
|
|
51
|
+
"no details".to_string()
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
///
|
|
55
|
+
/// Convert a core error into the corresponding Ruby exception.
|
|
56
56
|
///
|
|
57
|
-
///
|
|
58
|
-
pub fn to_ruby(ruby: &Ruby, error:
|
|
57
|
+
/// The message is the wording the core returns, verbatim (the same wording as the CLI).
|
|
58
|
+
pub fn to_ruby(ruby: &Ruby, error: ConvertError) -> magnus::Error {
|
|
59
59
|
let (class_name, message) = match error {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
ConvertError::Usage(message) => ("UsageError", message),
|
|
61
|
+
ConvertError::Input(message) => ("InputError", message),
|
|
62
|
+
ConvertError::Render(message) => ("RenderError", message),
|
|
63
|
+
ConvertError::Timeout(message) => ("TimeoutError", message),
|
|
64
|
+
// A classification added in a later core release. Surface it rather than hide it.
|
|
65
|
+
other => ("Error", other.to_string()),
|
|
64
66
|
};
|
|
65
67
|
magnus::Error::new(class(ruby, class_name), message)
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
/// `Sghtmltopdf::<name
|
|
70
|
+
/// Look up the `Sghtmltopdf::<name>` exception class.
|
|
69
71
|
///
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
+
/// It is defined when the `.so` is loaded ([`define`]). On the off chance the lookup fails,
|
|
73
|
+
/// the error is raised as a `RuntimeError` rather than swallowed.
|
|
72
74
|
pub fn class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
73
75
|
ruby.class_object()
|
|
74
76
|
.const_get::<_, RModule>("Sghtmltopdf")
|