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