#' @useDynLib processx, .registration = TRUE, .fixes = "c_" NULL ## Workaround an R CMD check false positive dummy_r6 <- function() R6::R6Class #' External process #' #' @description #' Managing external processes from R is not trivial, and this #' class aims to help with this deficiency. It is essentially a small #' wrapper around the `system` base R function, to return the process #' id of the started process, and set its standard output and error #' streams. The process id is then used to manage the process. #' #' @param n Number of characters or lines to read. #' @param grace Currently not used. #' @param close_connections Whether to close standard input, standard #' output, standard error connections and the poll connection, after #' killing the process. #' @param timeout Timeout in milliseconds, for the wait or the I/O #' polling. #' #' @section Batch files: #' Running Windows batch files (`.bat` or `.cmd` files) may be complicated #' because of the `cmd.exe` command line parsing rules. For example you #' cannot easily have whitespace in both the command (path) and one of the #' arguments. To work around these limitations you need to start a #' `cmd.exe` shell explicitly and use its `call` command. For example: #' #' ```r #' process$new("cmd.exe", c("/c", "call", bat_file, "arg 1", "arg 2")) #' ``` #' #' This works even if `bat_file` contains whitespace characters. #' #' @section Polling: #' The `poll_io()` function polls the standard output and standard #' error connections of a process, with a timeout. If there is output #' in either of them, or they are closed (e.g. because the process exits) #' `poll_io()` returns immediately. #' #' In addition to polling a single process, the [poll()] function #' can poll the output of several processes, and returns as soon as any #' of them has generated output (or exited). #' #' @section Cleaning up background processes: #' processx kills processes that are not referenced any more (if `cleanup` #' is set to `TRUE`), or the whole subprocess tree (if `cleanup_tree` is #' also set to `TRUE`). #' #' The cleanup happens when the references of the processes object are #' garbage collected. To clean up earlier, you can call the `kill()` or #' `kill_tree()` method of the process(es), from an `on.exit()` expression, #' or an error handler: #' ```r #' process_manager <- function() { #' on.exit({ #' try(p1$kill(), silent = TRUE) #' try(p2$kill(), silent = TRUE) #' }, add = TRUE) #' p1 <- process$new("sleep", "3") #' p2 <- process$new("sleep", "10") #' p1$wait() #' p2$wait() #' } #' process_manager() #' ``` #' #' If you interrupt `process_manager()` or an error happens then both `p1` #' and `p2` are cleaned up immediately. Their connections will also be #' closed. The same happens at a regular exit. #' #' @export #' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true") #' p <- process$new("sleep", "2") #' p$is_alive() #' p #' p$kill() #' p$is_alive() #' #' p <- process$new("sleep", "1") #' p$is_alive() #' Sys.sleep(2) #' p$is_alive() process <- R6::R6Class( "process", cloneable = FALSE, public = list( #' @description #' Start a new process in the background, and then return immediately. #' #' @return R6 object representing the process. #' @param command Character scalar, the command to run. #' Note that this argument is not passed to a shell, so no #' tilde-expansion or variable substitution is performed on it. #' It should not be quoted with [base::shQuote()]. See #' [base::normalizePath()] for tilde-expansion. If you want to run #' `.bat` or `.cmd` files on Windows, make sure you read the #' 'Batch files' section above. #' @param args Character vector, arguments to the command. They will be #' passed to the process as is, without a shell transforming them, #' They don't need to be escaped. #' @param stdin What to do with the standard input. Possible values: #' * `NULL`: set to the _null device_, i.e. no standard input is #' provided; #' * a file name, use this file as standard input; #' * `"|"`: create a (writeable) connection for stdin. #' * `""` (empty string): inherit it from the main R process. If the #' main R process does not have a standard input stream, e.g. in #' RGui on Windows, then an error is thrown. #' @param stdout What to do with the standard output. Possible values: #' * `NULL`: discard it; #' * a string, redirect it to this file; #' * `"|"`: create a connection for it. #' * `""` (empty string): inherit it from the main R process. If the #' main R process does not have a standard output stream, e.g. in #' RGui on Windows, then an error is thrown. #' @param stderr What to do with the standard error. Possible values: #' * `NULL`: discard it; #' * a string, redirect it to this file; #' * `"|"`: create a connection for it; #' * `"2>&1"`: redirect it to the same connection (i.e. pipe or file) #' as `stdout`. `"2>&1"` is a way to keep standard output and error #' correctly interleaved. #' * `""` (empty string): inherit it from the main R process. If the #' main R process does not have a standard error stream, e.g. in #' RGui on Windows, then an error is thrown. #' @param pty Whether to create a pseudo terminal (pty) for the #' background process. This is currently only supported on Unix #' systems, but not supported on Solaris. #' If it is `TRUE`, then the `stdin`, `stdout` and `stderr` arguments #' must be `NULL`. If a pseudo terminal is created, then processx #' will create pipes for standard input and standard output. There is #' no separate pipe for standard error, because there is no way to #' distinguish between stdout and stderr on a pty. Note that the #' standard output connection of the pty is _blocking_, so we always #' poll the standard output connection before reading from it using #' the `$read_output()` method. Also, because `$read_output_lines()` #' could still block if no complete line is available, this function #' always fails if the process has a pty. Use `$read_output()` to #' read from ptys. #' @param pty_options Unix pseudo terminal options, a named list. see #' [default_pty_options()] for details and defaults. #' @param connections A list of processx connections to pass to the #' child process. This is an experimental feature currently. #' @param poll_connection Whether to create an extra connection to the #' process that allows polling, even if the standard input and #' standard output are not pipes. If this is `NULL` (the default), #' then this connection will be only created if standard output and #' standard error are not pipes, and `connections` is an empty list. #' If the poll connection is created, you can query it via #' `p$get_poll_connection()` and it is also included in the response #' to `p$poll_io()` and [poll()]. The numeric file descriptor of the #' poll connection comes right after `stderr` (2), and the #' connections listed in `connections`. #' @param env Environment variables of the child process. If `NULL`, #' the parent's environment is inherited. On Windows, many programs #' cannot function correctly if some environment variables are not #' set, so we always set `HOMEDRIVE`, `HOMEPATH`, `LOGONSERVER`, #' `PATH`, `SYSTEMDRIVE`, `SYSTEMROOT`, `TEMP`, `USERDOMAIN`, #' `USERNAME`, `USERPROFILE` and `WINDIR`. To append new environment #' variables to the ones set in the current process, specify #' `"current"` in `env`, without a name, and the appended ones with #' names. The appended ones can overwrite the current ones. #' @param cleanup Whether to kill the process when the `process` #' object is garbage collected. #' @param cleanup_tree Whether to kill the process and its child #' process tree when the `process` object is garbage collected. #' @param wd Working directory of the process. It must exist. #' If `NULL`, then the current working directory is used. #' @param echo_cmd Whether to print the command to the screen before #' running it. #' @param supervise Whether to register the process with a supervisor. #' If `TRUE`, the supervisor will ensure that the process is #' killed when the R process exits. #' @param windows_verbatim_args Whether to omit quoting the arguments #' on Windows. It is ignored on other platforms. #' @param windows_hide_window Whether to hide the application's window #' on Windows. It is ignored on other platforms. #' @param windows_detached_process Whether to use the #' `DETACHED_PROCESS` flag on Windows. If this is `TRUE`, then #' the child process will have no attached console, even if the #' parent had one. #' @param encoding The encoding to assume for `stdin`, `stdout` and #' `stderr`. By default the encoding of the current locale is #' used. Note that `processx` always reencodes the output of the #' `stdout` and `stderr` streams in UTF-8 currently. #' If you want to read them without any conversion, on all platforms, #' specify `"UTF-8"` as encoding. #' @param post_process An optional function to run when the process has #' finished. Currently it only runs if `$get_result()` is called. #' It is only run once. initialize = function(command = NULL, args = character(), stdin = NULL, stdout = NULL, stderr = NULL, pty = FALSE, pty_options = list(), connections = list(), poll_connection = NULL, env = NULL, cleanup = TRUE, cleanup_tree = FALSE, wd = NULL, echo_cmd = FALSE, supervise = FALSE, windows_verbatim_args = FALSE, windows_hide_window = FALSE, windows_detached_process = !cleanup, encoding = "", post_process = NULL) process_initialize(self, private, command, args, stdin, stdout, stderr, pty, pty_options, connections, poll_connection, env, cleanup, cleanup_tree, wd, echo_cmd, supervise, windows_verbatim_args, windows_hide_window, windows_detached_process, encoding, post_process), #' @description #' Cleanup method that is called when the `process` object is garbage #' collected. If requested so in the process constructor, then it #' eliminates all processes in the process's subprocess tree. finalize = function() { if (!is.null(private$tree_id) && private$cleanup_tree && ps::ps_is_supported()) self$kill_tree() }, #' @description #' Terminate the process. It also terminate all of its child #' processes, except if they have created a new process group (on Unix), #' or job object (on Windows). It returns `TRUE` if the process #' was terminated, and `FALSE` if it was not (because it was #' already finished/dead when `processx` tried to terminate it). kill = function(grace = 0.1, close_connections = TRUE) process_kill(self, private, grace, close_connections), #' @description #' Process tree cleanup. It terminates the process #' (if still alive), together with any child (or grandchild, etc.) #' processes. It uses the _ps_ package, so that needs to be installed, #' and _ps_ needs to support the current platform as well. Process tree #' cleanup works by marking the process with an environment variable, #' which is inherited in all child processes. This allows finding #' descendents, even if they are orphaned, i.e. they are not connected #' to the root of the tree cleanup in the process tree any more. #' `$kill_tree()` returns a named integer vector of the process ids that #' were killed, the names are the names of the processes (e.g. `"sleep"`, #' `"notepad.exe"`, `"Rterm.exe"`, etc.). kill_tree = function(grace = 0.1, close_connections = TRUE) process_kill_tree(self, private, grace, close_connections), #' @description #' Send a signal to the process. On Windows only the #' `SIGINT`, `SIGTERM` and `SIGKILL` signals are interpreted, #' and the special 0 signal. The first three all kill the process. The 0 #' signal returns `TRUE` if the process is alive, and `FALSE` #' otherwise. On Unix all signals are supported that the OS supports, #' and the 0 signal as well. #' @param signal An integer scalar, the id of the signal to send to #' the process. See [tools::pskill()] for the list of signals. signal = function(signal) process_signal(self, private, signal), #' @description #' Send an interrupt to the process. On Unix this is a #' `SIGINT` signal, and it is usually equivalent to pressing CTRL+C at #' the terminal prompt. On Windows, it is a CTRL+BREAK keypress. #' Applications may catch these events. By default they will quit. interrupt = function() process_interrupt(self, private), #' @description #' Query the process id. #' @return Integer scalar, the process id of the process. get_pid = function() process_get_pid(self, private), #' @description Check if the process is alive. #' @return Logical scalar. is_alive = function() process_is_alive(self, private), #' @description #' Wait until the process finishes, or a timeout happens. #' Note that if the process never finishes, and the timeout is infinite #' (the default), then R will never regain control. In some rare cases, #' `$wait()` might take a bit longer than specified to time out. This #' happens on Unix, when another package overwrites the processx #' `SIGCHLD` signal handler, after the processx process has started. #' One such package is parallel, if used with fork clusters, e.g. #' through `parallel::mcparallel()`. #' @return It returns the process itself, invisibly. wait = function(timeout = -1) process_wait(self, private, timeout), #' @description #' `$get_exit_status` returns the exit code of the process if it has #' finished and `NULL` otherwise. On Unix, in some rare cases, the exit #' status might be `NA`. This happens if another package (or R itself) #' overwrites the processx `SIGCHLD` handler, after the processx process #' has started. In these cases processx cannot determine the real exit #' status of the process. One such package is parallel, if used with #' fork clusters, e.g. through the `parallel::mcparallel()` function. get_exit_status = function() process_get_exit_status(self, private), #' @description #' `format(p)` or `p$format()` creates a string representation of the #' process, usually for printing. format = function() process_format(self, private), #' @description #' `print(p)` or `p$print()` shows some information about the #' process on the screen, whether it is running and it's process id, etc. print = function() process_print(self, private), #' @description #' `$get_start_time()` returns the time when the process was #' started. get_start_time = function() process_get_start_time(self, private), #' @description #' `$is_supervised()` returns whether the process is being tracked by #' supervisor process. is_supervised = function() process_is_supervised(self, private), #' @description #' `$supervise()` if passed `TRUE`, tells the supervisor to start #' tracking the process. If `FALSE`, tells the supervisor to stop #' tracking the process. Note that even if the supervisor is disabled #' for a process, if it was started with `cleanup = TRUE`, the process #' will still be killed when the object is garbage collected. #' @param status Whether to turn on of off the supervisor for this #' process. supervise = function(status) process_supervise(self, private, status), ## Output #' @description #' `$read_output()` reads from the standard output connection of the #' process. If the standard output connection was not requested, then #' then it returns an error. It uses a non-blocking text connection. This #' will work only if `stdout="|"` was used. Otherwise, it will throw an #' error. read_output = function(n = -1) process_read_output(self, private, n), #' @description #' `$read_error()` is similar to `$read_output`, but it reads #' from the standard error stream. read_error = function(n = -1) process_read_error(self, private, n), #' @description #' `$read_output_lines()` reads lines from standard output connection #' of the process. If the standard output connection was not requested, #' then it returns an error. It uses a non-blocking text connection. #' This will work only if `stdout="|"` was used. Otherwise, it will #' throw an error. read_output_lines = function(n = -1) process_read_output_lines(self, private, n), #' @description #' `$read_error_lines()` is similar to `$read_output_lines`, but #' it reads from the standard error stream. read_error_lines = function(n = -1) process_read_error_lines(self, private, n), #' @description #' `$is_incomplete_output()` return `FALSE` if the other end of #' the standard output connection was closed (most probably because the #' process exited). It return `TRUE` otherwise. is_incomplete_output = function() process_is_incompelete_output(self, private), #' @description #' `$is_incomplete_error()` return `FALSE` if the other end of #' the standard error connection was closed (most probably because the #' process exited). It return `TRUE` otherwise. is_incomplete_error = function() process_is_incompelete_error(self, private), #' @description #' `$has_input_connection()` return `TRUE` if there is a connection #' object for standard input; in other words, if `stdout="|"`. It returns #' `FALSE` otherwise. has_input_connection = function() process_has_input_connection(self, private), #' @description #' `$has_output_connection()` returns `TRUE` if there is a connection #' object for standard output; in other words, if `stdout="|"`. It returns #' `FALSE` otherwise. has_output_connection = function() process_has_output_connection(self, private), #' @description #' `$has_error_connection()` returns `TRUE` if there is a connection #' object for standard error; in other words, if `stderr="|"`. It returns #' `FALSE` otherwise. has_error_connection = function() process_has_error_connection(self, private), #' @description #' `$has_poll_connection()` return `TRUE` if there is a poll connection, #' `FALSE` otherwise. has_poll_connection = function() process_has_poll_connection(self, private), #' @description #' `$get_input_connection()` returns a connection object, to the #' standard input stream of the process. get_input_connection = function() process_get_input_connection(self, private), #' @description #' `$get_output_connection()` returns a connection object, to the #' standard output stream of the process. get_output_connection = function() process_get_output_connection(self, private), #' @description #' `$get_error_conneciton()` returns a connection object, to the #' standard error stream of the process. get_error_connection = function() process_get_error_connection(self, private), #' @description #' `$read_all_output()` waits for all standard output from the process. #' It does not return until the process has finished. #' Note that this process involves waiting for the process to finish, #' polling for I/O and potentially several `readLines()` calls. #' It returns a character scalar. This will return content only if #' `stdout="|"` was used. Otherwise, it will throw an error. read_all_output = function() process_read_all_output(self, private), #' @description #' `$read_all_error()` waits for all standard error from the process. #' It does not return until the process has finished. #' Note that this process involves waiting for the process to finish, #' polling for I/O and potentially several `readLines()` calls. #' It returns a character scalar. This will return content only if #' `stderr="|"` was used. Otherwise, it will throw an error. read_all_error = function() process_read_all_error(self, private), #' @description #' `$read_all_output_lines()` waits for all standard output lines #' from a process. It does not return until the process has finished. #' Note that this process involves waiting for the process to finish, #' polling for I/O and potentially several `readLines()` calls. #' It returns a character vector. This will return content only if #' `stdout="|"` was used. Otherwise, it will throw an error. read_all_output_lines = function() process_read_all_output_lines(self, private), #' @description #' `$read_all_error_lines()` waits for all standard error lines from #' a process. It does not return until the process has finished. #' Note that this process involves waiting for the process to finish, #' polling for I/O and potentially several `readLines()` calls. #' It returns a character vector. This will return content only if #' `stderr="|"` was used. Otherwise, it will throw an error. read_all_error_lines = function() process_read_all_error_lines(self, private), #' @description #' `$write_input()` writes the character vector (separated by `sep`) to #' the standard input of the process. It will be converted to the specified #' encoding. This operation is non-blocking, and it will return, even if #' the write fails (because the write buffer is full), or if it suceeds #' partially (i.e. not the full string is written). It returns with a raw #' vector, that contains the bytes that were not written. You can supply #' this raw vector to `$write_input()` again, until it is fully written, #' and then the return value will be `raw(0)` (invisibly). #' #' @param str Character or raw vector to write to the standard input #' of the process. If a character vector with a marked encoding, #' it will be converted to `encoding`. #' @param sep Separator to add between `str` elements if it is a #' character vector. It is ignored if `str` is a raw vector. #' @return Leftover text (as a raw vector), that was not written. write_input = function(str, sep = "\n") process_write_input(self, private, str, sep), #' @description #' `$get_input_file()` if the `stdin` argument was a filename, #' this returns the absolute path to the file. If `stdin` was `"|"` or #' `NULL`, this simply returns that value. get_input_file = function() process_get_input_file(self, private), #' @description #' `$get_output_file()` if the `stdout` argument was a filename, #' this returns the absolute path to the file. If `stdout` was `"|"` or #' `NULL`, this simply returns that value. get_output_file = function() process_get_output_file(self, private), #' @description #' `$get_error_file()` if the `stderr` argument was a filename, #' this returns the absolute path to the file. If `stderr` was `"|"` or #' `NULL`, this simply returns that value. get_error_file = function() process_get_error_file(self, private), #' @description #' `$poll_io()` polls the process's connections for I/O. See more in #' the _Polling_ section, and see also the [poll()] function #' to poll on multiple processes. poll_io = function(timeout) process_poll_io(self, private, timeout), #' @description #' `$get_poll_connetion()` returns the poll connection, if the process has #' one. get_poll_connection = function() process_get_poll_connection(self, private), #' @description #' `$get_result()` returns the result of the post processesing function. #' It can only be called once the process has finished. If the process has #' no post-processing function, then `NULL` is returned. get_result = function() process_get_result(self, private), #' @description #' `$as_ps_handle()` returns a [ps::ps_handle] object, corresponding to #' the process. as_ps_handle = function() process_as_ps_handle(self, private), #' @description #' Calls [ps::ps_name()] to get the process name. get_name = function() ps_method(ps::ps_name, self), #' @description #' Calls [ps::ps_exe()] to get the path of the executable. get_exe = function() ps_method(ps::ps_exe, self), #' @description #' Calls [ps::ps_cmdline()] to get the command line. get_cmdline = function() ps_method(ps::ps_cmdline, self), #' @description #' Calls [ps::ps_status()] to get the process status. get_status = function() ps_method(ps::ps_status, self), #' @description #' calls [ps::ps_username()] to get the username. get_username = function() ps_method(ps::ps_username, self), #' @description #' Calls [ps::ps_cwd()] to get the current working directory. get_wd = function() ps_method(ps::ps_cwd, self), #' @description #' Calls [ps::ps_cpu_times()] to get CPU usage data. get_cpu_times = function() ps_method(ps::ps_cpu_times, self), #' @description #' Calls [ps::ps_memory_info()] to get memory data. get_memory_info = function() ps_method(ps::ps_memory_info, self), #' @description #' Calls [ps::ps_suspend()] to suspend the process. suspend = function() ps_method(ps::ps_suspend, self), #' @description #' Calls [ps::ps_resume()] to resume a suspended process. resume = function() ps_method(ps::ps_resume, self) ), private = list( command = NULL, # Save 'command' argument here args = NULL, # Save 'args' argument here cleanup = NULL, # cleanup argument cleanup_tree = NULL, # cleanup_tree argument stdin = NULL, # stdin argument or stream stdout = NULL, # stdout argument or stream stderr = NULL, # stderr argument or stream pty = NULL, # whether we should create a PTY pty_options = NULL, # various PTY options pstdin = NULL, # the original stdin argument pstdout = NULL, # the original stdout argument pstderr = NULL, # the original stderr argument cleanfiles = NULL, # which temp stdout/stderr file(s) to clean up wd = NULL, # working directory (or NULL for current) starttime = NULL, # timestamp of start echo_cmd = NULL, # whether to echo the command windows_verbatim_args = NULL, windows_hide_window = NULL, status = NULL, # C file handle supervised = FALSE, # Whether process is tracked by supervisor stdin_pipe = NULL, stdout_pipe = NULL, stderr_pipe = NULL, poll_pipe = NULL, encoding = "", env = NULL, connections = list(), post_process = NULL, post_process_result = NULL, post_process_done = FALSE, tree_id = NULL, get_short_name = function() process_get_short_name(self, private), close_connections = function() process_close_connections(self, private) ) ) ## See the C source code for a discussion about the implementation ## of these methods process_wait <- function(self, private, timeout) { "!DEBUG process_wait `private$get_short_name()`" rethrow_call_with_cleanup( c_processx_wait, private$status, as.integer(timeout), private$get_short_name() ) invisible(self) } process_is_alive <- function(self, private) { "!DEBUG process_is_alive `private$get_short_name()`" rethrow_call(c_processx_is_alive, private$status, private$get_short_name()) } process_get_exit_status <- function(self, private) { "!DEBUG process_get_exit_status `private$get_short_name()`" rethrow_call(c_processx_get_exit_status, private$status, private$get_short_name()) } process_signal <- function(self, private, signal) { "!DEBUG process_signal `private$get_short_name()` `signal`" rethrow_call(c_processx_signal, private$status, as.integer(signal), private$get_short_name()) } process_interrupt <- function(self, private) { "!DEBUG process_interrupt `private$get_short_name()`" if (os_type() == "windows") { pid <- as.character(self$get_pid()) st <- run(get_tool("interrupt"), c(pid, "c"), error_on_status = FALSE) if (st$status == 0) TRUE else FALSE } else { rethrow_call(c_processx_interrupt, private$status, private$get_short_name()) } } process_kill <- function(self, private, grace, close_connections) { "!DEBUG process_kill '`private$get_short_name()`', pid `self$get_pid()`" ret <- rethrow_call(c_processx_kill, private$status, as.numeric(grace), private$get_short_name()) if (close_connections) private$close_connections() ret } process_kill_tree <- function(self, private, grace, close_connections) { "!DEBUG process_kill_tree '`private$get_short_name()`', pid `self$get_pid()`" if (!ps::ps_is_supported()) { throw(new_not_implemented_error( "kill_tree is not supported on this platform")) } ret <- get("ps_kill_tree", asNamespace("ps"))(private$tree_id) if (close_connections) private$close_connections() ret } process_get_start_time <- function(self, private) { format_unix_time(private$starttime) } process_get_pid <- function(self, private) { rethrow_call(c_processx_get_pid, private$status) } process_is_supervised <- function(self, private) { private$supervised } process_supervise <- function(self, private, status) { if (status && !self$is_supervised()) { supervisor_watch_pid(self$get_pid()) private$supervised <- TRUE } else if (!status && self$is_supervised()) { supervisor_unwatch_pid(self$get_pid()) private$supervised <- FALSE } } process_get_result <- function(self, private) { if (self$is_alive()) throw(new_error("Process is still alive")) if (!private$post_process_done && is.function(private$post_process)) { private$post_process_result <- private$post_process() private$post_process_done <- TRUE } private$post_process_result } process_as_ps_handle <- function(self, private) { ps::ps_handle(self$get_pid(), self$get_start_time()) } ps_method <- function(fun, self) { fun(ps::ps_handle(self$get_pid(), self$get_start_time())) } process_close_connections <- function(self, private) { for (f in c("stdin_pipe", "stdout_pipe", "stderr_pipe", "poll_pipe")) { if (!is.null(p <- private[[f]])) { rethrow_call(c_processx_connection_close, p) } } } #' Default options for pseudo terminals (ptys) #' #' @return Named list of default values of pty options. #' #' Options and default values: #' * `echo` whether to keep the echo on the terminal. `FALSE` turns echo #' off. #' * `rows` the (initial) terminal size, number of rows. #' * `cols` the (initial) terminal size, number of columns. #' #' @export default_pty_options <- function() { list( echo = FALSE, rows = 25L, cols = 80L ) }