Skip to content

Commit 0f67d94

Browse files
committed
src: use JSON configuration and blob content for SEA
1 parent f086555 commit 0f67d94

13 files changed

+272
-25
lines changed

doc/api/single-executable-applications.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,28 @@ tool, [postject][]:
5454
the following options:
5555

5656
* `hello` - The name of the copy of the `node` executable created in step 2.
57-
* `NODE_JS_CODE` - The name of the resource / note / section in the binary
57+
* `NODE_SEA_BLOB` - The name of the resource / note / section in the binary
5858
where the contents of the JavaScript file will be stored.
5959
* `hello.js` - The name of the JavaScript file created in step 1.
60-
* `--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
60+
* `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
6161
[fuse][] used by the Node.js project to detect if a file has been injected.
62-
* `--macho-segment-name NODE_JS` (only needed on macOS) - The name of the
62+
* `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the
6363
segment in the binary where the contents of the JavaScript file will be
6464
stored.
6565

6666
To summarize, here is the required command for each platform:
6767

6868
* On systems other than macOS:
6969
```console
70-
$ npx postject hello NODE_JS_CODE hello.js \
71-
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2
70+
$ npx postject hello NODE_SEA_BLOB hello.js \
71+
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
7272
```
7373

7474
* On macOS:
7575
```console
76-
$ npx postject hello NODE_JS_CODE hello.js \
77-
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
78-
--macho-segment-name NODE_JS
76+
$ npx postject hello NODE_SEA_BLOB hello.js \
77+
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
78+
--macho-segment-name NODE_SEA
7979
```
8080

8181
5. Sign the binary:
@@ -137,13 +137,13 @@ of [`process.execPath`][].
137137
A tool aiming to create a single executable Node.js application must
138138
inject the contents of a JavaScript file into:
139139

140-
* a resource named `NODE_JS_CODE` if the `node` binary is a [PE][] file
141-
* a section named `NODE_JS_CODE` in the `NODE_JS` segment if the `node` binary
140+
* a resource named `NODE_SEA_BLOB` if the `node` binary is a [PE][] file
141+
* a section named `NODE_SEA_BLOB` in the `NODE_SEA` segment if the `node` binary
142142
is a [Mach-O][] file
143-
* a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file
143+
* a note named `NODE_SEA_BLOB` if the `node` binary is an [ELF][] file
144144

145145
Search the binary for the
146-
`NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
146+
`NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
147147
last character to `1` to indicate that a resource has been injected.
148148

149149
### Platform support

doc/contributing/maintaining-single-executable-application-support.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ To disable single executable application support, build Node.js with the
6666
## Implementation
6767

6868
When built with single executable application support, the Node.js process uses
69-
[`postject-api.h`][] to check if the `NODE_JS_CODE` section exists in the
69+
[`postject-api.h`][] to check if the `NODE_SEA_BLOB` section exists in the
7070
binary. If it is found, it passes the buffer to
7171
[`single_executable_application.js`][], which executes the contents of the
7272
embedded script.

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
'src/js_stream.cc',
8383
'src/json_utils.cc',
8484
'src/js_udp_wrap.cc',
85+
'src/json_parser.h',
86+
'src/json_parser.cc',
8587
'src/module_wrap.cc',
8688
'src/node.cc',
8789
'src/node_api.cc',

src/json_parser.cc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include "json_parser.h"
2+
#include "node_errors.h"
3+
#include "node_v8_platform-inl.h"
4+
#include "util-inl.h"
5+
6+
namespace node {
7+
using v8::ArrayBuffer;
8+
using v8::Context;
9+
using v8::Isolate;
10+
using v8::Local;
11+
using v8::Object;
12+
using v8::String;
13+
using v8::Value;
14+
15+
static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) {
16+
Isolate* isolate = Isolate::Allocate();
17+
CHECK_NOT_NULL(isolate);
18+
per_process::v8_platform.Platform()->RegisterIsolate(isolate,
19+
uv_default_loop());
20+
Isolate::CreateParams params;
21+
params.array_buffer_allocator = allocator;
22+
Isolate::Initialize(isolate, params);
23+
return isolate;
24+
}
25+
26+
void JSONParser::FreeIsolate(Isolate* isolate) {
27+
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
28+
isolate->Dispose();
29+
}
30+
31+
JSONParser::JSONParser()
32+
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
33+
isolate_(NewIsolate(allocator_.get())),
34+
handle_scope_(isolate_.get()),
35+
context_(isolate_.get(), Context::New(isolate_.get())),
36+
context_scope_(context_.Get(isolate_.get())) {}
37+
38+
bool JSONParser::Parse(const std::string& content) {
39+
DCHECK(!parsed_);
40+
41+
Isolate* isolate = isolate_.get();
42+
Local<Context> context = context_.Get(isolate);
43+
44+
errors::PrinterTryCatch bootstrapCatch(isolate);
45+
Local<Value> json_string_value;
46+
Local<Value> result_value;
47+
if (!ToV8Value(context, content).ToLocal(&json_string_value) ||
48+
!json_string_value->IsString() ||
49+
!v8::JSON::Parse(context, json_string_value.As<String>())
50+
.ToLocal(&result_value) ||
51+
!result_value->IsObject()) {
52+
return false;
53+
}
54+
content_.Reset(isolate, result_value.As<Object>());
55+
parsed_ = true;
56+
return true;
57+
}
58+
59+
std::optional<std::string> JSONParser::GetTopLevelField(
60+
const std::string& field) {
61+
Isolate* isolate = isolate_.get();
62+
Local<Context> context = context_.Get(isolate);
63+
Local<Object> content_object = content_.Get(isolate);
64+
Local<Value> value;
65+
errors::PrinterTryCatch bootstrapCatch(isolate);
66+
if (!content_object
67+
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
68+
.ToLocal(&value) ||
69+
!value->IsString()) {
70+
return {};
71+
}
72+
Utf8Value utf8_value(isolate, value);
73+
return utf8_value.ToString();
74+
}
75+
76+
} // namespace node

src/json_parser.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#ifndef SRC_JSON_PARSER_H_
2+
#define SRC_JSON_PARSER_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include <memory>
7+
#include <optional>
8+
#include <string>
9+
#include "util.h"
10+
#include "v8.h"
11+
12+
namespace node {
13+
// This is intended to be used to get some top-level fields out of a JSON
14+
// without having to spin up a full Node.js environment that unnecessarily
15+
// complicates things.
16+
class JSONParser {
17+
public:
18+
JSONParser();
19+
~JSONParser() {}
20+
bool Parse(const std::string& content);
21+
std::optional<std::string> GetTopLevelField(const std::string& field);
22+
23+
private:
24+
// We might want a lighter-weight JSON parser for this use case. But for now
25+
// using V8 is good enough.
26+
static void FreeIsolate(v8::Isolate* isolate);
27+
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
28+
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
29+
v8::HandleScope handle_scope_;
30+
v8::Global<v8::Context> context_;
31+
v8::Context::Scope context_scope_;
32+
v8::Global<v8::Object> content_;
33+
bool parsed_ = false;
34+
};
35+
} // namespace node
36+
37+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
38+
39+
#endif // SRC_JSON_PARSER_H_

src/node.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,11 @@ static ExitCode StartInternal(int argc, char** argv) {
12281228

12291229
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
12301230

1231+
std::string sea_config = per_process::cli_options->experimental_sea_config;
1232+
if (!sea_config.empty()) {
1233+
return sea::BuildSingleExecutableBlob(sea_config);
1234+
}
1235+
12311236
// --build-snapshot indicates that we are in snapshot building mode.
12321237
if (per_process::cli_options->per_isolate->build_snapshot) {
12331238
if (result->args().size() < 2) {

src/node_errors.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,13 @@ void TriggerUncaughtException(Isolate* isolate, const v8::TryCatch& try_catch) {
12091209
false /* from_promise */);
12101210
}
12111211

1212+
PrinterTryCatch::~PrinterTryCatch() {
1213+
if (!HasCaught()) {
1214+
return;
1215+
}
1216+
PrintCaughtException(isolate_, isolate_->GetCurrentContext(), *this);
1217+
}
1218+
12121219
} // namespace errors
12131220

12141221
} // namespace node

src/node_errors.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,17 @@ void PerIsolateMessageListener(v8::Local<v8::Message> message,
275275

276276
void DecorateErrorStack(Environment* env,
277277
const errors::TryCatchScope& try_catch);
278+
279+
class PrinterTryCatch : public v8::TryCatch {
280+
public:
281+
explicit PrinterTryCatch(v8::Isolate* isolate)
282+
: v8::TryCatch(isolate), isolate_(isolate) {}
283+
~PrinterTryCatch();
284+
285+
private:
286+
v8::Isolate* isolate_;
287+
};
288+
278289
} // namespace errors
279290

280291
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
990990
kAllowedInEnvvar);
991991
Implies("--node-memory-debug", "--debug-arraybuffer-allocations");
992992
Implies("--node-memory-debug", "--verify-base-objects");
993+
994+
AddOption("--experimental-sea-config",
995+
"Generate a blob that can be embedded into the single executable "
996+
"application",
997+
&PerProcessOptions::experimental_sea_config);
993998
}
994999

9951000
inline std::string RemoveBrackets(const std::string& host) {

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ class PerProcessOptions : public Options {
265265
bool print_help = false;
266266
bool print_v8_help = false;
267267
bool print_version = false;
268+
std::string experimental_sea_config;
268269

269270
#ifdef NODE_HAVE_I18N_SUPPORT
270271
std::string icu_data_dir;

src/node_sea.cc

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "node_sea.h"
22

33
#include "env-inl.h"
4+
#include "json_parser.h"
45
#include "node_external_reference.h"
56
#include "node_internals.h"
67
#include "node_union_bytes.h"
@@ -10,7 +11,7 @@
1011
// used by the postject_has_resource() function to efficiently detect if a
1112
// resource has been injected. See
1213
// https://github.com/nodejs/postject/blob/35343439cac8c488f2596d7c4c1dddfec1fddcae/postject-api.h#L42-L45.
13-
#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
14+
#define POSTJECT_SENTINEL_FUSE "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
1415
#include "postject-api.h"
1516
#undef POSTJECT_SENTINEL_FUSE
1617

@@ -20,30 +21,46 @@
2021

2122
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
2223

24+
using node::ExitCode;
25+
using node::Utf8Value;
26+
using v8::ArrayBuffer;
2327
using v8::Context;
2428
using v8::FunctionCallbackInfo;
29+
using v8::Global;
30+
using v8::HandleScope;
31+
using v8::Isolate;
32+
using v8::Just;
2533
using v8::Local;
34+
using v8::Maybe;
35+
using v8::Nothing;
2636
using v8::Object;
37+
using v8::String;
38+
using v8::TryCatch;
2739
using v8::Value;
2840

2941
namespace node {
3042
namespace sea {
3143

44+
static const uint32_t kMagic = 0x143da20;
45+
3246
std::string_view FindSingleExecutableCode() {
3347
CHECK(IsSingleExecutable());
3448
static const std::string_view sea_code = []() -> std::string_view {
3549
size_t size;
3650
#ifdef __APPLE__
3751
postject_options options;
3852
postject_options_init(&options);
39-
options.macho_segment_name = "NODE_JS";
53+
options.macho_segment_name = "NODE_SEA";
4054
const char* code = static_cast<const char*>(
41-
postject_find_resource("NODE_JS_CODE", &size, &options));
55+
postject_find_resource("NODE_SEA_BLOB", &size, &options));
4256
#else
4357
const char* code = static_cast<const char*>(
44-
postject_find_resource("NODE_JS_CODE", &size, nullptr));
58+
postject_find_resource("NODE_SEA_BLOB", &size, nullptr));
4559
#endif
46-
return {code, size};
60+
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
61+
CHECK_EQ(first_word, kMagic);
62+
// TODO(joyeecheung): do more checks here e.g. matching the versions.
63+
return {code + sizeof(first_word), size - sizeof(first_word)};
4764
}();
4865
return sea_code;
4966
}
@@ -78,6 +95,71 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
7895
return {argc, argv};
7996
}
8097

98+
ExitCode BuildSingleExecutableBlob(const std::string& config_path) {
99+
std::string config;
100+
int r = ReadFileSync(&config, config_path.c_str());
101+
if (r != 0) {
102+
fprintf(stderr,
103+
"Cannot read single executable configuration from \"%s\n",
104+
config_path.c_str());
105+
return ExitCode::kGenericUserError;
106+
}
107+
108+
std::string main_path;
109+
std::string output_path;
110+
{
111+
JSONParser parser;
112+
if (!parser.Parse(config)) {
113+
fprintf(stderr, "Cannot parse JSON from \"%s\n", config_path.c_str());
114+
return ExitCode::kGenericUserError;
115+
}
116+
117+
main_path = parser.GetTopLevelField("main").value_or(std::string());
118+
if (main_path.empty()) {
119+
fprintf(stderr,
120+
"\"main\" field of %s is not a string\n",
121+
config_path.c_str());
122+
return ExitCode::kGenericUserError;
123+
}
124+
125+
output_path = parser.GetTopLevelField("output").value_or(std::string());
126+
if (output_path.empty()) {
127+
fprintf(stderr,
128+
"\"output\" field of %s is not a string\n",
129+
config_path.c_str());
130+
return ExitCode::kGenericUserError;
131+
}
132+
}
133+
134+
std::string main_script;
135+
// TODO(joyeecheung): unify the file utils.
136+
r = ReadFileSync(&main_script, main_path.c_str());
137+
if (r != 0) {
138+
fprintf(stderr, "Cannot read main script %s\n", main_path.c_str());
139+
return ExitCode::kGenericUserError;
140+
}
141+
142+
std::vector<char> sink;
143+
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
144+
sink.reserve(sizeof(kMagic) + main_script.size());
145+
const char* pos = reinterpret_cast<const char*>(&kMagic);
146+
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
147+
sink.insert(
148+
sink.end(), main_script.data(), main_script.data() + main_script.size());
149+
150+
uv_buf_t buf = uv_buf_init(sink.data(), sink.size());
151+
r = WriteFileSync(output_path.c_str(), buf);
152+
if (r != 0) {
153+
fprintf(stderr, "Cannot write output to %s\n", output_path.c_str());
154+
return ExitCode::kGenericUserError;
155+
}
156+
157+
fprintf(stderr,
158+
"Wrote single executable preparation blob to %s\n",
159+
output_path.c_str());
160+
return ExitCode::kNoFailure;
161+
}
162+
81163
void Initialize(Local<Object> target,
82164
Local<Value> unused,
83165
Local<Context> context,

0 commit comments

Comments
 (0)