mirror of
https://github.com/servalproject/serval-dna.git
synced 2024-12-23 23:12:31 +00:00
593 lines
16 KiB
C
593 lines
16 KiB
C
/*
|
|
Copyright (C) 2010-2012 Paul Gardner-Stephen, Serval Project.
|
|
|
|
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 2
|
|
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, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
/*
|
|
Android does unix domain sockets, but only stream sockets, not datagram sockets.
|
|
So we need a separate monitor interface for Android. A bit of a pain, but in
|
|
fact it lets us make a very Android/Java-friendly interface, without any binary
|
|
data structures (except for a binary extent for an audio sample block).
|
|
*/
|
|
|
|
#include "serval.h"
|
|
#include "rhizome.h"
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef HAVE_UCRED_H
|
|
#include <ucred.h>
|
|
#endif
|
|
|
|
#ifdef linux
|
|
#if defined(LOCAL_PEERCRED) && !defined(SO_PEERCRED)
|
|
#define SO_PEERCRED LOCAL_PEERCRED
|
|
#endif
|
|
#endif
|
|
|
|
#define MONITOR_LINE_LENGTH 160
|
|
#define MONITOR_DATA_SIZE MAX_AUDIO_BYTES
|
|
struct monitor_context {
|
|
struct sched_ent alarm;
|
|
int flags;
|
|
char line[MONITOR_LINE_LENGTH];
|
|
int line_length;
|
|
#define MONITOR_STATE_COMMAND 1
|
|
#define MONITOR_STATE_DATA 2
|
|
int state;
|
|
unsigned char buffer[MONITOR_DATA_SIZE];
|
|
int data_expected;
|
|
int data_offset;
|
|
int sample_codec;
|
|
int sample_call_session_token;
|
|
};
|
|
|
|
#define MAX_MONITOR_SOCKETS 8
|
|
int monitor_socket_count=0;
|
|
struct monitor_context monitor_sockets[MAX_MONITOR_SOCKETS];
|
|
|
|
int monitor_process_command(struct monitor_context *c);
|
|
int monitor_process_data(struct monitor_context *c);
|
|
static void monitor_new_client(int s);
|
|
|
|
struct sched_ent named_socket;
|
|
struct profile_total named_stats;
|
|
struct profile_total client_stats;
|
|
|
|
int monitor_socket_name(struct sockaddr_un *name){
|
|
int len;
|
|
#ifdef linux
|
|
/* Use abstract namespace as Android has no writable FS which supports sockets.
|
|
Abstract namespace is just plain better, anyway, as no dead files end up
|
|
hanging around. */
|
|
name->sun_path[0]=0;
|
|
/* XXX: 104 comes from OSX sys/un.h - no #define (note Linux has UNIX_PATH_MAX and it's 108(!)) */
|
|
snprintf(&name->sun_path[1],104-2,
|
|
confValueGet("monitor.socket",DEFAULT_MONITOR_SOCKET_NAME));
|
|
/* Doesn't include trailing nul */
|
|
len = 1+strlen(&name->sun_path[1]) + sizeof(name->sun_family);
|
|
#else
|
|
snprintf(name->sun_path,104-1,"%s/%s",
|
|
serval_instancepath(),
|
|
confValueGet("monitor.socket",DEFAULT_MONITOR_SOCKET_NAME));
|
|
/* Includes trailing nul */
|
|
len = 1+strlen(name->sun_path) + sizeof(name->sun_family);
|
|
#endif
|
|
return len;
|
|
}
|
|
|
|
int monitor_setup_sockets()
|
|
{
|
|
struct sockaddr_un name;
|
|
int len;
|
|
int sock;
|
|
|
|
bzero(&name, sizeof(name));
|
|
name.sun_family = AF_UNIX;
|
|
|
|
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0))==-1) {
|
|
WHY_perror("socket");
|
|
goto error;
|
|
}
|
|
|
|
len = monitor_socket_name(&name);
|
|
#ifndef linux
|
|
unlink(name.sun_path);
|
|
#endif
|
|
|
|
if(bind(sock, (struct sockaddr *)&name, len)==-1) {
|
|
WHY_perror("bind");
|
|
goto error;
|
|
}
|
|
if(listen(sock,MAX_MONITOR_SOCKETS)==-1) {
|
|
WHY_perror("listen");
|
|
goto error;
|
|
}
|
|
|
|
int reuseP=1;
|
|
if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
|
|
&reuseP, sizeof(reuseP)) < 0) {
|
|
WHY_perror("setsockopt");
|
|
WHY("Could not indicate reuse addresses. Not necessarily a problem (yet)");
|
|
}
|
|
|
|
int send_buffer_size=64*1024;
|
|
if(setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
|
|
&send_buffer_size, sizeof(send_buffer_size))==-1)
|
|
WHY_perror("setsockopt");
|
|
if (debug&(DEBUG_IO|DEBUG_VERBOSE_IO)) DEBUG("Monitor server socket setup");
|
|
|
|
named_socket.function=monitor_poll;
|
|
named_stats.name="monitor_poll";
|
|
named_socket.stats=&named_stats;
|
|
named_socket.poll.fd=sock;
|
|
named_socket.poll.events=POLLIN;
|
|
watch(&named_socket);
|
|
return 0;
|
|
|
|
error:
|
|
close(sock);
|
|
return -1;
|
|
}
|
|
|
|
void monitor_poll(struct sched_ent *alarm)
|
|
{
|
|
int s;
|
|
unsigned char buffer[1024];
|
|
struct sockaddr *ignored_address=(struct sockaddr *)&buffer[0];
|
|
socklen_t ignored_length=sizeof(ignored_address);
|
|
|
|
/* Check for new connections */
|
|
/* We don't care about the peer's address */
|
|
ignored_length = 0;
|
|
while (
|
|
#ifdef HAVE_LINUX_IF_H
|
|
(s = accept4(alarm->poll.fd, NULL, &ignored_length,O_NONBLOCK))
|
|
#else
|
|
(s = accept(alarm->poll.fd,NULL, &ignored_length))
|
|
#endif
|
|
!= -1
|
|
) {
|
|
monitor_new_client(s);
|
|
}
|
|
if (errno != EAGAIN) {
|
|
#ifdef HAVE_LINUX_IF_H
|
|
WHY_perror("accept4(O_NONBLOCK)");
|
|
#else
|
|
WHY_perror("accept");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void monitor_client_close(struct monitor_context *c){
|
|
struct monitor_context *last;
|
|
|
|
unwatch(&c->alarm);
|
|
close(c->alarm.poll.fd);
|
|
c->alarm.poll.fd=-1;
|
|
|
|
monitor_socket_count--;
|
|
last = &monitor_sockets[monitor_socket_count];
|
|
if (last != c){
|
|
unwatch(&last->alarm);
|
|
bcopy(last, c,
|
|
sizeof(struct monitor_context));
|
|
watch(&c->alarm);
|
|
}
|
|
}
|
|
|
|
void monitor_client_poll(struct sched_ent *alarm)
|
|
{
|
|
/* Read available data from a monitor socket */
|
|
struct monitor_context *c=(struct monitor_context *)alarm;
|
|
errno=0;
|
|
int bytes;
|
|
|
|
switch(c->state) {
|
|
case MONITOR_STATE_COMMAND:
|
|
bytes = 1;
|
|
while(bytes == 1) {
|
|
if (c->line_length >= MONITOR_LINE_LENGTH) {
|
|
/* line too long */
|
|
c->line[MONITOR_LINE_LENGTH-1] = 0;
|
|
monitor_process_command(c);
|
|
bytes = -1;
|
|
break;
|
|
}
|
|
bytes = read(c->alarm.poll.fd, &c->line[c->line_length], 1);
|
|
if (bytes < 1) {
|
|
switch(errno) {
|
|
case EINTR:
|
|
case ENOTRECOVERABLE:
|
|
/* transient errors */
|
|
WHY_perror("read");
|
|
break;
|
|
case EAGAIN:
|
|
break;
|
|
default:
|
|
WHY_perror("read");
|
|
/* all other errors; close socket */
|
|
INFO("Tearing down monitor client");
|
|
monitor_client_close(c);
|
|
return;
|
|
}
|
|
}
|
|
if (bytes > 0 && (c->line[c->line_length] != '\r')) {
|
|
c->line_length += bytes;
|
|
if (c->line[c->line_length-1] == '\n') {
|
|
/* got command */
|
|
c->line[c->line_length-1] = 0; /* trim new line for easier parsing */
|
|
monitor_process_command(c);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case MONITOR_STATE_DATA:
|
|
bytes = read(c->alarm.poll.fd,
|
|
&c->buffer[c->data_offset],
|
|
c->data_expected-c->data_offset);
|
|
if (bytes < 1) {
|
|
switch(errno) {
|
|
case EAGAIN: case EINTR:
|
|
/* transient errors */
|
|
break;
|
|
default:
|
|
/* all other errors; close socket */
|
|
WHYF("Tearing down monitor client due to errno=%d",
|
|
errno);
|
|
monitor_client_close(c);
|
|
return;
|
|
}
|
|
} else {
|
|
c->data_offset += bytes;
|
|
if (c->data_offset >= c->data_expected)
|
|
{
|
|
/* we have the binary data we were expecting. */
|
|
monitor_process_data(c);
|
|
c->state = MONITOR_STATE_COMMAND;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
c->state = MONITOR_STATE_COMMAND;
|
|
WHY("fixed monitor connection state");
|
|
}
|
|
return;
|
|
}
|
|
|
|
static void monitor_new_client(int s) {
|
|
#ifdef SO_PEERCRED
|
|
struct ucred ucred;
|
|
socklen_t len;
|
|
int res;
|
|
#elif defined(HAVE_GETPEEREID)
|
|
gid_t othergid;
|
|
#elif defined(HAVE_UCRED_H)
|
|
ucred_t *ucred;
|
|
#endif
|
|
uid_t otheruid;
|
|
struct monitor_context *c;
|
|
|
|
if (set_nonblock(s) == -1)
|
|
goto error;
|
|
|
|
#ifdef SO_PEERCRED
|
|
/* Linux way */
|
|
len = sizeof(ucred);
|
|
res = getsockopt(s, SOL_SOCKET, SO_PEERCRED, &ucred, &len);
|
|
if (res) {
|
|
WHY_perror("getsockopt(SO_PEERCRED)");
|
|
goto error;
|
|
}
|
|
if (len < sizeof(ucred)) {
|
|
WHYF("getsockopt(SO_PEERCRED) returned the wrong size (Got %d expected %d)", len, sizeof(ucred));
|
|
goto error;
|
|
}
|
|
otheruid = ucred.uid;
|
|
#elif defined(HAVE_UCRED_H)
|
|
/* Solaris way */
|
|
if (getpeerucred(s, &ucred) != 0) {
|
|
WHY_perror("getpeerucred()");
|
|
goto error;
|
|
}
|
|
otheruid = ucred_geteuid(ucred);
|
|
ucred_free(ucred);
|
|
#elif defined(HAVE_GETPEEREID)
|
|
/* BSD way */
|
|
if (getpeereid(s, &otheruid, &othergid) != 0) {
|
|
WHY_perror("getpeereid()");
|
|
goto error;
|
|
}
|
|
#else
|
|
#error No way to get socket peer credentials
|
|
#endif
|
|
|
|
if (otheruid != getuid()) {
|
|
int allowed_id = confValueGetInt64("monitor.uid",-1);
|
|
if (otheruid != allowed_id){
|
|
WHYF("monitor.socket client has wrong uid (%d versus %d)", otheruid,getuid());
|
|
write_str(s, "\nCLOSE:Incorrect UID\n");
|
|
goto error;
|
|
}
|
|
}
|
|
if (monitor_socket_count >= MAX_MONITOR_SOCKETS
|
|
||monitor_socket_count < 0) {
|
|
write_str(s, "\nCLOSE:All sockets busy\n");
|
|
goto error;
|
|
}
|
|
|
|
c = &monitor_sockets[monitor_socket_count++];
|
|
c->alarm.function = monitor_client_poll;
|
|
client_stats.name = "monitor_client_poll";
|
|
c->alarm.stats=&client_stats;
|
|
c->alarm.poll.fd = s;
|
|
c->alarm.poll.events=POLLIN;
|
|
c->line_length = 0;
|
|
c->state = MONITOR_STATE_COMMAND;
|
|
write_str(s,"\nMONITOR:You are talking to servald\n");
|
|
INFOF("Got %d clients", monitor_socket_count);
|
|
watch(&c->alarm);
|
|
|
|
return;
|
|
|
|
error:
|
|
close(s);
|
|
return;
|
|
}
|
|
|
|
int monitor_send_lookup_response(const char *sid, const int port, const char *ext, const char *name){
|
|
struct sockaddr_mdp addr={
|
|
.port = port
|
|
};
|
|
|
|
if (stowSid((unsigned char *)&addr.sid, 0, sid)==-1)
|
|
return WHYF("Invalid SID %s", sid);
|
|
|
|
int cn=0, in=0, kp=0;
|
|
if (!keyring_next_identity(keyring, &cn, &in, &kp))
|
|
WHY("No local identity, cannot send DNA LOOKUP reply");
|
|
else{
|
|
char uri[256];
|
|
snprintf(uri, sizeof(uri), "sid://%s/%s", alloca_tohex_sid(keyring->contexts[cn]->identities[in]->keypairs[kp]->public_key), ext);
|
|
DEBUGF("Sending response to %s for %s", sid, uri);
|
|
overlay_mdp_dnalookup_reply(&addr, keyring->contexts[cn]->identities[in]->keypairs[kp]->public_key, uri, ext, name);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int monitor_process_command(struct monitor_context *c)
|
|
{
|
|
int callSessionToken,sampleType,bytes;
|
|
char sid[MONITOR_LINE_LENGTH],localDid[MONITOR_LINE_LENGTH];
|
|
char remoteDid[MONITOR_LINE_LENGTH],digits[MONITOR_LINE_LENGTH];
|
|
int port;
|
|
|
|
char *cmd = c->line;
|
|
IN();
|
|
|
|
remoteDid[0]='\0';
|
|
c->line_length=0;
|
|
|
|
if (strlen(cmd)>MONITOR_LINE_LENGTH) {
|
|
write_str(c->alarm.poll.fd,"\nERROR:Command too long\n");
|
|
RETURN(-1);
|
|
}
|
|
|
|
char msg[1024];
|
|
|
|
if (cmd[0]=='*') {
|
|
/* command with content */
|
|
int ofs=0;
|
|
if (sscanf(cmd,"*%d:%n",&bytes,&ofs)==1) {
|
|
/* work out rest of command */
|
|
cmd=&cmd[ofs];
|
|
c->state=MONITOR_STATE_DATA;
|
|
c->data_expected=bytes;
|
|
c->data_offset=0;
|
|
c->sample_codec=-1;
|
|
|
|
if (sscanf(cmd,"AUDIO %x %d",
|
|
&callSessionToken,&sampleType)==2)
|
|
{
|
|
/* Start getting sample */
|
|
c->sample_call_session_token=callSessionToken;
|
|
c->sample_codec=sampleType;
|
|
RETURN(0);
|
|
}
|
|
}
|
|
}
|
|
else if (strcase_startswith(cmd,"monitor vomp",NULL))
|
|
// TODO add supported codec list argument
|
|
c->flags|=MONITOR_VOMP;
|
|
else if (strcase_startswith(cmd,"ignore vomp",NULL))
|
|
c->flags&=~MONITOR_VOMP;
|
|
else if (strcase_startswith(cmd,"monitor rhizome", NULL))
|
|
c->flags|=MONITOR_RHIZOME;
|
|
else if (strcase_startswith(cmd,"ignore rhizome", NULL))
|
|
c->flags&=~MONITOR_RHIZOME;
|
|
else if (strcase_startswith(cmd,"monitor peers", NULL))
|
|
c->flags|=MONITOR_PEERS;
|
|
else if (strcase_startswith(cmd,"ignore peers", NULL))
|
|
c->flags&=~MONITOR_PEERS;
|
|
else if (strcase_startswith(cmd,"monitor dnahelper", NULL))
|
|
c->flags|=MONITOR_DNAHELPER;
|
|
else if (strcase_startswith(cmd,"ignore dnahelper", NULL))
|
|
c->flags&=~MONITOR_DNAHELPER;
|
|
else if (sscanf(cmd,"lookup match %s %d %s %s",sid,&port,localDid,remoteDid)>=3) {
|
|
monitor_send_lookup_response(sid,port,localDid,remoteDid);
|
|
}else if (sscanf(cmd,"call %s %s %s",sid,localDid,remoteDid)==3) {
|
|
int gotSid = 0;
|
|
if (sid[0]=='*') {
|
|
/* For testing, pick any peer and call them */
|
|
int bin, slot;
|
|
for (bin = 0; bin < overlay_bin_count; bin++)
|
|
for (slot = 0; slot < overlay_bin_size; slot++) {
|
|
if (overlay_nodes[bin][slot].sid[0]) {
|
|
memcpy(sid, overlay_nodes[bin][slot].sid, SID_SIZE);
|
|
gotSid = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!gotSid)
|
|
write_str(c->alarm.poll.fd,"\nERROR:no known peers, so cannot place call\n");
|
|
} else {
|
|
// pack the binary representation of the sid into the same buffer.
|
|
if (stowSid((unsigned char*)sid, 0, sid) == -1)
|
|
write_str(c->alarm.poll.fd,"\nERROR:invalid SID, so cannot place call\n");
|
|
else
|
|
gotSid = 1;
|
|
}
|
|
if (gotSid) {
|
|
int cn=0, in=0, kp=0;
|
|
if (!keyring_next_identity(keyring, &cn, &in, &kp))
|
|
write_str(c->alarm.poll.fd,"\nERROR:no local identity, so cannot place call\n");
|
|
else {
|
|
vomp_dial(keyring->contexts[cn]->identities[in]->keypairs[kp]->public_key, (unsigned char *)sid, localDid, remoteDid);
|
|
}
|
|
}
|
|
} else if (sscanf(cmd,"ringing %x",&callSessionToken)==1) {
|
|
struct vomp_call_state *call=vomp_find_call_by_session(callSessionToken);
|
|
vomp_ringing(call);
|
|
} else if (sscanf(cmd,"pickup %x",&callSessionToken)==1) {
|
|
struct vomp_call_state *call=vomp_find_call_by_session(callSessionToken);
|
|
vomp_pickup(call);
|
|
}
|
|
else if (sscanf(cmd,"hangup %x",&callSessionToken)==1) {
|
|
struct vomp_call_state *call=vomp_find_call_by_session(callSessionToken);
|
|
vomp_hangup(call);
|
|
} else if (sscanf(cmd,"dtmf %x %s",&callSessionToken,digits)==2) {
|
|
struct vomp_call_state *call=vomp_find_call_by_session(callSessionToken);
|
|
if (call){
|
|
int i;
|
|
for(i=0;i<strlen(digits);i++) {
|
|
int digit=vomp_parse_dtmf_digit(digits[i]);
|
|
if (digit<0) {
|
|
snprintf(msg,1024,"\nERROR: invalid DTMF digit 0x%02x\n",digit);
|
|
write_str(c->alarm.poll.fd,msg);
|
|
}
|
|
/* 80ms standard tone duration, so that it is a multiple
|
|
of the majority of codec time units (70ms is the nominal
|
|
DTMF tone length for most systems). */
|
|
unsigned char code = digit <<4;
|
|
vomp_received_audio(call, VOMP_CODEC_DTMF, &code, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
snprintf(msg,1024,"\nMONITORSTATUS:%d\n",c->flags);
|
|
write_str(c->alarm.poll.fd,msg);
|
|
|
|
RETURN(0);
|
|
}
|
|
|
|
int monitor_process_data(struct monitor_context *c)
|
|
{
|
|
IN();
|
|
/* Called when we have received an entire data sample */
|
|
c->state=MONITOR_STATE_COMMAND;
|
|
|
|
struct vomp_call_state *call=vomp_find_call_by_session(c->sample_call_session_token);
|
|
if (!call) {
|
|
write_str(c->alarm.poll.fd,"\nERROR:No such call\n");
|
|
RETURN(-1);
|
|
}
|
|
|
|
vomp_received_audio(call, c->sample_codec, &c->buffer[0], vomp_sample_size(c->sample_codec));
|
|
|
|
RETURN(0);
|
|
}
|
|
|
|
int monitor_announce_bundle(rhizome_manifest *m)
|
|
{
|
|
int i;
|
|
char msg[1024];
|
|
const char *service = rhizome_manifest_get(m, "service", NULL, 0);
|
|
const char *sender = rhizome_manifest_get(m, "sender", NULL, 0);
|
|
const char *recipient = rhizome_manifest_get(m, "recipient", NULL, 0);
|
|
snprintf(msg,1024,"\nBUNDLE:%s:%s:%lld:%lld:%s:%s:%s\n",
|
|
/* XXX bit of a hack here, since SIDs and cryptosign public keys have the same length */
|
|
alloca_tohex_sid(m->cryptoSignPublic),
|
|
service ? service : "",
|
|
m->version,
|
|
m->fileLength,
|
|
sender ? sender : "",
|
|
recipient ? recipient : "",
|
|
m->dataFileName?m->dataFileName:"");
|
|
for(i=monitor_socket_count -1;i>=0;i--) {
|
|
if (monitor_sockets[i].flags & MONITOR_RHIZOME) {
|
|
if ( set_nonblock(monitor_sockets[i].alarm.poll.fd) == -1
|
|
|| write_str_nonblock(monitor_sockets[i].alarm.poll.fd, msg) == -1
|
|
|| set_block(monitor_sockets[i].alarm.poll.fd) == -1
|
|
) {
|
|
INFO("Tearing down monitor client");
|
|
monitor_client_close(&monitor_sockets[i]);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int monitor_announce_peer(const unsigned char *sid)
|
|
{
|
|
return monitor_tell_formatted(MONITOR_PEERS, "\nNEWPEER:%s\n", alloca_tohex_sid(sid));
|
|
}
|
|
|
|
int monitor_announce_unreachable_peer(const unsigned char *sid)
|
|
{
|
|
return monitor_tell_formatted(MONITOR_PEERS, "\nOLDPEER:%s\n", alloca_tohex_sid(sid));
|
|
}
|
|
|
|
// test if any monitor clients are interested in a particular type of event
|
|
int monitor_client_interested(int mask){
|
|
int i;
|
|
for(i=monitor_socket_count -1;i>=0;i--) {
|
|
if (monitor_sockets[i].flags & mask)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int monitor_tell_clients(char *msg, int msglen, int mask)
|
|
{
|
|
int i;
|
|
IN();
|
|
for(i=monitor_socket_count -1;i>=0;i--) {
|
|
if (monitor_sockets[i].flags & mask) {
|
|
// DEBUG("Writing AUDIOPACKET to client");
|
|
if ( set_nonblock(monitor_sockets[i].alarm.poll.fd) == -1
|
|
|| write_all_nonblock(monitor_sockets[i].alarm.poll.fd, msg, msglen) == -1
|
|
|| set_block(monitor_sockets[i].alarm.poll.fd) == -1
|
|
) {
|
|
INFOF("Tearing down monitor client #%d", i);
|
|
monitor_client_close(&monitor_sockets[i]);
|
|
}
|
|
}
|
|
}
|
|
RETURN(0);
|
|
}
|
|
|
|
int monitor_tell_formatted(int mask, char *fmt, ...){
|
|
char msg[1024];
|
|
int n;
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
n=vsnprintf(msg, sizeof(msg), fmt, ap);
|
|
va_end(ap);
|
|
monitor_tell_clients(msg, n, mask);
|
|
return 0;
|
|
}
|