Branch data Line data Source code
1 : : /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
2 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
3 : : // SPDX-FileCopyrightText: 2008 litl, LLC
4 : :
5 : : #include <config.h> // for PACKAGE_STRING
6 : :
7 : : #include <locale.h> // for setlocale, LC_ALL
8 : : #include <stdint.h>
9 : : #include <stdlib.h> // for EXIT_SUCCESS / EXIT_FAILURE
10 : : #include <string.h> // for strcmp, strlen
11 : :
12 : : #ifdef HAVE_UNISTD_H
13 : : # include <unistd.h> // for close
14 : : #elif defined (_WIN32)
15 : : # include <io.h>
16 : : #endif
17 : :
18 : : #include <gio/gio.h>
19 : : #include <glib-object.h>
20 : : #include <glib.h>
21 : :
22 : : #include "gjs/auto.h"
23 : : #include "gjs/gerror-result.h"
24 : : #include "gjs/gjs.h"
25 : : #include "util/console.h"
26 : :
27 : : static Gjs::AutoStrv include_path;
28 : : static Gjs::AutoStrv coverage_prefixes;
29 : : static Gjs::AutoChar coverage_output_path;
30 : : static Gjs::AutoChar profile_output_path;
31 : : static Gjs::AutoChar command;
32 : : static gboolean print_version = false;
33 : : static gboolean print_js_version = false;
34 : : static gboolean debugging = false;
35 : : static gboolean exec_as_module = false;
36 : : static bool enable_profiler = false;
37 : :
38 : : static gboolean parse_profile_arg(const char *, const char *, void *, GError **);
39 : :
40 : : using GjsAutoGOptionContext =
41 : 128 : Gjs::AutoPointer<GOptionContext, GOptionContext, g_option_context_free>;
42 : :
43 : : // clang-format off
44 : : static GOptionEntry entries[] = {
45 : : { "version", 0, 0, G_OPTION_ARG_NONE, &print_version, "Print GJS version and exit" },
46 : : { "jsversion", 0, 0, G_OPTION_ARG_NONE, &print_js_version,
47 : : "Print version of the JS engine and exit" },
48 : : { "command", 'c', 0, G_OPTION_ARG_STRING, command.out(), "Program passed in as a string", "COMMAND" },
49 : : { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, coverage_prefixes.out(),
50 : : "Add the prefix PREFIX to the list of files to generate coverage info for", "PREFIX" },
51 : : { "coverage-output", 0, 0, G_OPTION_ARG_STRING, coverage_output_path.out(),
52 : : "Write coverage output to a directory DIR. This option is mandatory when using --coverage-prefix", "DIR", },
53 : : { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, include_path.out(),
54 : : "Add DIR to the list of paths to search for JS files", "DIR" },
55 : : { "module", 'm', 0, G_OPTION_ARG_NONE, &exec_as_module,
56 : : "Execute the file as a module" },
57 : : { "profile", 0, G_OPTION_FLAG_OPTIONAL_ARG | G_OPTION_FLAG_FILENAME,
58 : : G_OPTION_ARG_CALLBACK, reinterpret_cast<void *>(&parse_profile_arg),
59 : : "Enable the profiler and write output to FILE (default: gjs-$PID.syscap)",
60 : : "FILE" },
61 : : { "debugger", 'd', 0, G_OPTION_ARG_NONE, &debugging, "Start in debug mode" },
62 : : { NULL }
63 : : };
64 : : // clang-format on
65 : :
66 : 76 : [[nodiscard]] static Gjs::AutoStrv strndupv(int n, char* const* strv) {
67 : : #if GLIB_CHECK_VERSION(2, 68, 0)
68 : 76 : Gjs::AutoPointer<GStrvBuilder, GStrvBuilder, g_strv_builder_unref> builder{
69 : 76 : g_strv_builder_new()};
70 : :
71 [ + + ]: 292 : for (int i = 0; i < n; ++i)
72 : 216 : g_strv_builder_add(builder, strv[i]);
73 : :
74 : 76 : return g_strv_builder_end(builder);
75 : :
76 : : #else
77 : :
78 : : int ix;
79 : : if (n == 0)
80 : : return NULL;
81 : : char **retval = g_new(char *, n + 1);
82 : : for (ix = 0; ix < n; ix++)
83 : : retval[ix] = g_strdup(strv[ix]);
84 : : retval[n] = NULL;
85 : : return retval;
86 : : #endif // GLIB_CHECK_VERSION(2, 68, 0)
87 : 76 : }
88 : :
89 : 3 : [[nodiscard]] static Gjs::AutoStrv strcatv(char** strv1, char** strv2) {
90 [ + - - + ]: 3 : if (strv1 == NULL && strv2 == NULL)
91 : 0 : return NULL;
92 [ + - ]: 3 : if (strv1 == NULL)
93 : 3 : return g_strdupv(strv2);
94 [ # # ]: 0 : if (strv2 == NULL)
95 : 0 : return g_strdupv(strv1);
96 : :
97 : : #if GLIB_CHECK_VERSION(2, 70, 0)
98 : : Gjs::AutoPointer<GStrvBuilder, GStrvBuilder, g_strv_builder_unref> builder{
99 : 0 : g_strv_builder_new()};
100 : :
101 : 0 : g_strv_builder_addv(builder, const_cast<const char**>(strv1));
102 : 0 : g_strv_builder_addv(builder, const_cast<const char**>(strv2));
103 : :
104 : 0 : return g_strv_builder_end(builder);
105 : :
106 : : #else
107 : :
108 : : unsigned len1 = g_strv_length(strv1);
109 : : unsigned len2 = g_strv_length(strv2);
110 : : char **retval = g_new(char *, len1 + len2 + 1);
111 : : unsigned ix;
112 : :
113 : : for (ix = 0; ix < len1; ix++)
114 : : retval[ix] = g_strdup(strv1[ix]);
115 : : for (ix = 0; ix < len2; ix++)
116 : : retval[len1 + ix] = g_strdup(strv2[ix]);
117 : : retval[len1 + len2] = NULL;
118 : :
119 : : return retval;
120 : : #endif // GLIB_CHECK_VERSION(2, 70, 0)
121 : 0 : }
122 : :
123 : 6 : static gboolean parse_profile_arg(const char* option_name [[maybe_unused]],
124 : : const char* value, void*, GError**) {
125 : 6 : enable_profiler = true;
126 : 6 : profile_output_path = Gjs::AutoChar{value, Gjs::TakeOwnership{}};
127 : 6 : return true;
128 : : }
129 : :
130 : : static void
131 : 62 : check_script_args_for_stray_gjs_args(int argc,
132 : : char * const *argv)
133 : : {
134 : 62 : Gjs::AutoError error;
135 : 62 : Gjs::AutoStrv new_coverage_prefixes;
136 : 62 : Gjs::AutoChar new_coverage_output_path;
137 : 62 : Gjs::AutoStrv new_include_paths;
138 : : // Don't add new entries here. This is only for arguments that were
139 : : // previously accepted after the script name on the command line, for
140 : : // backwards compatibility.
141 : : // clang-format off
142 : 62 : GOptionEntry script_check_entries[] = {
143 : 62 : { "coverage-prefix", 'C', 0, G_OPTION_ARG_STRING_ARRAY, new_coverage_prefixes.out() },
144 : 62 : { "coverage-output", 0, 0, G_OPTION_ARG_STRING, new_coverage_output_path.out() },
145 : 62 : { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, new_include_paths.out() },
146 : : { NULL }
147 : 62 : };
148 : : // clang-format on
149 : :
150 : 62 : Gjs::AutoStrv argv_copy{g_new(char*, argc + 2)};
151 : : int ix;
152 : :
153 : 62 : argv_copy[0] = g_strdup("dummy"); /* Fake argv[0] for GOptionContext */
154 [ + + ]: 79 : for (ix = 0; ix < argc; ix++)
155 : 34 : argv_copy[ix + 1] = g_strdup(argv[ix]);
156 : 62 : argv_copy[argc + 1] = NULL;
157 : :
158 : 62 : GjsAutoGOptionContext script_options = g_option_context_new(NULL);
159 : 62 : g_option_context_set_ignore_unknown_options(script_options, true);
160 : 62 : g_option_context_set_help_enabled(script_options, false);
161 : 62 : g_option_context_add_main_entries(script_options, script_check_entries, NULL);
162 [ - + ]: 62 : if (!g_option_context_parse_strv(script_options, argv_copy.out(), &error)) {
163 : 0 : g_warning("Scanning script arguments failed: %s", error->message);
164 : 0 : return;
165 : : }
166 : :
167 [ + + ]: 62 : if (new_coverage_prefixes) {
168 : 2 : g_warning("You used the --coverage-prefix option after the script on "
169 : : "the GJS command line. Support for this will be removed in a "
170 : : "future version. Place the option before the script or use "
171 : : "the GJS_COVERAGE_PREFIXES environment variable.");
172 : 2 : coverage_prefixes = strcatv(coverage_prefixes, new_coverage_prefixes);
173 : : }
174 [ + + ]: 62 : if (new_include_paths) {
175 : 1 : g_warning("You used the --include-path option after the script on the "
176 : : "GJS command line. Support for this will be removed in a "
177 : : "future version. Place the option before the script or use "
178 : : "the GJS_PATH environment variable.");
179 : 1 : include_path = strcatv(include_path, new_include_paths);
180 : : }
181 [ + + ]: 62 : if (new_coverage_output_path) {
182 : 2 : g_warning(
183 : : "You used the --coverage-output option after the script on "
184 : : "the GJS command line. Support for this will be removed in a "
185 : : "future version. Place the option before the script or use "
186 : : "the GJS_COVERAGE_OUTPUT environment variable.");
187 : 2 : coverage_output_path = new_coverage_output_path;
188 : : }
189 [ + - + - : 62 : }
+ - + - +
- + - ]
190 : :
191 : 62 : int define_argv_and_eval_script(GjsContext* js_context, int argc,
192 : : char* const* argv, const char* script,
193 : : size_t len, const char* filename) {
194 : 62 : gjs_context_set_argv(js_context, argc, const_cast<const char**>(argv));
195 : :
196 : 62 : Gjs::AutoError error;
197 : : /* evaluate the script */
198 : 62 : int code = 0;
199 [ + + ]: 62 : if (exec_as_module) {
200 : 5 : Gjs::AutoUnref<GFile> output{g_file_new_for_commandline_arg(filename)};
201 : 5 : Gjs::AutoChar uri{g_file_get_uri(output)};
202 [ - + ]: 5 : if (!gjs_context_register_module(js_context, uri, uri, &error)) {
203 : 0 : g_critical("%s", error->message);
204 : 0 : code = 1;
205 : : }
206 : :
207 : 5 : uint8_t code_u8 = 0;
208 [ + - + - ]: 9 : if (!code &&
209 [ + - ]: 5 : !gjs_context_eval_module(js_context, uri, &code_u8, &error)) {
210 : 4 : code = code_u8;
211 : :
212 [ - + ]: 4 : if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT))
213 : 0 : g_critical("%s", error->message);
214 : : }
215 [ + + ]: 61 : } else if (!gjs_context_eval(js_context, script, len, filename, &code,
216 : : &error)) {
217 [ - + ]: 23 : if (!g_error_matches(error, GJS_ERROR, GJS_ERROR_SYSTEM_EXIT))
218 : 0 : g_critical("%s", error->message);
219 : : }
220 : 120 : return code;
221 : 60 : }
222 : :
223 : 76 : int main(int argc, char** argv) {
224 : 76 : Gjs::AutoError error;
225 : : const char *filename;
226 : : const char *program_name;
227 : : gsize len;
228 : 76 : int gjs_argc = argc, script_argc, ix;
229 : : char * const *script_argv;
230 : : const char *env_coverage_output_path;
231 : 76 : bool interactive_mode = false;
232 : :
233 : 76 : setlocale(LC_ALL, "");
234 : :
235 : 76 : GjsAutoGOptionContext context = g_option_context_new(NULL);
236 : 76 : g_option_context_set_ignore_unknown_options(context, true);
237 : 76 : g_option_context_set_help_enabled(context, false);
238 : :
239 : 76 : Gjs::AutoStrv argv_copy_addr{g_strdupv(argv)};
240 : 76 : char** argv_copy = argv_copy_addr;
241 : :
242 : 76 : g_option_context_add_main_entries(context, entries, NULL);
243 [ - + ]: 76 : if (!g_option_context_parse_strv(context, &argv_copy, &error))
244 : 0 : g_error("option parsing failed: %s", error->message);
245 : :
246 : : /* Split options so we pass unknown ones through to the JS script */
247 : 76 : int argc_copy = g_strv_length(argv_copy);
248 [ + + ]: 123 : for (ix = 1; ix < argc; ix++) {
249 : : /* Check if a file was given and split after it */
250 [ + + + + ]: 119 : if (argc_copy >= 2 && strcmp(argv[ix], argv_copy[1]) == 0) {
251 : : /* Filename given; split after this argument */
252 : 51 : gjs_argc = ix + 1;
253 : 51 : break;
254 : : }
255 : :
256 : : /* Check if -c or --command was given and split after following arg */
257 [ + + + + : 71 : if (command && (strcmp(argv[ix], "-c") == 0 ||
+ + ]
258 [ - + ]: 71 : strcmp(argv[ix], "--command") == 0)) {
259 : 21 : gjs_argc = ix + 2;
260 : 21 : break;
261 : : }
262 : : }
263 : 76 : Gjs::AutoStrv gjs_argv_addr{strndupv(gjs_argc, argv)};
264 : 76 : char** gjs_argv = gjs_argv_addr;
265 : 76 : script_argc = argc - gjs_argc;
266 : 76 : script_argv = argv + gjs_argc;
267 : :
268 : : /* Parse again, only the GJS options this time */
269 : 76 : include_path.release();
270 : 76 : coverage_prefixes.release();
271 : 76 : coverage_output_path.release();
272 : 76 : command.release();
273 : 76 : print_version = false;
274 : 76 : print_js_version = false;
275 : 76 : debugging = false;
276 : 76 : exec_as_module = false;
277 : 76 : g_option_context_set_ignore_unknown_options(context, false);
278 : 76 : g_option_context_set_help_enabled(context, true);
279 [ + + ]: 76 : if (!g_option_context_parse_strv(context, &gjs_argv, &error)) {
280 : : Gjs::AutoChar help_text{
281 : 2 : g_option_context_get_help(context, true, nullptr)};
282 : 2 : g_printerr("%s\n\n%s\n", error->message, help_text.get());
283 : 2 : return EXIT_FAILURE;
284 : 2 : }
285 : :
286 [ + + ]: 66 : if (print_version) {
287 : 2 : g_print("%s\n", PACKAGE_STRING);
288 : 2 : return EXIT_SUCCESS;
289 : : }
290 : :
291 [ + + ]: 64 : if (print_js_version) {
292 : 2 : g_print("%s\n", gjs_get_js_version());
293 : 2 : return EXIT_SUCCESS;
294 : : }
295 : :
296 : 62 : Gjs::AutoChar program_path;
297 : 62 : gjs_argc = g_strv_length(gjs_argv);
298 : 62 : Gjs::AutoChar script;
299 [ + + ]: 62 : if (command) {
300 : 21 : script = command;
301 : 21 : len = strlen(script);
302 : 21 : filename = "<command line>";
303 : 21 : program_name = gjs_argv[0];
304 [ - + ]: 41 : } else if (gjs_argc == 1) {
305 [ # # ]: 0 : if (exec_as_module) {
306 : 0 : g_warning(
307 : : "'-m' requires a file argument.\nExample: gjs -m main.js");
308 : 0 : return EXIT_FAILURE;
309 : : }
310 : :
311 : 0 : script = g_strdup("const Console = imports.console; Console.interact();");
312 : 0 : len = strlen(script);
313 : 0 : filename = "<stdin>";
314 : 0 : program_name = gjs_argv[0];
315 : 0 : interactive_mode = true;
316 : : } else {
317 : : /* All unprocessed options should be in script_argv */
318 : 41 : g_assert(gjs_argc == 2);
319 : : Gjs::AutoUnref<GFile> input{
320 : 41 : g_file_new_for_commandline_arg(gjs_argv[1])};
321 [ - + ]: 41 : if (!g_file_load_contents(input, nullptr, script.out(), &len, nullptr,
322 : : &error)) {
323 : 0 : g_printerr("%s\n", error->message);
324 : 0 : return EXIT_FAILURE;
325 : : }
326 : 41 : program_path = g_file_get_path(input);
327 : 41 : filename = gjs_argv[1];
328 : 41 : program_name = gjs_argv[1];
329 [ + - ]: 41 : }
330 : :
331 : : /* This should be removed after a suitable time has passed */
332 : 62 : check_script_args_for_stray_gjs_args(script_argc, script_argv);
333 : :
334 : : /* Check for GJS_TRACE_FD for sysprof profiling */
335 : 62 : const char* env_tracefd = g_getenv("GJS_TRACE_FD");
336 : 62 : int tracefd = -1;
337 [ - + ]: 62 : if (env_tracefd) {
338 : 0 : tracefd = g_ascii_strtoll(env_tracefd, nullptr, 10);
339 : 0 : g_setenv("GJS_TRACE_FD", "", true);
340 [ # # ]: 0 : if (tracefd > 0)
341 : 0 : enable_profiler = true;
342 : : }
343 : :
344 [ - + - - ]: 62 : if (interactive_mode && enable_profiler) {
345 : 0 : g_message("Profiler disabled in interactive mode.");
346 : 0 : enable_profiler = false;
347 : 0 : g_unsetenv("GJS_ENABLE_PROFILER"); /* ignore env var in eval() */
348 : 0 : g_unsetenv("GJS_TRACE_FD"); /* ignore env var in eval() */
349 : : }
350 : :
351 : 62 : const char* env_coverage_prefixes = g_getenv("GJS_COVERAGE_PREFIXES");
352 [ - + ]: 62 : if (env_coverage_prefixes)
353 : 0 : coverage_prefixes = g_strsplit(env_coverage_prefixes, ":", -1);
354 : :
355 [ + + ]: 62 : if (coverage_prefixes)
356 : 3 : gjs_coverage_enable();
357 : :
358 : : #ifdef HAVE_READLINE_READLINE_H
359 : 62 : Gjs::AutoChar repl_history_path = gjs_console_get_repl_history_path();
360 : : #else
361 : : Gjs::AutoChar repl_history_path = nullptr;
362 : : #endif
363 : :
364 : : Gjs::AutoUnref<GjsContext> js_context{GJS_CONTEXT(g_object_new(
365 : : GJS_TYPE_CONTEXT,
366 : : // clang-format off
367 : : "search-path", include_path.get(),
368 : : "program-name", program_name,
369 : : "program-path", program_path.get(),
370 : : "profiler-enabled", enable_profiler,
371 : : "exec-as-module", exec_as_module,
372 : : "repl-history-path", repl_history_path.get(),
373 : : // clang-format on
374 : 62 : nullptr))};
375 : :
376 : 62 : env_coverage_output_path = g_getenv("GJS_COVERAGE_OUTPUT");
377 [ - + ]: 62 : if (env_coverage_output_path != NULL) {
378 : 0 : g_free(coverage_output_path);
379 : 0 : coverage_output_path = g_strdup(env_coverage_output_path);
380 : : }
381 : :
382 : 62 : Gjs::AutoUnref<GjsCoverage> coverage;
383 [ + + ]: 62 : if (coverage_prefixes) {
384 [ - + ]: 3 : if (!coverage_output_path)
385 : 0 : g_error("--coverage-output is required when taking coverage statistics");
386 : :
387 : : Gjs::AutoUnref<GFile> output{
388 : 3 : g_file_new_for_commandline_arg(coverage_output_path)};
389 : 3 : coverage = gjs_coverage_new(coverage_prefixes, js_context, output);
390 : 3 : }
391 : :
392 [ + + + + : 62 : if (enable_profiler && profile_output_path) {
+ + ]
393 : 1 : GjsProfiler *profiler = gjs_context_get_profiler(js_context);
394 : 1 : gjs_profiler_set_filename(profiler, profile_output_path);
395 [ + + - + ]: 61 : } else if (enable_profiler && tracefd > -1) {
396 : 0 : GjsProfiler* profiler = gjs_context_get_profiler(js_context);
397 : 0 : gjs_profiler_set_fd(profiler, tracefd);
398 : 0 : tracefd = -1;
399 : : }
400 : :
401 [ - + ]: 62 : if (tracefd != -1) {
402 : 0 : close(tracefd);
403 : 0 : tracefd = -1;
404 : : }
405 : :
406 : : /* If we're debugging, set up the debugger. It will break on the first
407 : : * frame. */
408 [ + + ]: 62 : if (debugging)
409 : 25 : gjs_context_setup_debugger_console(js_context);
410 : :
411 : 62 : int code = define_argv_and_eval_script(js_context, script_argc, script_argv,
412 : : script, len, filename);
413 : :
414 : : /* Probably doesn't make sense to write statistics on failure */
415 [ + + + - : 60 : if (coverage && code == 0)
+ + ]
416 : 3 : gjs_coverage_write_statistics(coverage);
417 : :
418 [ + + ]: 60 : if (debugging)
419 : 25 : g_print("Program exited with code %d\n", code);
420 : :
421 : 60 : return code;
422 : 66 : }
|