mirror of
https://git.kernel.org/pub/scm/network/wireless/iwd.git
synced 2024-11-30 06:39:46 +01:00
a327e3f4d8
This fixes up a previous commit which breaks iwctl. The check was added to satisfy static analysis but it ended up preventing iwctl from starting. In this case mkdir can fail (e.g. if the directory already exists) and only if it fails should the history be read. Otherwise a successful mkdir return indicates the history folder is new and there is no reason to try reading it.
763 lines
16 KiB
C
763 lines
16 KiB
C
/*
|
|
*
|
|
* Wireless daemon for Linux
|
|
*
|
|
* Copyright (C) 2017-2019 Intel Corporation. All rights reserved.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#define _GNU_SOURCE
|
|
#include <stdio.h>
|
|
#include <signal.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/ioctl.h>
|
|
#include <unistd.h>
|
|
|
|
#include <readline/history.h>
|
|
#include <readline/readline.h>
|
|
#include <ell/ell.h>
|
|
|
|
#include "client/agent.h"
|
|
#include "client/command.h"
|
|
#include "client/display.h"
|
|
|
|
#define IWD_PROMPT \
|
|
"\001" COLOR_GREEN "\002" "[iwd]" "\001" COLOR_OFF "\002" "# "
|
|
#define LINE_LEN 81
|
|
|
|
static struct l_signal *window_change_signal;
|
|
static struct l_io *io;
|
|
static char dashed_line[LINE_LEN] = { [0 ... LINE_LEN - 2] = '-' };
|
|
static char empty_line[LINE_LEN] = { [0 ... LINE_LEN - 2] = ' ' };
|
|
static struct l_timeout *refresh_timeout;
|
|
static struct saved_input *agent_saved_input;
|
|
|
|
static struct display_refresh {
|
|
bool enabled;
|
|
char *family;
|
|
char *entity;
|
|
const struct command *cmd;
|
|
char **argv;
|
|
int argc;
|
|
size_t undo_lines;
|
|
struct l_queue *redo_entries;
|
|
bool recording;
|
|
} display_refresh = { .enabled = true };
|
|
|
|
struct saved_input {
|
|
char *line;
|
|
int point;
|
|
};
|
|
|
|
static struct saved_input *save_input(void)
|
|
{
|
|
struct saved_input *input;
|
|
|
|
if (RL_ISSTATE(RL_STATE_DONE))
|
|
return NULL;
|
|
|
|
input = l_new(struct saved_input, 1);
|
|
|
|
input->point = rl_point;
|
|
input->line = rl_copy_text(0, rl_end);
|
|
rl_save_prompt();
|
|
rl_replace_line("", 0);
|
|
rl_redisplay();
|
|
|
|
return input;
|
|
}
|
|
|
|
static void restore_input(struct saved_input *input)
|
|
{
|
|
if (!input)
|
|
return;
|
|
|
|
rl_restore_prompt();
|
|
rl_replace_line(input->line, 0);
|
|
rl_point = input->point;
|
|
rl_forced_update_display();
|
|
|
|
l_free(input->line);
|
|
l_free(input);
|
|
}
|
|
|
|
static void display_refresh_undo_lines(void)
|
|
{
|
|
size_t num_lines = display_refresh.undo_lines;
|
|
|
|
printf("\033[%dA", (int) num_lines);
|
|
|
|
do {
|
|
printf("%s\n", empty_line);
|
|
} while (--display_refresh.undo_lines);
|
|
|
|
printf("\033[%dA", (int) num_lines);
|
|
}
|
|
|
|
static void display_refresh_redo_lines(void)
|
|
{
|
|
const struct l_queue_entry *entry;
|
|
struct saved_input *input;
|
|
|
|
input = save_input();
|
|
|
|
for (entry = l_queue_get_entries(display_refresh.redo_entries); entry;
|
|
entry = entry->next) {
|
|
char *line = entry->data;
|
|
|
|
printf("%s", line);
|
|
|
|
display_refresh.undo_lines++;
|
|
}
|
|
|
|
restore_input(input);
|
|
display_refresh.recording = true;
|
|
|
|
l_timeout_modify(refresh_timeout, 1);
|
|
}
|
|
|
|
void display_refresh_reset(void)
|
|
{
|
|
l_free(display_refresh.family);
|
|
display_refresh.family = NULL;
|
|
|
|
l_free(display_refresh.entity);
|
|
display_refresh.entity = NULL;
|
|
|
|
display_refresh.cmd = NULL;
|
|
|
|
l_strfreev(display_refresh.argv);
|
|
display_refresh.argv = NULL;
|
|
display_refresh.argc = 0;
|
|
|
|
display_refresh.undo_lines = 0;
|
|
display_refresh.recording = false;
|
|
|
|
l_queue_clear(display_refresh.redo_entries, l_free);
|
|
}
|
|
|
|
void display_refresh_set_cmd(const char *family, const char *entity,
|
|
const struct command *cmd,
|
|
char **argv, int argc)
|
|
{
|
|
int i;
|
|
|
|
if (cmd->refreshable) {
|
|
l_free(display_refresh.family);
|
|
display_refresh.family = l_strdup(family);
|
|
|
|
l_free(display_refresh.entity);
|
|
display_refresh.entity = l_strdup(entity);
|
|
|
|
display_refresh.cmd = cmd;
|
|
|
|
l_strfreev(display_refresh.argv);
|
|
display_refresh.argc = argc;
|
|
|
|
display_refresh.argv = l_new(char *, argc + 1);
|
|
|
|
for (i = 0; i < argc; i++)
|
|
display_refresh.argv[i] = l_strdup(argv[i]);
|
|
|
|
l_queue_clear(display_refresh.redo_entries, l_free);
|
|
|
|
display_refresh.recording = false;
|
|
display_refresh.undo_lines = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
if (display_refresh.family && family &&
|
|
!strcmp(display_refresh.family, family)) {
|
|
struct l_string *buf = l_string_new(128);
|
|
L_AUTO_FREE_VAR(char *, args);
|
|
char *prompt;
|
|
|
|
for (i = 0; i < argc; i++) {
|
|
bool needs_quotes = false;
|
|
char *p = argv[i];
|
|
|
|
for (p = argv[i]; *p != '\0'; p++) {
|
|
if (*p != ' ')
|
|
continue;
|
|
|
|
needs_quotes = true;
|
|
break;
|
|
}
|
|
|
|
if (needs_quotes)
|
|
l_string_append_printf(buf, "\"%s\" ", argv[i]);
|
|
else
|
|
l_string_append_printf(buf, "%s ", argv[i]);
|
|
}
|
|
|
|
args = l_string_unwrap(buf);
|
|
|
|
prompt = l_strdup_printf(IWD_PROMPT"%s%s%s %s %s\n", family,
|
|
entity ? " " : "",
|
|
entity ? : "",
|
|
cmd->cmd ? : "", args ? : "");
|
|
|
|
l_queue_push_tail(display_refresh.redo_entries, prompt);
|
|
display_refresh.undo_lines++;
|
|
|
|
display_refresh.recording = true;
|
|
} else {
|
|
display_refresh_reset();
|
|
}
|
|
}
|
|
|
|
static void display_refresh_check_feasibility(void)
|
|
{
|
|
const struct winsize ws;
|
|
|
|
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
|
|
|
|
if (ws.ws_col < LINE_LEN - 1) {
|
|
if (display_refresh.enabled) {
|
|
display_refresh.recording = false;
|
|
display(COLOR_YELLOW "Auto-refresh is disabled. "
|
|
"Enlarge window width to at least %u to enable."
|
|
"\n" COLOR_OFF, LINE_LEN - 1);
|
|
display_refresh.recording = true;
|
|
}
|
|
|
|
display_refresh.enabled = false;
|
|
} else {
|
|
display_refresh.enabled = true;
|
|
}
|
|
}
|
|
|
|
static void display_refresh_check_applicability(void)
|
|
{
|
|
if (display_refresh.enabled && display_refresh.cmd)
|
|
display_refresh_redo_lines();
|
|
else if (display_refresh.cmd)
|
|
display_refresh_timeout_set();
|
|
}
|
|
|
|
static void timeout_callback(struct l_timeout *timeout, void *user_data)
|
|
{
|
|
struct saved_input *input;
|
|
|
|
if (!display_refresh.enabled || !display_refresh.cmd) {
|
|
if (display_refresh.cmd)
|
|
display_refresh_timeout_set();
|
|
|
|
return;
|
|
}
|
|
|
|
input = save_input();
|
|
display_refresh_undo_lines();
|
|
restore_input(input);
|
|
|
|
display_refresh.recording = false;
|
|
display_refresh.cmd->function(display_refresh.entity,
|
|
display_refresh.argv,
|
|
display_refresh.argc);
|
|
}
|
|
|
|
void display_refresh_timeout_set(void)
|
|
{
|
|
if (refresh_timeout)
|
|
l_timeout_modify(refresh_timeout, 1);
|
|
else
|
|
refresh_timeout = l_timeout_create(1, timeout_callback,
|
|
NULL, NULL);
|
|
}
|
|
|
|
static void display_text(const char *text)
|
|
{
|
|
struct saved_input *input = save_input();
|
|
|
|
printf("%s", text);
|
|
|
|
restore_input(input);
|
|
|
|
if (!display_refresh.cmd)
|
|
return;
|
|
|
|
display_refresh.undo_lines++;
|
|
|
|
if (display_refresh.recording)
|
|
l_queue_push_tail(display_refresh.redo_entries, l_strdup(text));
|
|
}
|
|
|
|
void display(const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
char *text;
|
|
|
|
va_start(args, fmt);
|
|
text = l_strdup_vprintf(fmt, args);
|
|
va_end(args);
|
|
|
|
display_text(text);
|
|
|
|
l_free(text);
|
|
}
|
|
|
|
void display_error(const char *error)
|
|
{
|
|
char *text = l_strdup_printf(COLOR_RED "%s" COLOR_OFF "\n", error);
|
|
|
|
display_text(text);
|
|
|
|
l_free(text);
|
|
}
|
|
|
|
static char get_flasher(void)
|
|
{
|
|
static char c;
|
|
|
|
if (c == ' ')
|
|
c = '*';
|
|
else
|
|
c = ' ';
|
|
|
|
return c;
|
|
}
|
|
|
|
void display_table_header(const char *caption, const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
char *text;
|
|
char *body;
|
|
int caption_pos =
|
|
(int) ((sizeof(dashed_line) - 1) / 2 + strlen(caption) / 2);
|
|
|
|
text = l_strdup_printf("%*s" COLOR_BOLDGRAY "%*c" COLOR_OFF "\n",
|
|
caption_pos, caption,
|
|
LINE_LEN - 2 - caption_pos,
|
|
display_refresh.cmd ? get_flasher() : ' ');
|
|
display_text(text);
|
|
l_free(text);
|
|
|
|
text = l_strdup_printf("%s%s%s\n", COLOR_GRAY, dashed_line, COLOR_OFF);
|
|
display_text(text);
|
|
l_free(text);
|
|
|
|
va_start(args, fmt);
|
|
text = l_strdup_vprintf(fmt, args);
|
|
va_end(args);
|
|
|
|
body = l_strdup_printf("%s%s%s\n", COLOR_BOLDGRAY, text, COLOR_OFF);
|
|
display_text(body);
|
|
l_free(body);
|
|
l_free(text);
|
|
|
|
text = l_strdup_printf("%s%s%s\n", COLOR_GRAY, dashed_line, COLOR_OFF);
|
|
display_text(text);
|
|
l_free(text);
|
|
}
|
|
|
|
void display_table_footer(void)
|
|
{
|
|
display_text("\n");
|
|
|
|
display_refresh_check_applicability();
|
|
}
|
|
|
|
void display_command_line(const char *command_family,
|
|
const struct command *cmd)
|
|
{
|
|
char *cmd_line = l_strdup_printf("%s%s%s%s%s%s%s",
|
|
command_family ? : "",
|
|
command_family ? " " : "",
|
|
cmd->entity ? : "",
|
|
cmd->entity ? " " : "",
|
|
cmd->cmd,
|
|
cmd->arg ? " " : "",
|
|
cmd->arg ? : "");
|
|
|
|
display(MARGIN "%-*s%s\n", 50, cmd_line, cmd->desc ? : "");
|
|
|
|
l_free(cmd_line);
|
|
}
|
|
|
|
static void display_completion_matches(char **matches, int num_matches,
|
|
int max_length)
|
|
{
|
|
char *prompt;
|
|
char *entry;
|
|
char line[LINE_LEN];
|
|
size_t index;
|
|
size_t line_used;
|
|
char *input = rl_copy_text(0, rl_end);
|
|
|
|
prompt = l_strdup_printf("%s%s\n", IWD_PROMPT, input);
|
|
l_free(input);
|
|
|
|
display_text(prompt);
|
|
l_free(prompt);
|
|
|
|
for (index = 1, line_used = 0; matches[index]; index++) {
|
|
if ((line_used + max_length) > LINE_LEN) {
|
|
strcpy(&line[line_used], "\n");
|
|
|
|
display_text(line);
|
|
|
|
line_used = 0;
|
|
}
|
|
|
|
entry = l_strdup_printf("%-*s ", max_length, matches[index]);
|
|
l_strlcpy(&line[line_used], entry, sizeof(line) - line_used);
|
|
l_free(entry);
|
|
|
|
line_used += max_length + 1;
|
|
}
|
|
|
|
strcpy(&line[line_used], "\n");
|
|
|
|
display_text(line);
|
|
}
|
|
|
|
#define MAX_PASSPHRASE_LEN 63
|
|
|
|
static struct masked_input {
|
|
bool use_mask;
|
|
char passphrase[MAX_PASSPHRASE_LEN + 1];
|
|
uint8_t point;
|
|
uint8_t end;
|
|
} masked_input;
|
|
|
|
static void mask_input(void)
|
|
{
|
|
if (!masked_input.use_mask)
|
|
return;
|
|
|
|
if (rl_end > MAX_PASSPHRASE_LEN) {
|
|
rl_end = MAX_PASSPHRASE_LEN;
|
|
rl_point = masked_input.point;
|
|
|
|
goto set_mask;
|
|
}
|
|
|
|
if (masked_input.end == rl_end) {
|
|
/* Moving cursor. */
|
|
goto done;
|
|
} else if (masked_input.end < rl_end) {
|
|
/* Insertion. */
|
|
memcpy(masked_input.passphrase + rl_point,
|
|
masked_input.passphrase + masked_input.point,
|
|
masked_input.end - masked_input.point);
|
|
memcpy(masked_input.passphrase + masked_input.point,
|
|
rl_line_buffer + masked_input.point,
|
|
rl_point - masked_input.point);
|
|
} else {
|
|
/* Deletion. */
|
|
if (masked_input.point > rl_point)
|
|
/* Backspace key. */
|
|
memcpy(masked_input.passphrase + rl_point,
|
|
masked_input.passphrase + masked_input.point,
|
|
masked_input.end - masked_input.point);
|
|
else
|
|
/* Delete key. */
|
|
memcpy(masked_input.passphrase + rl_point,
|
|
masked_input.passphrase + masked_input.point
|
|
+ 1,
|
|
rl_end - rl_point);
|
|
memset(masked_input.passphrase + rl_end, 0,
|
|
masked_input.end - rl_end);
|
|
}
|
|
|
|
set_mask:
|
|
memset(rl_line_buffer, '*', rl_end);
|
|
rl_line_buffer[rl_end] = '\0';
|
|
|
|
rl_redisplay();
|
|
|
|
masked_input.end = rl_end;
|
|
done:
|
|
masked_input.point = rl_point;
|
|
}
|
|
|
|
static void reset_masked_input(void)
|
|
{
|
|
memset(masked_input.passphrase, 0, MAX_PASSPHRASE_LEN + 1);
|
|
masked_input.point = 0;
|
|
masked_input.end = 0;
|
|
}
|
|
|
|
static void readline_callback(char *prompt)
|
|
{
|
|
char **argv;
|
|
int argc;
|
|
|
|
HIST_ENTRY *previous_prompt;
|
|
|
|
if (agent_prompt(masked_input.use_mask ?
|
|
masked_input.passphrase : prompt))
|
|
goto done;
|
|
|
|
if (!prompt) {
|
|
display_quit();
|
|
|
|
l_main_quit();
|
|
|
|
return;
|
|
}
|
|
|
|
if (!strlen(prompt))
|
|
goto done;
|
|
|
|
previous_prompt = history_get(history_base + history_length - 1);
|
|
if (!previous_prompt || strcmp(previous_prompt->line, prompt)) {
|
|
add_history(prompt);
|
|
}
|
|
|
|
argv = l_parse_args(prompt, &argc);
|
|
if (!argv) {
|
|
display("Invalid command\n");
|
|
goto done;
|
|
}
|
|
|
|
command_process_prompt(argv, argc);
|
|
|
|
l_strfreev(argv);
|
|
done:
|
|
l_free(prompt);
|
|
}
|
|
|
|
bool display_agent_is_active(void)
|
|
{
|
|
if (agent_saved_input)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool read_handler(struct l_io *io, void *user_data)
|
|
{
|
|
rl_callback_read_char();
|
|
|
|
if (display_agent_is_active() || !command_is_interactive_mode())
|
|
mask_input();
|
|
|
|
return true;
|
|
}
|
|
|
|
static void disconnect_callback(struct l_io *io, void *user_data)
|
|
{
|
|
l_main_quit();
|
|
}
|
|
|
|
void display_enable_cmd_prompt(void)
|
|
{
|
|
if (!io)
|
|
io = l_io_new(fileno(stdin));
|
|
|
|
l_io_set_read_handler(io, read_handler, NULL, NULL);
|
|
l_io_set_disconnect_handler(io, disconnect_callback, NULL, NULL);
|
|
|
|
rl_set_prompt(IWD_PROMPT);
|
|
|
|
/*
|
|
* The following sequence of rl_* commands forces readline to properly
|
|
* update its internal state and re-display the new prompt.
|
|
*/
|
|
rl_save_prompt();
|
|
rl_redisplay();
|
|
rl_restore_prompt();
|
|
rl_forced_update_display();
|
|
}
|
|
|
|
void display_disable_cmd_prompt(void)
|
|
{
|
|
display_refresh_reset();
|
|
|
|
rl_set_prompt("Waiting for IWD to start...");
|
|
printf("\r");
|
|
rl_on_new_line();
|
|
rl_redisplay();
|
|
}
|
|
|
|
void display_agent_prompt(const char *label, bool mask_input)
|
|
{
|
|
char *prompt;
|
|
|
|
masked_input.use_mask = mask_input;
|
|
|
|
if (mask_input)
|
|
reset_masked_input();
|
|
|
|
prompt = l_strdup_printf(COLOR_BLUE "%s " COLOR_OFF, label);
|
|
|
|
if (command_is_interactive_mode()) {
|
|
if (agent_saved_input) {
|
|
l_free(prompt);
|
|
return;
|
|
}
|
|
|
|
agent_saved_input = l_new(struct saved_input, 1);
|
|
|
|
agent_saved_input->point = rl_point;
|
|
agent_saved_input->line = rl_copy_text(0, rl_end);
|
|
rl_set_prompt("");
|
|
rl_replace_line("", 0);
|
|
rl_redisplay();
|
|
|
|
rl_erase_empty_line = 0;
|
|
rl_set_prompt(prompt);
|
|
} else {
|
|
rl_callback_handler_install(prompt, readline_callback);
|
|
|
|
if (!io)
|
|
io = l_io_new(fileno(stdin));
|
|
|
|
l_io_set_read_handler(io, read_handler, NULL, NULL);
|
|
}
|
|
|
|
l_free(prompt);
|
|
|
|
rl_redisplay();
|
|
}
|
|
|
|
void display_agent_prompt_release(const char *label)
|
|
{
|
|
if (!command_is_interactive_mode()) {
|
|
rl_callback_handler_remove();
|
|
l_io_destroy(io);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!agent_saved_input)
|
|
return;
|
|
|
|
if (display_refresh.cmd) {
|
|
char *text = rl_copy_text(0, rl_end);
|
|
char *prompt = l_strdup_printf(COLOR_BLUE "%s " COLOR_OFF
|
|
"%s\n", label, text);
|
|
l_free(text);
|
|
|
|
l_queue_push_tail(display_refresh.redo_entries, prompt);
|
|
display_refresh.undo_lines++;
|
|
}
|
|
|
|
rl_erase_empty_line = 1;
|
|
|
|
rl_replace_line(agent_saved_input->line, 0);
|
|
rl_point = agent_saved_input->point;
|
|
|
|
l_free(agent_saved_input->line);
|
|
l_free(agent_saved_input);
|
|
agent_saved_input = NULL;
|
|
|
|
rl_set_prompt(IWD_PROMPT);
|
|
}
|
|
|
|
void display_quit(void)
|
|
{
|
|
rl_crlf();
|
|
}
|
|
|
|
static void window_change_signal_handler(void *user_data)
|
|
{
|
|
display_refresh_check_feasibility();
|
|
}
|
|
|
|
static char *history_path;
|
|
|
|
void display_init(void)
|
|
{
|
|
const char *data_home;
|
|
char *data_path;
|
|
|
|
display_refresh.redo_entries = l_queue_new();
|
|
|
|
stifle_history(24);
|
|
|
|
data_home = getenv("XDG_DATA_HOME");
|
|
if (!data_home || *data_home != '/') {
|
|
const char *home_path;
|
|
|
|
home_path = getenv("HOME");
|
|
if (home_path)
|
|
data_path = l_strdup_printf("%s/%s/iwctl",
|
|
home_path, ".local/share");
|
|
else
|
|
data_path = NULL;
|
|
} else {
|
|
data_path = l_strdup_printf("%s/iwctl", data_home);
|
|
}
|
|
|
|
if (data_path) {
|
|
/*
|
|
* If mkdir succeeds that means its a new directory, no need
|
|
* to read the history since it doesn't exist
|
|
*/
|
|
if (mkdir(data_path, 0700) != 0) {
|
|
history_path = l_strdup_printf("%s/history", data_path);
|
|
read_history(history_path);
|
|
}
|
|
|
|
l_free(data_path);
|
|
} else {
|
|
history_path = NULL;
|
|
}
|
|
|
|
setlinebuf(stdout);
|
|
|
|
window_change_signal =
|
|
l_signal_create(SIGWINCH, window_change_signal_handler, NULL,
|
|
NULL);
|
|
|
|
rl_attempted_completion_function = command_completion;
|
|
rl_completion_display_matches_hook = display_completion_matches;
|
|
|
|
rl_completer_quote_characters = "\"";
|
|
rl_erase_empty_line = 1;
|
|
rl_callback_handler_install("Waiting for IWD to start...",
|
|
readline_callback);
|
|
|
|
rl_redisplay();
|
|
|
|
display_refresh_check_feasibility();
|
|
}
|
|
|
|
void display_exit(void)
|
|
{
|
|
if (agent_saved_input) {
|
|
l_free(agent_saved_input->line);
|
|
l_free(agent_saved_input);
|
|
agent_saved_input = NULL;
|
|
}
|
|
|
|
l_timeout_remove(refresh_timeout);
|
|
refresh_timeout = NULL;
|
|
|
|
l_queue_destroy(display_refresh.redo_entries, l_free);
|
|
|
|
rl_callback_handler_remove();
|
|
|
|
l_io_destroy(io);
|
|
|
|
l_signal_remove(window_change_signal);
|
|
|
|
if (history_path)
|
|
write_history(history_path);
|
|
|
|
l_free(history_path);
|
|
|
|
display_quit();
|
|
}
|