/*
* Copyright (C) 2012-2017 Robin Haberkorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include
#include "sciteco.h"
#include "interface.h"
#include "undo.h"
#include "expressions.h"
#include "qregisters.h"
#include "eol.h"
#include "ring.h"
#include "parser.h"
#include "error.h"
#include "spawn.h"
/*
* Debian 7 is still at libglib v2.33, so
* for the time being we support this UNIX-only
* implementation of g_spawn_check_exit_status()
* partially emulating libglib v2.34
*/
#ifndef G_SPAWN_EXIT_ERROR
#ifdef G_OS_UNIX
#warning "libglib v2.34 or later recommended."
#else
#error "libglib v2.34 or later required."
#endif
#include
#include
#define G_SPAWN_EXIT_ERROR \
g_quark_from_static_string("g-spawn-exit-error-quark")
static gboolean
g_spawn_check_exit_status(gint exit_status, GError **error)
{
if (!WIFEXITED(exit_status)) {
g_set_error(error, G_SPAWN_ERROR, G_SPAWN_ERROR_FAILED,
"Abnormal process termination (%d)",
exit_status);
return FALSE;
}
if (WEXITSTATUS(exit_status) != 0) {
g_set_error(error, G_SPAWN_EXIT_ERROR, WEXITSTATUS(exit_status),
"Unsuccessful exit status %d",
WEXITSTATUS(exit_status));
return FALSE;
}
return TRUE;
}
#endif
namespace SciTECO {
namespace States {
StateExecuteCommand executecommand;
StateEGCommand egcommand;
}
extern "C" {
static void child_watch_cb(GPid pid, gint status, gpointer data);
static gboolean stdin_watch_cb(GIOChannel *chan,
GIOCondition condition, gpointer data);
static gboolean stdout_watch_cb(GIOChannel *chan,
GIOCondition condition, gpointer data);
}
static QRegister *register_argument = NULL;
gchar **
parse_shell_command_line(const gchar *cmdline, GError **error)
{
gchar **argv;
#ifdef G_OS_WIN32
if (!(Flags::ed & Flags::ED_SHELLEMU)) {
QRegister *reg = QRegisters::globals["$COMSPEC"];
argv = (gchar **)g_malloc(5*sizeof(gchar *));
argv[0] = reg->get_string();
argv[1] = g_strdup("/q");
argv[2] = g_strdup("/c");
argv[3] = g_strdup(cmdline);
argv[4] = NULL;
return argv;
}
#elif defined(G_OS_UNIX) || defined(G_OS_HAIKU)
if (!(Flags::ed & Flags::ED_SHELLEMU)) {
QRegister *reg = QRegisters::globals["$SHELL"];
argv = (gchar **)g_malloc(4*sizeof(gchar *));
argv[0] = reg->get_string();
argv[1] = g_strdup("-c");
argv[2] = g_strdup(cmdline);
argv[3] = NULL;
return argv;
}
#endif
if (!g_shell_parse_argv(cmdline, NULL, &argv, error))
return NULL;
return argv;
}
/*$ EC pipe filter
* EC[command]$ -- Execute operating system command and filter buffer contents
* linesEC[command]$
* -EC[command]$
* from,toEC[command]$
* :EC[command]$ -> Success|Failure
* lines:EC[command]$ -> Success|Failure
* -:EC[command]$ -> Success|Failure
* from,to:EC[command]$ -> Success|Failure
*
* The EC command allows you to interface with the operating
* system shell and external programs.
* The external program is spawned as a background process
* and its standard input stream is fed with data from the
* current document, i.e. text is piped into the external
* program.
* When automatic EOL translation is enabled, this will
* translate all end of line sequences according to the
* source document's EOL mode (see \fBEL\fP command).
* For instance when piping from a document with DOS
* line breaks, the receiving program will only be sent
* DOS line breaks.
* The process' standard output stream is also redirected
* and inserted into the current document.
* End of line sequences are normalized accordingly
* but the EOL mode guessed from the program's output is
* \fBnot\fP set on the document.
* The process' standard error stream is discarded.
* If data is piped into the external program, its output
* replaces that data in the buffer.
* Dot is always left at the end of the insertion.
*
* If invoked without parameters, no data is piped into
* the process (and no characters are removed) and its
* output is inserted at the current buffer position.
* This is equivalent to invoking \(lq.,.EC\(rq.
* If invoked with one parameter, the next or previous number
* of are piped from the buffer into the program and
* its output replaces these .
* This effectively runs as a filter over .
* \(lq-EC\(rq may be written as a short-cut for \(lq-1EC\(rq.
* When invoked with two parameters, the characters beginning
* at position up to the character at position
* are piped into the program and replaced with its output.
* This effectively runs as a filter over a buffer
* range.
*
* Errors are thrown not only for invalid buffer ranges
* but also for errors during process execution.
* If the external has an unsuccessful exit code,
* the EC command will also fail.
* If the EC command is colon-modified, it will instead return
* a TECO boolean signifying success or failure.
* In case of an unsuccessful exit code, a colon-modified EC
* will return the absolute value of the process exit
* code (which is also a TECO failure boolean) and 0 for all
* other failures.
* This feature may be used to take action depending on a
* specific process exit code.
*
* execution is by default platform-dependent.
* On DOS-like systems like Windows, is passed to
* the command interpreter specified in the \fB$COMSPEC\fP
* environment variable with the \(lq/q\(rq and \(lq/c\(rq
* command-line arguments.
* On UNIX-like systems, is passed to the interpreter
* specified by the \fB$SHELL\fP environment variable
* with the \(lq-c\(rq command-line argument.
* Therefore the default shell can be configured using
* the corresponding environment registers.
* The operating system restrictions on the maximum
* length of command-line arguments apply to and
* quoting of parameters within is somewhat platform
* dependent.
* On all other platforms, \*(ST will uniformly parse
* just as an UNIX98 \(lq/bin/sh\(rq would, but without
* performing any expansions.
* The program specified in is searched for in
* standard locations (according to the \fB$PATH\fP environment
* variable).
* This mode of operation can also be enforced on all platforms
* by enabling bit 7 in the ED flag, e.g. by executing
* \(lq0,128ED\(rq, and is recommended when writing cross-platform
* macros using the EC command.
*
* When using an UNIX-compatible shell or the UNIX98 shell emulation,
* you might want to use the \fB^E@\fP string-building character
* to pass Q-Register contents reliably as single arguments to
* the spawned process.
*
* The spawned process inherits both \*(ST's current working
* directory and its environment variables.
* More precisely, \*(ST uses its environment registers
* to construct the spawned process' environment.
* Therefore it is also straight forward to change the working
* directory or some environment variable temporarily
* for a spawned process.
*
* Note that when run interactively and subsequently rubbed
* out, \*(ST can easily undo all changes to the editor
* state.
* It \fBcannot\fP however undo any other side-effects that the
* execution of might have had on your system.
*
* Note also that the EC command blocks indefinitely until
* the completes, which may result in editor hangs.
* You may however interrupt the spawned process by sending
* the \fBSIGINT\fP signal to \*(ST, e.g. by pressing CTRL+C.
*
* In interactive mode, \*(ST performs TAB-completion
* of filenames in the string parameter but
* does not attempt any escaping of shell-relevant
* characters like whitespaces.
*/
StateExecuteCommand::StateExecuteCommand() : StateExpectString()
{
/*
* Context and loop can be reused between EC invocations.
* However we should not use the default context, since it
* may be used by GTK
*/
ctx.mainctx = g_main_context_new();
ctx.mainloop = g_main_loop_new(ctx.mainctx, FALSE);
}
StateExecuteCommand::~StateExecuteCommand()
{
g_main_loop_unref(ctx.mainloop);
#ifndef G_OS_HAIKU
/*
* Apparently, there's some kind of double-free
* bug in Haiku's glib-2.38.
* It is unknown whether this is has
* already been fixed and affects other platforms
* (but I never observed any segfaults).
*/
g_main_context_unref(ctx.mainctx);
#endif
delete ctx.error;
}
void
StateExecuteCommand::initial(void)
{
tecoBool rc = SUCCESS;
expressions.eval();
/*
* By evaluating arguments here, the command may fail
* before the string argument is typed
*/
switch (expressions.args()) {
case 0:
if (expressions.num_sign > 0) {
/* pipe nothing, insert at dot */
ctx.from = ctx.to = interface.ssm(SCI_GETCURRENTPOS);
break;
}
/* fall through if prefix sign is "-" */
case 1: {
/* pipe and replace line range */
sptr_t line;
ctx.from = interface.ssm(SCI_GETCURRENTPOS);
line = interface.ssm(SCI_LINEFROMPOSITION, ctx.from) +
expressions.pop_num_calc();
ctx.to = interface.ssm(SCI_POSITIONFROMLINE, line);
rc = TECO_BOOL(Validate::line(line));
if (ctx.to < ctx.from) {
tecoInt temp = ctx.from;
ctx.from = ctx.to;
ctx.to = temp;
}
break;
}
default:
/* pipe and replace character range */
ctx.to = expressions.pop_num_calc();
ctx.from = expressions.pop_num_calc();
rc = TECO_BOOL(ctx.from <= ctx.to &&
Validate::pos(ctx.from) &&
Validate::pos(ctx.to));
break;
}
if (IS_FAILURE(rc)) {
if (eval_colon()) {
expressions.push(rc);
ctx.from = ctx.to = -1;
/* done() will still be called */
} else {
throw RangeError("EC");
}
}
}
State *
StateExecuteCommand::done(const gchar *str)
{
BEGIN_EXEC(&States::start);
if (ctx.from < 0)
/*
* initial() failed without throwing
* error (colon-modified)
*/
return &States::start;
GError *error = NULL;
gchar **argv, **envp;
static const gint flags = G_SPAWN_DO_NOT_REAP_CHILD |
G_SPAWN_SEARCH_PATH |
G_SPAWN_STDERR_TO_DEV_NULL;
GPid pid;
gint stdin_fd, stdout_fd;
GIOChannel *stdin_chan, *stdout_chan;
/*
* We always read from the current view,
* so we use its EOL mode.
*
* NOTE: We do not declare the writer/reader objects as part of
* StateExecuteCommand::Context so we do not have to
* reset it. It's only required for the life time of this call
* anyway.
* I do not see a more elegant way out of this.
*/
EOLWriterGIO stdin_writer(interface.ssm(SCI_GETEOLMODE));
EOLReaderGIO stdout_reader;
ctx.text_added = false;
ctx.stdin_writer = &stdin_writer;
ctx.stdout_reader = &stdout_reader;
delete ctx.error;
ctx.error = NULL;
ctx.rc = FAILURE;
argv = parse_shell_command_line(str, &error);
if (!argv)
goto gerror;
envp = QRegisters::globals.get_environ();
g_spawn_async_with_pipes(NULL, argv, envp, (GSpawnFlags)flags,
NULL, NULL, &pid,
&stdin_fd, &stdout_fd, NULL,
&error);
g_strfreev(envp);
g_strfreev(argv);
if (error)
goto gerror;
ctx.child_src = g_child_watch_source_new(pid);
g_source_set_callback(ctx.child_src, (GSourceFunc)child_watch_cb,
&ctx, NULL);
g_source_attach(ctx.child_src, ctx.mainctx);
#ifdef G_OS_WIN32
stdin_chan = g_io_channel_win32_new_fd(stdin_fd);
stdout_chan = g_io_channel_win32_new_fd(stdout_fd);
#else /* the UNIX constructors should work everywhere else */
stdin_chan = g_io_channel_unix_new(stdin_fd);
stdout_chan = g_io_channel_unix_new(stdout_fd);
#endif
g_io_channel_set_flags(stdin_chan, G_IO_FLAG_NONBLOCK, NULL);
g_io_channel_set_encoding(stdin_chan, NULL, NULL);
/*
* EOLWriterGIO expects the channel to be buffered
* for performance reasons
*/
g_io_channel_set_buffered(stdin_chan, TRUE);
g_io_channel_set_flags(stdout_chan, G_IO_FLAG_NONBLOCK, NULL);
g_io_channel_set_encoding(stdout_chan, NULL, NULL);
g_io_channel_set_buffered(stdout_chan, FALSE);
stdin_writer.set_channel(stdin_chan);
stdout_reader.set_channel(stdout_chan);
ctx.stdin_src = g_io_create_watch(stdin_chan,
(GIOCondition)(G_IO_OUT | G_IO_ERR | G_IO_HUP));
g_source_set_callback(ctx.stdin_src, (GSourceFunc)stdin_watch_cb,
&ctx, NULL);
g_source_attach(ctx.stdin_src, ctx.mainctx);
ctx.stdout_src = g_io_create_watch(stdout_chan,
(GIOCondition)(G_IO_IN | G_IO_ERR | G_IO_HUP));
g_source_set_callback(ctx.stdout_src, (GSourceFunc)stdout_watch_cb,
&ctx, NULL);
g_source_attach(ctx.stdout_src, ctx.mainctx);
if (!register_argument) {
if (current_doc_must_undo())
interface.undo_ssm(SCI_GOTOPOS, interface.ssm(SCI_GETCURRENTPOS));
interface.ssm(SCI_GOTOPOS, ctx.to);
}
interface.ssm(SCI_BEGINUNDOACTION);
ctx.start = ctx.from;
g_main_loop_run(ctx.mainloop);
if (!register_argument)
interface.ssm(SCI_DELETERANGE, ctx.from, ctx.to - ctx.from);
interface.ssm(SCI_ENDUNDOACTION);
if (register_argument) {
if (stdout_reader.eol_style >= 0) {
register_argument->undo_set_eol_mode();
register_argument->set_eol_mode(stdout_reader.eol_style);
}
} else if (ctx.from != ctx.to || ctx.text_added) {
/* undo action is only effective if it changed anything */
if (current_doc_must_undo())
interface.undo_ssm(SCI_UNDO);
interface.ssm(SCI_SCROLLCARET);
ring.dirtify();
}
if (!g_source_is_destroyed(ctx.stdin_src))
g_io_channel_shutdown(stdin_chan, TRUE, NULL);
g_io_channel_unref(stdin_chan);
g_source_unref(ctx.stdin_src);
g_io_channel_shutdown(stdout_chan, TRUE, NULL);
g_io_channel_unref(stdout_chan);
g_source_unref(ctx.stdout_src);
g_source_unref(ctx.child_src);
g_spawn_close_pid(pid);
if (ctx.error) {
if (!eval_colon())
throw *ctx.error;
/*
* This may contain the exit status
* encoded as a tecoBool.
*/
expressions.push(ctx.rc);
goto cleanup;
}
if (interface.is_interrupted())
throw Error("Interrupted");
if (eval_colon())
expressions.push(SUCCESS);
goto cleanup;
gerror:
if (!eval_colon())
throw GlibError(error);
g_error_free(error);
expressions.push(ctx.rc);
cleanup:
undo.push_var(register_argument) = NULL;
return &States::start;
}
/*$ EG EGq
* EGq[command]$ -- Set Q-Register to output of operating system command
* linesEGq[command]$
* -EGq[command]$
* from,toEGq[command]$
* :EGq[command]$ -> Success|Failure
* lines:EGq[command]$ -> Success|Failure
* -:EGq[command]$ -> Success|Failure
* from,to:EGq[command]$ -> Success|Failure
*
* Runs an operating system and set Q-Register
* to the data read from its standard output stream.
* Data may be fed to from the current buffer/document.
* The interpretation of the parameters and as well
* as the colon-modification is analoguous to the EC command.
*
* The EG command only differs from EC in not deleting any
* characters from the current buffer, not changing
* the current buffer position and writing process output
* to the Q-Register .
* In other words, the current buffer is not modified by EG.
* Also since EG replaces the string value of , the register's
* EOL mode is set to the mode guessed from the external program's
* output.
*
* The register is defined if it does not already exist.
*/
State *
StateEGCommand::got_register(QRegister *reg)
{
machine.reset();
BEGIN_EXEC(&States::executecommand);
undo.push_var(register_argument) = reg;
return &States::executecommand;
}
/*
* Glib callbacks
*/
static void
child_watch_cb(GPid pid, gint status, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
GError *error = NULL;
/*
* Writing stdin or reading stdout might have already
* failed. We preserve the earliest GError.
*/
if (!ctx.error && !g_spawn_check_exit_status(status, &error)) {
ctx.rc = error->domain == G_SPAWN_EXIT_ERROR
? ABS(error->code) : FAILURE;
ctx.error = new GlibError(error);
}
if (g_source_is_destroyed(ctx.stdout_src))
g_main_loop_quit(ctx.mainloop);
}
static gboolean
stdin_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
sptr_t gap;
gsize convert_len;
const gchar *buffer;
gsize bytes_written;
if (!(condition & G_IO_OUT))
/* stdin might be closed prematurely */
goto remove;
/* we always read from the current view */
gap = interface.ssm(SCI_GETGAPPOSITION);
convert_len = ctx.start < gap && gap < ctx.to
? gap - ctx.start : ctx.to - ctx.start;
buffer = (const gchar *)interface.ssm(SCI_GETRANGEPOINTER,
ctx.start, convert_len);
try {
/*
* This cares about automatic EOL conversion and
* returns the number of consumed bytes.
* If it can only write a part of the EOL sequence (ie. CR of CRLF)
* it may return a short byte count (possibly 0) which ensures that
* we do not yet remove the source.
*/
bytes_written = ctx.stdin_writer->convert(buffer, convert_len);
} catch (Error &e) {
ctx.error = new Error(e);
/* do not yet quit -- we still have to reap the child */
goto remove;
}
ctx.start += bytes_written;
if (ctx.start == ctx.to)
/* this will signal EOF to the process */
goto remove;
return G_SOURCE_CONTINUE;
remove:
/*
* Channel is always shut down here (fd is closed),
* so it's always shut down IF the GSource has been
* destroyed. It is not guaranteed to be destroyed
* during the main loop run however since it quits
* as soon as the child was reaped and stdout was read.
*/
g_io_channel_shutdown(chan, TRUE, NULL);
return G_SOURCE_REMOVE;
}
static gboolean
stdout_watch_cb(GIOChannel *chan, GIOCondition condition, gpointer data)
{
StateExecuteCommand::Context &ctx =
*(StateExecuteCommand::Context *)data;
for (;;) {
const gchar *buffer;
gsize data_len;
try {
buffer = ctx.stdout_reader->convert(data_len);
} catch (Error &e) {
ctx.error = new Error(e);
goto remove;
}
if (!buffer)
/* EOF */
goto remove;
if (!data_len)
return G_SOURCE_CONTINUE;
if (register_argument) {
if (ctx.text_added) {
register_argument->undo_append_string();
register_argument->append_string(buffer, data_len);
} else {
register_argument->undo_set_string();
register_argument->set_string(buffer, data_len);
}
} else {
interface.ssm(SCI_ADDTEXT, data_len, (sptr_t)buffer);
}
ctx.text_added = true;
}
/* not reached */
return G_SOURCE_CONTINUE;
remove:
if (g_source_is_destroyed(ctx.child_src))
g_main_loop_quit(ctx.mainloop);
return G_SOURCE_REMOVE;
}
} /* namespace SciTECO */