Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 87 additions & 9 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
}
}

if (static_cast<bool>(sea.flags & SeaFlags::kIncludeUserArgv)) {
Debug("Write SEA resource user argv size %zu\n", sea.user_argv.size());
written_total += WriteArithmetic<size_t>(sea.user_argv.size());
for (const auto& arg : sea.user_argv) {
Debug("Write SEA resource user arg %s at %p, size=%zu\n",
arg.data(),
arg.data(),
arg.size());
written_total += WriteStringView(arg, StringLogMode::kAddressAndContent);
}
}
return written_total;
}

Expand Down Expand Up @@ -224,13 +236,29 @@ SeaResource SeaDeserializer::Read() {
exec_argv.emplace_back(arg);
}
}

std::vector<std::string_view> user_argv;
if (static_cast<bool>(flags & SeaFlags::kIncludeUserArgv)) {
size_t user_argv_size = ReadArithmetic<size_t>();
Debug("Read SEA resource user args size %zu\n", user_argv_size);
user_argv.reserve(user_argv_size);
for (size_t i = 0; i < user_argv_size; ++i) {
std::string_view arg = ReadStringView(StringLogMode::kAddressAndContent);
Debug("Read SEA resource user arg %s at %p, size=%zu\n",
arg.data(),
arg.data(),
arg.size());
user_argv.emplace_back(arg);
}
}
return {flags,
exec_argv_extension,
code_path,
code,
code_cache,
assets,
exec_argv};
exec_argv,
user_argv};
}

std::string_view FindSingleExecutableBlob() {
Expand Down Expand Up @@ -316,12 +344,14 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
static std::vector<char*> new_argv;
static std::vector<std::string> exec_argv_storage;
static std::vector<std::string> cli_extension_args;
static std::vector<std::string> user_argv_storage;

SeaResource sea_resource = FindSingleExecutableResource();

new_argv.clear();
exec_argv_storage.clear();
cli_extension_args.clear();
user_argv_storage.clear();

// Handle CLI extension mode for --node-options
if (sea_resource.exec_argv_extension == SeaExecArgvExtension::kCli) {
Expand All @@ -341,10 +371,11 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
}
}

// Reserve space for argv[0], exec argv, cli extension args, original argv,
// and nullptr
// Reserve space for two argv[0] , exec argv, cli extension args, user argv,
// original argv and nullptr
new_argv.reserve(argc + sea_resource.exec_argv.size() +
cli_extension_args.size() + 2);
cli_extension_args.size() + sea_resource.user_argv.size() +
3);
new_argv.emplace_back(argv[0]);

// Insert exec argv from SEA config
Expand All @@ -363,8 +394,18 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
new_argv.emplace_back(exec_argv_storage.back().data());
}

// Add actual run time arguments
new_argv.insert(new_argv.end(), argv, argv + argc);
new_argv.emplace_back(argv[0]);
// Insert user argv from SEA config
if (!sea_resource.user_argv.empty()) {
user_argv_storage.reserve(sea_resource.user_argv.size());
for (const auto& arg : sea_resource.user_argv) {
user_argv_storage.emplace_back(arg);
new_argv.emplace_back(user_argv_storage.back().data());
}
}

// Add actual run time arguments.
new_argv.insert(new_argv.end(), argv + 1, argv + argc);
new_argv.emplace_back(nullptr);
argc = new_argv.size() - 1;
argv = new_argv.data();
Expand All @@ -382,6 +423,7 @@ struct SeaConfig {
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
std::unordered_map<std::string, std::string> assets;
std::vector<std::string> exec_argv;
std::vector<std::string> user_argv;
};

std::optional<SeaConfig> ParseSingleExecutableConfig(
Expand Down Expand Up @@ -544,6 +586,35 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
config_path);
return std::nullopt;
}
} else if (key == "argv") {
simdjson::ondemand::array argv_array;
FPrintF(stderr, "\"argv\" start \n");

if (field.value().get_array().get(argv_array)) {
FPrintF(stderr,
"\"argv\" field of %s is not an array of strings\n",
config_path);
return std::nullopt;
}
std::vector<std::string> user_argv;

for (auto argv : argv_array) {
FPrintF(stderr, "\"argv\"\n");

std::string_view argv_str;
if (argv.get_string().get(argv_str)) {
FPrintF(stderr,
"\"argv\" field of %s is not an array of strings\n",
config_path);
return std::nullopt;
}
FPrintF(stderr, "\"argv\" : %s \n", argv_str);
user_argv.emplace_back(argv_str);
}
if (!user_argv.empty()) {
result.flags |= SeaFlags::kIncludeUserArgv;
result.user_argv = std::move(user_argv);
}
}
}

Expand Down Expand Up @@ -579,9 +650,11 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
const SnapshotConfig& snapshot_config,
std::vector<char>* snapshot_blob) {
SnapshotData snapshot;
// TODO(joyeecheung): make the arguments configurable through the JSON
// config or a programmatic API.
std::vector<std::string> patched_args = {args[0], config.main_path};
if (!config.user_argv.empty()) {
patched_args.insert(
patched_args.end(), config.user_argv.begin(), config.user_argv.end());
}
ExitCode exit_code = SnapshotBuilder::Generate(&snapshot,
patched_args,
exec_args,
Expand Down Expand Up @@ -741,6 +814,10 @@ ExitCode GenerateSingleExecutableBlob(
for (const auto& arg : config.exec_argv) {
exec_argv_view.emplace_back(arg);
}
std::vector<std::string_view> user_argv_view;
for (const auto& arg : config.user_argv) {
user_argv_view.emplace_back(arg);
}
SeaResource sea{
config.flags,
config.exec_argv_extension,
Expand All @@ -750,7 +827,8 @@ ExitCode GenerateSingleExecutableBlob(
: std::string_view{main_script.data(), main_script.size()},
optional_sv_code_cache,
assets_view,
exec_argv_view};
exec_argv_view,
user_argv_view};

SeaSerializer serializer;
serializer.Write(sea);
Expand Down
2 changes: 2 additions & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum class SeaFlags : uint32_t {
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
kIncludeExecArgv = 1 << 4,
kIncludeUserArgv = 1 << 5,
};

enum class SeaExecArgvExtension : uint8_t {
Expand All @@ -45,6 +46,7 @@ struct SeaResource {
std::optional<std::string_view> code_cache;
std::unordered_map<std::string_view, std::string_view> assets;
std::vector<std::string_view> exec_argv;
std::vector<std::string_view> user_argv;

bool use_snapshot() const;
bool use_code_cache() const;
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/sea-argv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const assert = require('assert');

process.emitWarning('This warning should not be shown in the output', 'TestWarning');

console.log('process.argv:', JSON.stringify(process.argv));

assert.deepStrictEqual(process.argv.slice(2), [
'user-arg1',
'user-arg2',
'user-arg3'
]);

console.log('multiple argv test passed');
65 changes: 65 additions & 0 deletions test/sequential/test-single-executable-application-argv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

require('../common');

const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');

skipIfSingleExecutableIsNotSupported();

// This tests the argv functionality with multiple arguments in single executable applications.

const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process');
const { join } = require('path');
const assert = require('assert');

const configFile = tmpdir.resolve('sea-config.json');
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');

tmpdir.refresh();

// Copy test fixture to working directory
copyFileSync(fixtures.path('sea-argv.js'), tmpdir.resolve('sea.js'));

writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true,
"argv": ["user-arg1", "user-arg2"]
}
`);

spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{ cwd: tmpdir.path });

assert(existsSync(seaPrepBlob));

generateSEA(outputFile, process.execPath, seaPrepBlob);

// Test that multiple argv are properly applied
spawnSyncAndAssert(
outputFile,
[
'user-arg3',
],
{
env: {
...process.env,
NODE_NO_WARNINGS: '0',
COMMON_DIRECTORY: join(__dirname, '..', 'common'),
NODE_DEBUG_NATIVE: 'SEA',
}
},
{
stdout: /multiple argv test passed/,
trim: true,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

require('../common');

const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');

skipIfSingleExecutableIsNotSupported();

// This tests the snapshot support in single executable applications.

const tmpdir = require('../common/tmpdir');
const { writeFileSync, existsSync } = require('fs');
const {
spawnSyncAndAssert,
spawnSyncAndExit,
} = require('../common/child_process');
const assert = require('assert');

const configFile = tmpdir.resolve('sea-config.json');
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');

{
tmpdir.refresh();

writeFileSync(tmpdir.resolve('snapshot.js'), '', 'utf-8');
writeFileSync(configFile, `
{
"main": "snapshot.js",
"output": "sea-prep.blob",
"useSnapshot": true
}
`);

spawnSyncAndExit(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
cwd: tmpdir.path
},
{
status: 1,
signal: null,
stderr: /snapshot\.js does not invoke v8\.startupSnapshot\.setDeserializeMainFunction\(\)/
});
}

{
tmpdir.refresh();
const code = `
const {
setDeserializeMainFunction,
} = require('v8').startupSnapshot;

setDeserializeMainFunction(() => {
console.log('Hello from snapshot with config argv:', process.argv[2]);
});
`;

writeFileSync(tmpdir.resolve('snapshot.js'), code, 'utf-8');
writeFileSync(configFile, `
{
"main": "snapshot.js",
"output": "sea-prep.blob",
"useSnapshot": true,
"argv": ["user-arg"]
}
`);

spawnSyncAndAssert(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
cwd: tmpdir.path,
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
stderr: /Single executable application is an experimental feature/
});

assert(existsSync(seaPrepBlob));

generateSEA(outputFile, process.execPath, seaPrepBlob);

spawnSyncAndAssert(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA,MKSNAPSHOT',
...process.env,
}
},
{
trim: true,
stdout: 'Hello from snapshot with config argv: user-arg',
stderr(output) {
assert.doesNotMatch(
output,
/Single executable application is an experimental feature/);
}
}
);
}
Loading