Support specifying an exit status on quit commands

The current client exit status can be specified as an optional
parameter, is nothing is given the exit status will be 0.

Fixes #1230
This commit is contained in:
Maxime Coste 2017-08-23 13:22:23 +07:00
parent 3efc406d57
commit f7bed9eb18
10 changed files with 95 additions and 57 deletions

View File

@ -682,11 +682,14 @@ command `q!` has to be used).
permissions are temporarily changed to allow saving the buffer and permissions are temporarily changed to allow saving the buffer and
restored afterwards when the write! command is used. restored afterwards when the write! command is used.
* `w[rite]a[ll]`: write all buffers that are associated to a file. * `w[rite]a[ll]`: write all buffers that are associated to a file.
* `q[uit][!]`: exit Kakoune, use quit! to force quitting even if there is some * `q[uit][!] [<exit status>]`: exit Kakoune, use quit! to force quitting even
unsaved buffers remaining. if there is some unsaved buffers remaining. If specified, the client exit
status will be set to <exit status>.
* `w[a]q[!] [<exit status>]`: write the current buffer (or all buffers when
`waq` is used) and quit. If specified, the client exit status will be set
to <exit status>.
* `kill[!]`: terminate the current session, all the clients as well as the server, * `kill[!]`: terminate the current session, all the clients as well as the server,
use kill! to ignore unsaved buffers use kill! to ignore unsaved buffers
* `w[a]q[!]`: write the current buffer (or all buffers when `waq` is used) and quit
* `b[uffer] <name>`: switch to buffer <name> * `b[uffer] <name>`: switch to buffer <name>
* `b[uffer]n[ext]`: switch to the next buffer * `b[uffer]n[ext]`: switch to the next buffer
* `b[uffer]p[rev]`: switch to the previous buffer * `b[uffer]p[rev]`: switch to the previous buffer

View File

@ -33,16 +33,18 @@ command *q!* has to be used).
*w[rite]a[ll]*:: *w[rite]a[ll]*::
write all buffers that are associated to a file write all buffers that are associated to a file
*q[uit][!]*:: *q[uit][!]* [<exit status>]::
exit Kakoune, use quit! to force quitting even if there is some exit Kakoune, use quit! to force quitting even if there is some
unsaved buffers remaining unsaved buffers remaining. If specified, the client exit status
will be set to <exit status>
*w[a]q[!]* [<exit status>]::
write the current buffer (or all buffers when *waq* is used) and quit.
If specified, the client exit status will be set to <exit status>
*kill*:: *kill*::
terminate the current session, all the clients as well as the server terminate the current session, all the clients as well as the server
*w[a]q[!]*::
write the current buffer (or all buffers when *waq* is used) and quit
*b[uffer]* <name>:: *b[uffer]* <name>::
switch to buffer <name> switch to buffer <name>

View File

@ -26,8 +26,10 @@ Client::Client(std::unique_ptr<UserInterface>&& ui,
std::unique_ptr<Window>&& window, std::unique_ptr<Window>&& window,
SelectionList selections, SelectionList selections,
EnvVarMap env_vars, EnvVarMap env_vars,
String name) String name,
OnExitCallback on_exit)
: m_ui{std::move(ui)}, m_window{std::move(window)}, : m_ui{std::move(ui)}, m_window{std::move(window)},
m_on_exit{std::move(on_exit)},
m_input_handler{std::move(selections), Context::Flags::None, m_input_handler{std::move(selections), Context::Flags::None,
std::move(name)}, std::move(name)},
m_env_vars(std::move(env_vars)) m_env_vars(std::move(env_vars))

View File

@ -21,15 +21,17 @@ enum class EventMode;
enum class InfoStyle; enum class InfoStyle;
enum class MenuStyle; enum class MenuStyle;
class Client : public SafeCountable, public OptionManagerWatcher class Client : public SafeCountable, public OptionManagerWatcher
{ {
public: public:
using OnExitCallback = std::function<void (int status)>;
Client(std::unique_ptr<UserInterface>&& ui, Client(std::unique_ptr<UserInterface>&& ui,
std::unique_ptr<Window>&& window, std::unique_ptr<Window>&& window,
SelectionList selections, SelectionList selections,
EnvVarMap env_vars, EnvVarMap env_vars,
String name); String name,
OnExitCallback on_exit);
~Client(); ~Client();
Client(Client&&) = delete; Client(Client&&) = delete;
@ -65,6 +67,8 @@ public:
Buffer* last_buffer() const { return m_last_buffer.get(); } Buffer* last_buffer() const { return m_last_buffer.get(); }
void set_last_buffer(Buffer* last_buffer) { m_last_buffer = last_buffer; } void set_last_buffer(Buffer* last_buffer) { m_last_buffer = last_buffer; }
void exit(int status) { m_on_exit(status); }
private: private:
void on_option_changed(const Option& option) override; void on_option_changed(const Option& option) override;
@ -79,6 +83,8 @@ private:
std::unique_ptr<UserInterface> m_ui; std::unique_ptr<UserInterface> m_ui;
std::unique_ptr<Window> m_window; std::unique_ptr<Window> m_window;
OnExitCallback m_on_exit;
EnvVarMap m_env_vars; EnvVarMap m_env_vars;
InputHandler m_input_handler; InputHandler m_input_handler;

View File

@ -40,13 +40,14 @@ String ClientManager::generate_name() const
Client* ClientManager::create_client(std::unique_ptr<UserInterface>&& ui, Client* ClientManager::create_client(std::unique_ptr<UserInterface>&& ui,
EnvVarMap env_vars, StringView init_cmds, EnvVarMap env_vars, StringView init_cmds,
Optional<BufferCoord> init_coord) Optional<BufferCoord> init_coord,
Client::OnExitCallback on_exit)
{ {
Buffer& buffer = BufferManager::instance().get_first_buffer(); Buffer& buffer = BufferManager::instance().get_first_buffer();
WindowAndSelections ws = get_free_window(buffer); WindowAndSelections ws = get_free_window(buffer);
Client* client = new Client{std::move(ui), std::move(ws.window), Client* client = new Client{std::move(ui), std::move(ws.window),
std::move(ws.selections), std::move(env_vars), std::move(ws.selections), std::move(env_vars),
generate_name()}; generate_name(), std::move(on_exit)};
m_clients.emplace_back(client); m_clients.emplace_back(client);
if (init_coord) if (init_coord)
@ -88,7 +89,7 @@ void ClientManager::process_pending_inputs() const
} }
} }
void ClientManager::remove_client(Client& client, bool graceful) void ClientManager::remove_client(Client& client, bool graceful, int status)
{ {
auto it = find(m_clients, &client); auto it = find(m_clients, &client);
if (it == m_clients.end()) if (it == m_clients.end())
@ -96,6 +97,7 @@ void ClientManager::remove_client(Client& client, bool graceful)
kak_assert(contains(m_client_trash, &client)); kak_assert(contains(m_client_trash, &client));
return; return;
} }
client.exit(status);
m_client_trash.push_back(std::move(*it)); m_client_trash.push_back(std::move(*it));
m_clients.erase(it); m_clients.erase(it);

View File

@ -21,7 +21,8 @@ public:
Client* create_client(std::unique_ptr<UserInterface>&& ui, Client* create_client(std::unique_ptr<UserInterface>&& ui,
EnvVarMap env_vars, StringView init_cmds, EnvVarMap env_vars, StringView init_cmds,
Optional<BufferCoord> init_coord); Optional<BufferCoord> init_coord,
Client::OnExitCallback on_exit);
bool empty() const { return m_clients.empty(); } bool empty() const { return m_clients.empty(); }
size_t count() const { return m_clients.size(); } size_t count() const { return m_clients.size(); }
@ -39,7 +40,7 @@ public:
Client* get_client_ifp(StringView name); Client* get_client_ifp(StringView name);
Client& get_client(StringView name); Client& get_client(StringView name);
bool validate_client_name(StringView name) const; bool validate_client_name(StringView name) const;
void remove_client(Client& client, bool graceful); void remove_client(Client& client, bool graceful, int status);
using ClientList = Vector<std::unique_ptr<Client>, MemoryDomain::Client>; using ClientList = Vector<std::unique_ptr<Client>, MemoryDomain::Client>;
using iterator = ClientList::const_iterator; using iterator = ClientList::const_iterator;

View File

@ -423,24 +423,26 @@ const CommandDesc force_kill_cmd = {
}; };
template<bool force> template<bool force>
void quit(Context& context) void quit(const ParametersParser& parser, Context& context, const ShellContext&)
{ {
if (not force and ClientManager::instance().count() == 1) if (not force and ClientManager::instance().count() == 1)
ensure_all_buffers_are_saved(); ensure_all_buffers_are_saved();
ClientManager::instance().remove_client(context.client(), true); const int status = parser.positional_count() > 0 ? str_to_int(parser[0]) : 0;
ClientManager::instance().remove_client(context.client(), true, status);
} }
const CommandDesc quit_cmd = { const CommandDesc quit_cmd = {
"quit", "quit",
"q", "q",
"quit current client, and the kakoune session if the client is the last " "quit current client, and the kakoune session if the client is the last "
"(if not running in daemon mode)", "(if not running in daemon mode). An optional integer parameter can set "
no_params, "the client exit status",
{ {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
CommandCompleter{}, CommandCompleter{},
[](const ParametersParser&, Context& context, const ShellContext&){ quit<false>(context); } quit<false>
}; };
const CommandDesc force_quit_cmd = { const CommandDesc force_quit_cmd = {
@ -448,27 +450,29 @@ const CommandDesc force_quit_cmd = {
"q!", "q!",
"quit current client, and the kakoune session if the client is the last " "quit current client, and the kakoune session if the client is the last "
"(if not running in daemon mode). force quit even if the client is the " "(if not running in daemon mode). force quit even if the client is the "
"last and some buffers are not saved.", "last and some buffers are not saved. An optional integer parameter can "
no_params, "set the client exit status",
{ {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
CommandCompleter{}, CommandCompleter{},
[](const ParametersParser&, Context& context, const ShellContext&){ quit<true>(context); } quit<true>
}; };
template<bool force> template<bool force>
void write_quit(const ParametersParser& parser, Context& context, void write_quit(const ParametersParser& parser, Context& context,
const ShellContext& shell_context) const ShellContext& shell_context)
{ {
write_buffer(parser, context, shell_context); write_buffer({{}, {}}, context, shell_context);
quit<force>(context); quit<force>(parser, context, shell_context);
} }
const CommandDesc write_quit_cmd = { const CommandDesc write_quit_cmd = {
"write-quit", "write-quit",
"wq", "wq",
"write current buffer and quit current client", "write current buffer and quit current client. An optional integer"
no_params, "parameter can set the client exit status",
{ {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
CommandCompleter{}, CommandCompleter{},
@ -479,8 +483,8 @@ const CommandDesc force_write_quit_cmd = {
"write-quit!", "write-quit!",
"wq!", "wq!",
"write current buffer and quit current client, even if other buffers are " "write current buffer and quit current client, even if other buffers are "
"not saved", "not saved. An optional integer parameter can set the client exit status",
no_params, { {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
CommandCompleter{}, CommandCompleter{},
@ -490,15 +494,16 @@ const CommandDesc force_write_quit_cmd = {
const CommandDesc write_all_quit_cmd = { const CommandDesc write_all_quit_cmd = {
"write-all-quit", "write-all-quit",
"waq", "waq",
"write all buffers associated to a file and quit current client", "write all buffers associated to a file and quit current client. An "
no_params, "optional integer parameter can set the client exit status",
{ {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
CommandFlags::None, CommandFlags::None,
CommandHelper{}, CommandHelper{},
CommandCompleter{}, CommandCompleter{},
[](const ParametersParser& parser, Context& context, const ShellContext&) [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
{ {
write_all_buffers(context); write_all_buffers(context);
quit<false>(context); quit<false>(parser, context, shell_context);
} }
}; };

View File

@ -328,6 +328,7 @@ void register_options()
} }
static Client* local_client = nullptr; static Client* local_client = nullptr;
static int local_client_exit = 0;
static UserInterface* local_ui = nullptr; static UserInterface* local_ui = nullptr;
static bool convert_to_client_pending = false; static bool convert_to_client_pending = false;
@ -403,7 +404,7 @@ std::unique_ptr<UserInterface> create_local_ui(UIType ui_type)
kak_assert(not local_ui); kak_assert(not local_ui);
local_ui = this; local_ui = this;
m_old_sighup = set_signal_handler(SIGHUP, [](int) { m_old_sighup = set_signal_handler(SIGHUP, [](int) {
ClientManager::instance().remove_client(*local_client, false); ClientManager::instance().remove_client(*local_client, false, -1);
static_cast<LocalUI*>(local_ui)->on_sighup(); static_cast<LocalUI*>(local_ui)->on_sighup();
}); });
@ -441,7 +442,7 @@ std::unique_ptr<UserInterface> create_local_ui(UIType ui_type)
if (fork_server_to_background()) if (fork_server_to_background())
{ {
this->NCursesUI::~NCursesUI(); this->NCursesUI::~NCursesUI();
exit(0); exit(local_client_exit);
} }
} }
} }
@ -475,16 +476,15 @@ int run_client(StringView session, StringView client_init,
{ {
EventManager event_manager; EventManager event_manager;
RemoteClient client{session, make_ui(ui_type), get_env_vars(), client_init, std::move(init_coord)}; RemoteClient client{session, make_ui(ui_type), get_env_vars(), client_init, std::move(init_coord)};
while (true) while (not client.exit_status())
event_manager.handle_next_events(EventMode::Normal); event_manager.handle_next_events(EventMode::Normal);
return *client.exit_status();
} }
catch (disconnected& e) catch (disconnected& e)
{ {
if (!e.m_graceful) write_stderr(format("{}\ndisconnecting\n", e.what()));
write_stderr(format("{}\ndisconnecting\n", e.what())); return -1;
return e.m_graceful ? 0 : -1;
} }
return 0;
} }
struct convert_to_client_mode struct convert_to_client_mode
@ -614,7 +614,8 @@ int run_server(StringView session, StringView server_init,
if (not (flags & ServerFlags::Daemon)) if (not (flags & ServerFlags::Daemon))
{ {
local_client = client_manager.create_client( local_client = client_manager.create_client(
create_local_ui(ui_type), get_env_vars(), client_init, std::move(init_coord)); create_local_ui(ui_type), get_env_vars(), client_init, std::move(init_coord),
[](int status) { local_client_exit = status; });
if (startup_error) if (startup_error)
local_client->print_status({ local_client->print_status({
@ -640,7 +641,7 @@ int run_server(StringView session, StringView server_init,
String buffer_name = local_client->context().buffer().name(); String buffer_name = local_client->context().buffer().name();
String selections = selection_list_to_string(local_client->context().selections()); String selections = selection_list_to_string(local_client->context().selections());
ClientManager::instance().remove_client(*local_client, true); ClientManager::instance().remove_client(*local_client, true, 0);
client_manager.clear_client_trash(); client_manager.clear_client_trash();
convert_to_client_pending = false; convert_to_client_pending = false;
@ -660,7 +661,7 @@ int run_server(StringView session, StringView server_init,
global_scope.hooks().run_hook("KakEnd", "", empty_context); global_scope.hooks().run_hook("KakEnd", "", empty_context);
} }
return 0; return local_client_exit;
} }
int run_filter(StringView keystr, StringView commands, ConstArrayView<StringView> files, bool quiet, StringView suffix_backup) int run_filter(StringView keystr, StringView commands, ConstArrayView<StringView> files, bool quiet, StringView suffix_backup)
@ -743,7 +744,7 @@ int run_pipe(StringView session)
catch (disconnected& e) catch (disconnected& e)
{ {
write_stderr(format("{}\ndisconnecting\n", e.what())); write_stderr(format("{}\ndisconnecting\n", e.what()));
return e.m_graceful ? 0 : -1; return -1;
} }
return 0; return 0;
} }

View File

@ -38,7 +38,8 @@ enum class MessageType : uint8_t
SetCursor, SetCursor,
Refresh, Refresh,
SetOptions, SetOptions,
Key Exit,
Key,
}; };
class MsgWriter class MsgWriter
@ -252,8 +253,7 @@ private:
kak_assert(m_write_pos + size <= m_stream.size()); kak_assert(m_write_pos + size <= m_stream.size());
int res = ::read(sock, m_stream.data() + m_write_pos, size); int res = ::read(sock, m_stream.data() + m_write_pos, size);
if (res <= 0) if (res <= 0)
throw disconnected{res ? format("socket read failed: {}", strerror(errno)) throw disconnected{format("socket read failed: {}", strerror(errno))};
: "peer disconnected", res == 0};
m_write_pos += res; m_write_pos += res;
} }
@ -352,6 +352,8 @@ public:
void set_client(Client* client) { m_client = client; } void set_client(Client* client) { m_client = client; }
Client* client() const { return m_client.get(); } Client* client() const { return m_client.get(); }
void exit(int status);
private: private:
FDWatcher m_socket_watcher; FDWatcher m_socket_watcher;
MsgReader m_reader; MsgReader m_reader;
@ -368,8 +370,7 @@ static bool send_data(int fd, RemoteBuffer& buffer)
{ {
int res = ::write(fd, buffer.data(), buffer.size()); int res = ::write(fd, buffer.data(), buffer.size());
if (res <= 0) if (res <= 0)
throw disconnected{res ? format("socket write failed: {}", strerror(errno)) throw disconnected{format("socket write failed: {}", strerror(errno))};
: "peer disconnected", res == 0};
buffer.erase(buffer.begin(), buffer.begin() + res); buffer.erase(buffer.begin(), buffer.begin() + res);
} }
return buffer.empty(); return buffer.empty();
@ -393,7 +394,7 @@ RemoteUI::RemoteUI(int socket, DisplayCoord dimensions)
if (m_reader.type() != MessageType::Key) if (m_reader.type() != MessageType::Key)
{ {
ClientManager::instance().remove_client(*m_client, false); ClientManager::instance().remove_client(*m_client, false, -1);
return; return;
} }
@ -407,7 +408,7 @@ RemoteUI::RemoteUI(int socket, DisplayCoord dimensions)
catch (const disconnected& err) catch (const disconnected& err)
{ {
write_to_debug_buffer(format("Error while transfering remote messages: {}", err.what())); write_to_debug_buffer(format("Error while transfering remote messages: {}", err.what()));
ClientManager::instance().remove_client(*m_client, false); ClientManager::instance().remove_client(*m_client, false, -1);
} }
}), }),
m_dimensions(dimensions) m_dimensions(dimensions)
@ -417,6 +418,9 @@ RemoteUI::RemoteUI(int socket, DisplayCoord dimensions)
RemoteUI::~RemoteUI() RemoteUI::~RemoteUI()
{ {
// Try to send the remaining data if possible, as it might contain the desired exit status
send_data(m_socket_watcher.fd(), m_send_buffer);
write_to_debug_buffer(format("remote client disconnected: {}", m_socket_watcher.fd())); write_to_debug_buffer(format("remote client disconnected: {}", m_socket_watcher.fd()));
m_socket_watcher.close_fd(); m_socket_watcher.close_fd();
} }
@ -510,6 +514,13 @@ void RemoteUI::set_ui_options(const Options& options)
m_socket_watcher.events() |= FdEvents::Write; m_socket_watcher.events() |= FdEvents::Write;
} }
void RemoteUI::exit(int status)
{
MsgWriter msg{m_send_buffer, MessageType::Exit};
msg.write(status);
m_socket_watcher.events() |= FdEvents::Write;
}
static sockaddr_un session_addr(StringView session) static sockaddr_un session_addr(StringView session)
{ {
sockaddr_un addr; sockaddr_un addr;
@ -637,6 +648,11 @@ RemoteClient::RemoteClient(StringView session, std::unique_ptr<UserInterface>&&
case MessageType::SetOptions: case MessageType::SetOptions:
m_ui->set_ui_options(reader.read_hash_map<String, String, MemoryDomain::Options>()); m_ui->set_ui_options(reader.read_hash_map<String, String, MemoryDomain::Options>());
break; break;
case MessageType::Exit:
m_exit_status = reader.read<int>();
m_socket_watcher->close_fd();
m_socket_watcher.reset();
return; // This lambda is now dead
default: default:
kak_assert(false); kak_assert(false);
} }
@ -697,7 +713,8 @@ private:
auto* ui = new RemoteUI{sock, dimensions}; auto* ui = new RemoteUI{sock, dimensions};
if (auto* client = ClientManager::instance().create_client( if (auto* client = ClientManager::instance().create_client(
std::unique_ptr<UserInterface>(ui), std::unique_ptr<UserInterface>(ui),
std::move(env_vars), init_cmds, init_coord)) std::move(env_vars), init_cmds, init_coord,
[ui](int status) { ui->exit(status); }))
ui->set_client(client); ui->set_client(client);
Server::instance().remove_accepter(this); Server::instance().remove_accepter(this);

View File

@ -13,10 +13,7 @@ namespace Kakoune
struct disconnected : runtime_error struct disconnected : runtime_error
{ {
disconnected(String what, bool graceful = false) using runtime_error::runtime_error;
: runtime_error{std::move(what)}, m_graceful{graceful} {}
const bool m_graceful;
}; };
class FDWatcher; class FDWatcher;
@ -36,10 +33,12 @@ public:
const EnvVarMap& env_vars, StringView init_command, const EnvVarMap& env_vars, StringView init_command,
Optional<BufferCoord> init_coord); Optional<BufferCoord> init_coord);
const Optional<int>& exit_status() const { return m_exit_status; }
private: private:
std::unique_ptr<UserInterface> m_ui; std::unique_ptr<UserInterface> m_ui;
std::unique_ptr<FDWatcher> m_socket_watcher; std::unique_ptr<FDWatcher> m_socket_watcher;
RemoteBuffer m_send_buffer; RemoteBuffer m_send_buffer;
Optional<int> m_exit_status;
}; };
void send_command(StringView session, StringView command); void send_command(StringView session, StringView command);