/*
* Copyright 1993, 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004 by Paul
* Mattes.
* Permission to use, copy, modify, and distribute this software and its
* documentation for any purpose and without fee is hereby granted,
* provided that the above copyright notice appear in all copies and that
* both that copyright notice and this permission notice appear in
* supporting documentation.
*
* x3270, c3270, s3270 and tcl3270 are distributed in the hope that they will
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file LICENSE
* for more details.
*/
/*
* host.c
* This module handles the ibm_hosts file, connecting to and
* disconnecting from hosts, and state changes on the host
* connection.
*/
#include "globals.h"
#include "appres.h"
#include "resources.h"
#include "actionsc.h"
#include "hostc.h"
#include "macrosc.h"
#include "menubarc.h"
#include "popupsc.h"
#include "telnetc.h"
#include "trace_dsc.h"
#include "utilc.h"
#include "xioc.h"
#include <errno.h>
#define RECONNECT_MS 2000 /* 2 sec before reconnecting to host */
#define RECONNECT_ERR_MS 5000 /* 5 sec before reconnecting to host */
#define MAX_RECENT 5
enum cstate cstate = NOT_CONNECTED;
Boolean std_ds_host = False;
Boolean no_login_host = False;
Boolean non_tn3270e_host = False;
Boolean passthru_host = False;
Boolean ssl_host = False;
#define LUNAME_SIZE 16
char luname[LUNAME_SIZE+1];
char *connected_lu = CN;
char *connected_type = CN;
Boolean ever_3270 = False;
char *current_host = CN;
char *full_current_host = CN;
unsigned short current_port;
char *reconnect_host = CN;
char *qualified_host = CN;
struct host *hosts = (struct host *)NULL;
static struct host *last_host = (struct host *)NULL;
static Boolean auto_reconnect_inprogress = False;
static int net_sock = -1;
#if defined(X3270_DISPLAY) /*[*/
static void save_recent(const char *);
static void try_reconnect(void);
#endif /*]*/
static char *
stoken(char **s)
{
char *r;
char *ss = *s;
if (!*ss)
return NULL;
r = ss;
while (*ss && *ss != ' ' && *ss != '\t')
ss++;
if (*ss) {
*ss++ = '\0';
while (*ss == ' ' || *ss == '\t')
ss++;
}
*s = ss;
return r;
}
/*
* Read the host file
*/
void
hostfile_init(void)
{
FILE *hf;
char buf[1024];
static Boolean hostfile_initted = False;
struct host *h;
char *hostfile_name;
if (hostfile_initted)
return;
hostfile_initted = True;
hostfile_name = appres.hostsfile;
if (hostfile_name == CN)
hostfile_name = xs_buffer("%s/ibm_hosts", appres.conf_dir);
else
hostfile_name = do_subst(appres.hostsfile, True, True);
hf = fopen(hostfile_name, "r");
if (hf != (FILE *)NULL) {
while (fgets(buf, sizeof(buf), hf)) {
char *s = buf;
char *name, *entry_type, *hostname;
char *slash;
if (strlen(buf) > (unsigned)1 &&
buf[strlen(buf) - 1] == '\n') {
buf[strlen(buf) - 1] = '\0';
}
while (isspace(*s))
s++;
if (!*s || *s == '#')
continue;
name = stoken(&s);
entry_type = stoken(&s);
hostname = stoken(&s);
if (!name || !entry_type || !hostname) {
popup_an_error("Bad %s syntax, entry skipped",
ResHostsFile);
continue;
}
h = (struct host *)Malloc(sizeof(*h));
if (!split_hier(NewString(name), &h->name,
&h->parents)) {
Free(h);
continue;
}
h->hostname = NewString(hostname);
/*
* Quick syntax extension to allow the hosts file to
* specify a port as host/port.
*/
if ((slash = strchr(h->hostname, '/')))
*slash = ':';
if (!strcmp(entry_type, "primary"))
h->entry_type = PRIMARY;
else
h->entry_type = ALIAS;
if (*s)
h->loginstring = NewString(s);
else
h->loginstring = CN;
h->prev = last_host;
h->next = (struct host *)NULL;
if (last_host)
last_host->next = h;
else
hosts = h;
last_host = h;
}
(void) fclose(hf);
} else if (appres.hostsfile != CN) {
popup_an_errno(errno, "Cannot open " ResHostsFile " '%s'",
appres.hostsfile);
}
Free(hostfile_name);
#if defined(X3270_DISPLAY) /*[*/
/*
* Read the recent-connection file, and prepend it to the hosts list.
*/
save_recent(CN);
#endif /*]*/
}
/*
* Look up a host in the list. Turns aliases into real hostnames, and
* finds loginstrings.
*/
static int
hostfile_lookup(const char *name, char **hostname, char **loginstring)
{
struct host *h;
hostfile_init();
for (h = hosts; h != (struct host *)NULL; h = h->next) {
if (h->entry_type == RECENT)
continue;
if (!strcmp(name, h->name)) {
*hostname = h->hostname;
*loginstring = h->loginstring;
return 1;
}
}
return 0;
}
#if defined(LOCAL_PROCESS) /*[*/
/* Recognize and translate "-e" options. */
static const char *
parse_localprocess(const char *s)
{
int sl = strlen(OptLocalProcess);
if (!strncmp(s, OptLocalProcess, sl)) {
if (s[sl] == ' ')
return(s + sl + 1);
else if (s[sl] == '\0') {
char *r;
r = getenv("SHELL");
if (r != CN)
return r;
else
return "/bin/sh";
}
}
return CN;
}
#endif /*]*/
/*
* Strip qualifiers from a hostname.
* Returns the hostname part in a newly-malloc'd string.
* 'needed' is returned True if anything was actually stripped.
* Returns NULL if there is a syntax error.
*/
static char *
split_host(char *s, Boolean *ansi, Boolean *std_ds, Boolean *passthru,
Boolean *non_e, Boolean *secure, Boolean *no_login, char *xluname,
char **port, Boolean *needed)
{
char *lbracket = CN;
char *at = CN;
char *r = NULL;
Boolean colon = False;
*ansi = False;
*std_ds = False;
*passthru = False;
*non_e = False;
*secure = False;
*xluname = '\0';
*port = CN;
*needed = False;
/*
* Hostname syntax is:
* Zero or more optional prefixes (A:, S:, P:, N:, L:, C:).
* An optional LU name separated by '@'.
* A hostname optionally in square brackets (which quote any colons
* in the name).
* An optional port name or number separated from the hostname by a
* space or colon.
* No additional white space or colons are allowed.
*/
/* Strip leading whitespace. */
while (*s && isspace(*s))
s++;
if (!*s) {
popup_an_error("Empty hostname");
goto split_fail;
}
/* Strip trailing whitespace. */
while (isspace(*(s + strlen(s) - 1)))
*(s + strlen(s) - 1) = '\0';
/* Start with the prefixes. */
while (*s && *(s + 1) && isalpha(*s) && *(s + 1) == ':') {
switch (*s) {
case 'a':
case 'A':
*ansi = True;
break;
case 's':
case 'S':
*std_ds = True;
break;
case 'p':
case 'P':
*passthru = True;
break;
case 'n':
case 'N':
*non_e = True;
break;
#if defined(HAVE_LIBSSL) /*[*/
case 'l':
case 'L':
*secure = True;
break;
#endif /*]*/
case 'c':
case 'C':
*no_login = True;
break;
default:
popup_an_error("Hostname syntax error:\n"
"Option '%c:' not supported", *s);
goto split_fail;
}
*needed = True;
s += 2;
/* Allow whitespace around the prefixes. */
while (*s && isspace(*s))
s++;
}
/* Process the LU name. */
lbracket = strchr(s, '[');
at = strchr(s, '@');
if (at != CN && lbracket != CN && at > lbracket)
at = CN;
if (at != CN) {
char *t;
char *lu_last = at - 1;
if (at == s) {
popup_an_error("Hostname syntax error:\n"
"Empty LU name");
goto split_fail;
}
while (lu_last < s && isspace(*lu_last))
lu_last--;
for (t = s; t <= lu_last; t++) {
if (isspace(*t)) {
char *u = t + 1;
while (isspace(*u))
u++;
if (*u != '@') {
popup_an_error("Hostname syntax "
"error:\n"
"Space in LU name");
goto split_fail;
}
break;
}
if (t - s < LUNAME_SIZE) {
xluname[t - s] = *t;
}
}
xluname[t - s] = '\0';
s = at + 1;
while (*s && isspace(*s))
s++;
*needed = True;
}
/*
* Isolate the hostname.
* At this point, we've found its start, so we can malloc the buffer
* that will hold the copy.
*/
if (lbracket != CN) {
char *rbracket;
/* Check for junk before the '['. */
if (lbracket != s) {
popup_an_error("Hostname syntax error:\n"
"Text before '['");
goto split_fail;
}
s = r = NewString(lbracket + 1);
/*
* Take whatever is inside square brackets, including
* whitespace, unmodified -- except for empty strings.
*/
rbracket = strchr(s, ']');
if (rbracket == CN) {
popup_an_error("Hostname syntax error:\n"
"Missing ']'");
goto split_fail;
}
if (rbracket == s) {
popup_an_error("Empty hostname");
goto split_fail;
}
*rbracket = '\0';
/* Skip over any whitespace after the bracketed name. */
s = rbracket + 1;
while (*s && isspace(*s))
s++;
if (!*s)
goto split_success;
colon = (*s == ':');
} else {
char *name_end;
/* Check for an empty string. */
if (!*s || *s == ':') {
popup_an_error("Empty hostname");
goto split_fail;
}
s = r = NewString(s);
/* Find the end of the hostname. */
while (*s && !isspace(*s) && *s != ':')
s++;
name_end = s;
/* If the terminator is whitespace, skip the rest of it. */
while (*s && isspace(*s))
s++;
/*
* If there's nothing but whitespace (or nothing) after the
* name, we're done.
*/
if (*s == '\0') {
*name_end = '\0';
goto split_success;
}
colon = (*s == ':');
*name_end = '\0';
}
/*
* If 'colon' is set, 's' points at it (or where it was). Skip
* it and any whitespace that follows.
*/
if (colon) {
s++;
while (*s && isspace(*s))
s++;
if (!*s) {
popup_an_error("Hostname syntax error:\n"
"Empty port name");
goto split_fail;
}
}
/*
* Set the portname and find its end.
* Note that trailing spaces were already stripped, so the end of the
* portname must be a NULL.
*/
*port = s;
*needed = True;
while (*s && !isspace(*s) && *s != ':')
s++;
if (*s != '\0') {
popup_an_error("Hostname syntax error:\n"
"Multiple port names");
goto split_fail;
}
goto split_success;
split_fail:
Free(r);
r = CN;
split_success:
return r;
}
/*
* Network connect/disconnect operations, combined with X input operations.
*
* Returns 0 for success, -1 for error.
* Sets 'reconnect_host', 'current_host' and 'full_current_host' as
* side-effects.
*/
int
host_connect(const char *n)
{
char nb[2048]; /* name buffer */
char *s; /* temporary */
const char *chost; /* to whom we will connect */
char *target_name;
char *ps = CN;
char *port = CN;
Boolean resolving;
Boolean pending;
static Boolean ansi_host;
const char *localprocess_cmd = NULL;
Boolean has_colons = False;
if (CONNECTED || auto_reconnect_inprogress)
return 0;
/* Skip leading blanks. */
while (*n == ' ')
n++;
if (!*n) {
popup_an_error("Invalid (empty) hostname");
return -1;
}
/* Save in a modifiable buffer. */
(void) strcpy(nb, n);
/* Strip trailing blanks. */
s = nb + strlen(nb) - 1;
while (*s == ' ')
*s-- = '\0';
/* Remember this hostname, as the last hostname we connected to. */
Replace(reconnect_host, NewString(nb));
#if defined(X3270_DISPLAY) /*[*/
/* Remember this hostname in the recent connection list and file. */
save_recent(nb);
#endif /*]*/
#if defined(LOCAL_PROCESS) /*[*/
if ((localprocess_cmd = parse_localprocess(nb)) != CN) {
chost = localprocess_cmd;
port = appres.port;
} else
#endif /*]*/
{
Boolean needed;
/* Strip off and remember leading qualifiers. */
if ((s = split_host(nb, &ansi_host, &std_ds_host,
&passthru_host, &non_tn3270e_host, &ssl_host,
&no_login_host, luname, &port,
&needed)) == CN)
return -1;
/* Look up the name in the hosts file. */
if (!needed && hostfile_lookup(s, &target_name, &ps)) {
/*
* Rescan for qualifiers.
* Qualifiers, LU names, and ports are all overridden
* by the hosts file.
*/
Free(s);
if (!(s = split_host(target_name, &ansi_host,
&std_ds_host, &passthru_host, &non_tn3270e_host,
&ssl_host, &no_login_host, luname, &port,
&needed)))
return -1;
}
chost = s;
/* Default the port. */
if (port == CN)
port = appres.port;
}
/*
* Store the original name in globals, even if we fail the connect
* later:
* current_host is the hostname part, stripped of qualifiers, luname
* and port number
* full_current_host is the entire string, for use in reconnecting
*/
if (n != full_current_host) {
Replace(full_current_host, NewString(n));
}
Replace(current_host, CN);
if (localprocess_cmd != CN) {
if (full_current_host[strlen(OptLocalProcess)] != '\0')
current_host = NewString(full_current_host +
strlen(OptLocalProcess) + 1);
else
current_host = NewString("default shell");
} else {
current_host = s;
}
has_colons = (strchr(chost, ':') != NULL);
Replace(qualified_host,
xs_buffer("%s%s%s%s:%s",
ssl_host? "L:": "",
has_colons? "[": "",
chost,
has_colons? "]": "",
port));
/* Attempt contact. */
ever_3270 = False;
net_sock = net_connect(chost, port, localprocess_cmd != CN, &resolving,
&pending);
if (net_sock < 0 && !resolving) {
#if defined(X3270_DISPLAY) /*[*/
if (appres.once) {
/* Exit when the error pop-up pops down. */
exiting = True;
}
else if (appres.reconnect) {
auto_reconnect_inprogress = True;
(void) AddTimeOut(RECONNECT_ERR_MS, try_reconnect);
}
#endif /*]*/
/* Redundantly signal a disconnect. */
st_changed(ST_CONNECT, False);
return -1;
}
/* Still thinking about it? */
if (resolving) {
cstate = RESOLVING;
st_changed(ST_RESOLVING, True);
return 0;
}
/* Success. */
/* Set pending string. */
if (ps != CN)
login_macro(ps);
/* Prepare Xt for I/O. */
x_add_input(net_sock);
/* Set state and tell the world. */
if (pending) {
cstate = PENDING;
st_changed(ST_HALF_CONNECT, True);
} else {
cstate = CONNECTED_INITIAL;
st_changed(ST_CONNECT, True);
#if defined(X3270_DISPLAY) /*[*/
if (appres.reconnect && error_popup_visible())
popdown_an_error();
#endif /*]*/
}
return 0;
}
#if defined(X3270_DISPLAY) /*[*/
/*
* Reconnect to the last host.
*/
static void
host_reconnect(void)
{
if (auto_reconnect_inprogress || current_host == CN ||
CONNECTED || HALF_CONNECTED)
return;
if (host_connect(reconnect_host) >= 0)
auto_reconnect_inprogress = False;
}
/*
* Called from timer to attempt an automatic reconnection.
*/
static void
try_reconnect(void)
{
auto_reconnect_inprogress = False;
host_reconnect();
}
#endif /*]*/
void
host_disconnect(Boolean failed)
{
if (CONNECTED || HALF_CONNECTED) {
x_remove_input();
net_disconnect();
net_sock = -1;
#if defined(X3270_DISPLAY) /*[*/
if (appres.once) {
if (error_popup_visible()) {
/*
* If there is an error pop-up, exit when it
* pops down.
*/
exiting = True;
} else {
/* Exit now. */
x3270_exit(0);
return;
}
} else if (appres.reconnect && !auto_reconnect_inprogress) {
/* Schedule an automatic reconnection. */
auto_reconnect_inprogress = True;
(void) AddTimeOut(failed? RECONNECT_ERR_MS:
RECONNECT_MS,
try_reconnect);
}
#endif /*]*/
/*
* Remember a disconnect from ANSI mode, to keep screen tracing
* in sync.
*/
#if defined(X3270_TRACE) /*[*/
if (IN_ANSI && toggled(SCREEN_TRACE))
trace_ansi_disc();
#endif /*]*/
cstate = NOT_CONNECTED;
/* Propagate the news to everyone else. */
st_changed(ST_CONNECT, False);
}
}
/* The host has entered 3270 or ANSI mode, or switched between them. */
void
host_in3270(enum cstate new_cstate)
{
Boolean now3270 = (new_cstate == CONNECTED_3270 ||
new_cstate == CONNECTED_SSCP ||
new_cstate == CONNECTED_TN3270E);
cstate = new_cstate;
ever_3270 = now3270;
st_changed(ST_3270_MODE, now3270);
}
void
host_connected(void)
{
cstate = CONNECTED_INITIAL;
st_changed(ST_CONNECT, True);
#if defined(X3270_DISPLAY) /*[*/
if (appres.reconnect && error_popup_visible())
popdown_an_error();
#endif /*]*/
}
#if defined(X3270_DISPLAY) /*[*/
/* Comparison function for the qsort. */
static int
host_compare(const void *e1, const void *e2)
{
const struct host *h1 = *(const struct host **)e1;
const struct host *h2 = *(const struct host **)e2;
int r;
if (h1->connect_time > h2->connect_time)
r = -1;
else if (h1->connect_time < h2->connect_time)
r = 1;
else
r = 0;
#if defined(CFDEBUG) /*[*/
printf("%s %ld %d %s %ld\n",
h1->name, h1->connect_time,
r,
h2->name, h2->connect_time);
#endif /*]*/
return r;
}
#endif /*]*/
#if defined(CFDEBUG) /*[*/
static void
dump_array(const char *when, struct host **array, int nh)
{
int i;
printf("%s\n", when);
for (i = 0; i < nh; i++) {
printf(" %15s %ld\n", array[i]->name, array[i]->connect_time);
}
}
#endif /*]*/
#if defined(X3270_DISPLAY) /*[*/
/* Save the most recent host in the recent host list. */
static void
save_recent(const char *hn)
{
char *lcf_name = CN;
FILE *lcf = (FILE *)NULL;
struct host *h;
struct host *rest = (struct host *)NULL;
int n_ent = 0;
struct host *h_array[(MAX_RECENT * 2) + 1];
int nh = 0;
int i, j;
time_t t = time((time_t *)NULL);
/* Allocate a new entry. */
if (hn != CN) {
h = (struct host *)Malloc(sizeof(*h));
h->name = NewString(hn);
h->parents = NULL;
h->hostname = NewString(hn);
h->entry_type = RECENT;
h->loginstring = CN;
h->connect_time = t;
h_array[nh++] = h;
}
/* Put the existing entries into the array. */
for (h = hosts; h != (struct host *)NULL; h = h->next) {
if (h->entry_type != RECENT)
break;
h_array[nh++] = h;
}
/* Save the ibm_hosts entries for later. */
rest = h;
if (rest != (struct host *)NULL)
rest->prev = (struct host *)NULL;
/*
* Read the last-connection file, to capture the any changes made by
* other instances of x3270.
*/
if (appres.connectfile_name != CN &&
strcasecmp(appres.connectfile_name, "none")) {
lcf_name = do_subst(appres.connectfile_name, True, True);
lcf = fopen(lcf_name, "r");
}
if (lcf != (FILE *)NULL) {
char buf[1024];
while (fgets(buf, sizeof(buf), lcf) != CN) {
int sl;
time_t connect_time;
char *ptr;
/* Pick apart the entry. */
sl = strlen(buf);
if (buf[sl - 1] == '\n')
buf[sl-- - 1] = '\0';
if (!sl ||
buf[0] == '#' ||
(connect_time = strtoul(buf, &ptr, 10)) == 0L ||
ptr == buf ||
*ptr != ' ' ||
!*(ptr + 1))
continue;
h = (struct host *)Malloc(sizeof(*h));
h->name = NewString(ptr + 1);
h->parents = NULL;
h->hostname = NewString(ptr + 1);
h->entry_type = RECENT;
h->loginstring = CN;
h->connect_time = connect_time;
h_array[nh++] = h;
if (nh > (MAX_RECENT * 2) + 1)
break;
}
fclose(lcf);
}
/* Sort the array, in reverse order by connect time. */
#if defined(CFDEBUG) /*[*/
dump_array("before", h_array, nh);
#endif /*]*/
qsort(h_array, nh, sizeof(struct host *), host_compare);
#if defined(CFDEBUG) /*[*/
dump_array("after", h_array, nh);
#endif /*]*/
/*
* Filter out duplicate host names, and limit the array to
* MAX_RECENT entries total.
*/
hosts = (struct host *)NULL;
last_host = (struct host *)NULL;
for (i = 0; i < nh; i++) {
h = h_array[i];
if (h == (struct host *)NULL)
continue;
h->next = (struct host *)NULL;
if (last_host != (struct host *)NULL)
last_host->next = h;
h->prev = last_host;
last_host = h;
if (hosts == (struct host *)NULL)
hosts = h;
n_ent++;
/* Zap the duplicates. */
for (j = i+1; j < nh; j++) {
if (h_array[j] &&
(n_ent >= MAX_RECENT ||
!strcmp(h_array[i]->name, h_array[j]->name))) {
#if defined(CFDEBUG) /*[*/
printf("%s is a dup of %s\n",
h_array[j]->name, h_array[i]->name);
#endif /*]*/
Free(h_array[j]->name);
Free(h_array[j]->hostname);
Free(h_array[j]);
h_array[j] = (struct host *)NULL;
}
}
}
/* Re-attach the ibm_hosts entries to the end. */
if (rest != (struct host *)NULL) {
if (last_host != (struct host *)NULL) {
last_host->next = rest;
} else {
hosts = rest;
}
rest->prev = last_host;
}
/* If there's been a change, rewrite the file. */
if (hn != CN &&
appres.connectfile_name != CN &&
strcasecmp(appres.connectfile_name, "none")) {
lcf = fopen(lcf_name, "w");
if (lcf != (FILE *)NULL) {
fprintf(lcf, "# Created %s# by %s\n", ctime(&t), build);
for (h = hosts; h != (struct host *)NULL; h = h->next) {
if (h->entry_type != RECENT)
break;
(void) fprintf(lcf, "%lu %s\n", h->connect_time,
h->name);
}
fclose(lcf);
}
}
if (lcf_name != CN)
Free(lcf_name);
}
#endif /*]*/
/* Support for state change callbacks. */
struct st_callback {
struct st_callback *next;
void (*func)(Boolean);
};
static struct st_callback *st_callbacks[N_ST];
static struct st_callback *st_last[N_ST];
/* Register a function interested in a state change. */
void
register_schange(int tx, void (*func)(Boolean))
{
struct st_callback *st;
st = (struct st_callback *)Malloc(sizeof(*st));
st->func = func;
st->next = (struct st_callback *)NULL;
if (st_last[tx] != (struct st_callback *)NULL)
st_last[tx]->next = st;
else
st_callbacks[tx] = st;
st_last[tx] = st;
}
/* Signal a state change. */
void
st_changed(int tx, Boolean mode)
{
struct st_callback *st;
for (st = st_callbacks[tx];
st != (struct st_callback *)NULL;
st = st->next) {
(*st->func)(mode);
}
}
/* Explicit connect/disconnect actions. */
void
Connect_action(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
action_debug(Connect_action, event, params, num_params);
if (check_usage(Connect_action, *num_params, 1, 1) < 0)
return;
if (CONNECTED || HALF_CONNECTED) {
popup_an_error("Already connected");
return;
}
(void) host_connect(params[0]);
/*
* If called from a script and the connection was successful (or
* half-successful), pause the script until we are connected and
* we have identified the host type.
*/
if (!w && (CONNECTED || HALF_CONNECTED))
sms_connect_wait();
}
#if defined(X3270_MENUS) /*[*/
void
Reconnect_action(Widget w, XEvent *event, String *params, Cardinal *num_params)
{
action_debug(Reconnect_action, event, params, num_params);
if (check_usage(Reconnect_action, *num_params, 0, 0) < 0)
return;
if (CONNECTED || HALF_CONNECTED) {
popup_an_error("Already connected");
return;
}
if (current_host == CN) {
popup_an_error("No previous host to connect to");
return;
}
host_reconnect();
/*
* If called from a script and the connection was successful (or
* half-successful), pause the script until we are connected and
* we have identified the host type.
*/
if (!w && (CONNECTED || HALF_CONNECTED))
sms_connect_wait();
}
#endif /*]*/
void
Disconnect_action(Widget w unused, XEvent *event, String *params,
Cardinal *num_params)
{
action_debug(Disconnect_action, event, params, num_params);
if (check_usage(Disconnect_action, *num_params, 0, 0) < 0)
return;
host_disconnect(False);
}
syntax highlighted by Code2HTML, v. 0.9.1