diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2014-11-02 20:00:45 +0100 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2014-11-09 20:29:23 +0100 |
commit | acc911ae724d42aaa2fecaa6f9b30651a91ad240 (patch) | |
tree | 69abdd4becf0c0e8cac28308da28c36b49cbfa41 /src | |
parent | a395c9df173b53e095f1a20d6bc583063cdb60c7 (diff) | |
download | sciteco-acc911ae724d42aaa2fecaa6f9b30651a91ad240.tar.gz |
implemented EC command (execute operating system command) in spawn.cpp
powerful command for filtering a SciTECO buffer through an external
program. It will be described in the sciteco(7) man pages.
The implementation uses an asynchronous background process with
pipes but is platform independant thanks to glib's g_spawn functions,
GIOChannels and event loops.
There are however platform differences in how the operating system
command is interpreted/parsed.
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/cmdline.cpp | 1 | ||||
-rw-r--r-- | src/parser.cpp | 2 | ||||
-rw-r--r-- | src/sciteco.h | 3 | ||||
-rw-r--r-- | src/spawn.cpp | 366 | ||||
-rw-r--r-- | src/spawn.h | 56 |
6 files changed, 428 insertions, 1 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 60b866e..53a2b85 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -28,6 +28,7 @@ libsciteco_base_a_SOURCES = main.cpp sciteco.h \ ring.cpp ring.h \ parser.cpp parser.h \ search.cpp search.h \ + spawn.cpp spawn.h \ goto.cpp goto.h \ rbtree.cpp rbtree.h \ symbols.cpp symbols.h \ diff --git a/src/cmdline.cpp b/src/cmdline.cpp index 8422248..668638c 100644 --- a/src/cmdline.cpp +++ b/src/cmdline.cpp @@ -36,6 +36,7 @@ #include "goto.h" #include "undo.h" #include "symbols.h" +#include "spawn.h" #include "cmdline.h" static inline const gchar *process_edit_cmd(gchar key); diff --git a/src/parser.cpp b/src/parser.cpp index 17a11b9..bbaad14 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -37,6 +37,7 @@ #include "parser.h" #include "symbols.h" #include "search.h" +#include "spawn.h" #include "cmdline.h" //#define DEBUG @@ -1811,6 +1812,7 @@ StateECommand::StateECommand() : State() { transitions['\0'] = this; transitions['B'] = &States::editfile; + transitions['C'] = &States::executecommand; transitions['M'] = &States::macro_file; transitions['S'] = &States::scintilla_symbols; transitions['Q'] = &States::eqcommand; diff --git a/src/sciteco.h b/src/sciteco.h index 1a349a1..4e5430c 100644 --- a/src/sciteco.h +++ b/src/sciteco.h @@ -38,7 +38,8 @@ typedef tecoInt tecoBool; namespace Flags { enum { ED_HOOKS = (1 << 5), - ED_FNKEYS = (1 << 6) + ED_FNKEYS = (1 << 6), + ED_SHELLEMU = (1 << 7) }; extern tecoInt ed; diff --git a/src/spawn.cpp b/src/spawn.cpp new file mode 100644 index 0000000..22c6a00 --- /dev/null +++ b/src/spawn.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2012-2014 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 <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <glib.h> + +#include "sciteco.h" +#include "interface.h" +#include "undo.h" +#include "expressions.h" +#include "qregisters.h" +#include "ring.h" +#include "parser.h" +#include "spawn.h" + +namespace States { + StateExecuteCommand executecommand; +} + +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); +} + +gchar ** +parse_shell_command_line(const gchar *cmdline, GError **error) +{ + gchar **argv; + +#ifdef G_OS_WIN32 + if (!(Flags::ed & Flags::ED_SHELLEMU)) { + const gchar *argv_win32[] = { + "cmd.exe", "/q", "/c", cmdline, NULL + }; + return g_strdupv((gchar **)argv_win32); + } +#elif defined(G_OS_UNIX) + if (!(Flags::ed & Flags::ED_SHELLEMU)) { + const gchar *argv_unix[] = { + "/bin/sh", "-c", cmdline, NULL + }; + return g_strdupv((gchar **)argv_unix); + } +#endif + + if (!g_shell_parse_argv(cmdline, NULL, &argv, error)) + return NULL; + + return argv; +} + +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); + g_main_context_unref(ctx.mainctx); +} + +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: + /* pipe nothing, insert at dot */ + ctx.from = ctx.to = interface.ssm(SCI_GETCURRENTPOS); + break; + + 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)); + + 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"); + } + } +} + +/* + * FIXME: `xclip -selection clipboard -in` hangs -- the + * stdout watcher is never activated! + * Workaround is to pipe to /dev/null + */ +State * +StateExecuteCommand::done(const gchar *str) +{ + BEGIN_EXEC(&States::start); + + if (ctx.from < 0) + /* + * initial() failed without throwing + * error (colon-modified) + */ + return &States::start; + + gchar **argv; + 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; + + ctx.text_added = false; + ctx.error = NULL; + + argv = parse_shell_command_line(str, &ctx.error); + if (!argv) + goto gerror; + + g_spawn_async_with_pipes(NULL, argv, NULL, (GSpawnFlags)flags, + NULL, NULL, &pid, + &stdin_fd, &stdout_fd, NULL, + &ctx.error); + + g_strfreev(argv); + + if (ctx.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); + g_io_channel_set_buffered(stdin_chan, FALSE); + 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); + + 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 (current_doc_must_undo()) + undo.push_msg(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); + interface.ssm(SCI_DELETERANGE, ctx.from, ctx.to - ctx.from); + interface.ssm(SCI_ENDUNDOACTION); + + if (ctx.start != ctx.from || ctx.text_added) { + /* undo action is only effective if it changed anything */ + if (current_doc_must_undo()) + undo.push_msg(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) + goto gerror; + + if (interface.is_interrupted()) + throw State::Error("Interrupted"); + + if (eval_colon()) + expressions.push(SUCCESS); + + return &States::start; + +gerror: + if (!eval_colon()) + throw GError(ctx.error); + + /* + * If possible, encode process exit code + * in return boolean. It's guaranteed to be + * a failure since it's non-negative. + */ + if (ctx.error->domain == G_SPAWN_EXIT_ERROR) + expressions.push(ABS(ctx.error->code)); + else + expressions.push(FAILURE); + return &States::start; +} + +/* + * Glib callbacks + */ + +static void +child_watch_cb(GPid pid, gint status, gpointer data) +{ + StateExecuteCommand::Context &ctx = + *(StateExecuteCommand::Context *)data; + + /* + * Writing stdin or reading stdout might have already + * failed. We preserve the earliest GError. + */ + if (!ctx.error) + g_spawn_check_exit_status(status, &ctx.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; + + const gchar *buffer; + gsize bytes_written; + + buffer = (const gchar *)interface.ssm(SCI_GETRANGEPOINTER, + ctx.from, + ctx.to - ctx.start); + + switch (g_io_channel_write_chars(chan, buffer, ctx.to - ctx.start, + &bytes_written, + ctx.error ? NULL : &ctx.error)) { + case G_IO_STATUS_ERROR: + /* do not yet quit -- we still have to reap the child */ + goto remove; + case G_IO_STATUS_NORMAL: + break; + case G_IO_STATUS_EOF: + /* process closed stdin preliminarily? */ + goto remove; + case G_IO_STATUS_AGAIN: + return G_SOURCE_CONTINUE; + } + + 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; + + gchar buffer[1024]; + gsize bytes_read; + + switch (g_io_channel_read_chars(chan, buffer, sizeof(buffer), + &bytes_read, + ctx.error ? NULL : &ctx.error)) { + case G_IO_STATUS_NORMAL: + break; + case G_IO_STATUS_ERROR: + case G_IO_STATUS_EOF: + if (g_source_is_destroyed(ctx.child_src)) + g_main_loop_quit(ctx.mainloop); + return G_SOURCE_REMOVE; + case G_IO_STATUS_AGAIN: + return G_SOURCE_CONTINUE; + } + + interface.ssm(SCI_ADDTEXT, bytes_read, (sptr_t)buffer); + ctx.text_added = true; + + return G_SOURCE_CONTINUE; +} diff --git a/src/spawn.h b/src/spawn.h new file mode 100644 index 0000000..b6cb298 --- /dev/null +++ b/src/spawn.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012-2014 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef __SPAWN_H +#define __SPAWN_H + +#include <glib.h> + +#include "sciteco.h" +#include "parser.h" + +gchar **parse_shell_command_line(const gchar *cmdline, GError **error); + +class StateExecuteCommand : public StateExpectString { +public: + StateExecuteCommand(); + ~StateExecuteCommand(); + + struct Context { + GMainContext *mainctx; + GMainLoop *mainloop; + GSource *child_src; + GSource *stdin_src, *stdout_src; + + tecoInt from, to; + tecoInt start; + bool text_added; + ::GError *error; + }; + +private: + Context ctx; + + void initial(void); + State *done(const gchar *str); +}; + +namespace States { + extern StateExecuteCommand executecommand; +} + +#endif |