mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-27 05:13:10 +00:00
784 lines
14 KiB
C++
784 lines
14 KiB
C++
/** \file fishd.c
|
|
|
|
The universal variable server. fishd is automatically started by fish
|
|
if a fishd server isn't already running. fishd reads any saved
|
|
variables from ~/.fishd, and takes care of communication between fish
|
|
instances. When no clients are running, fishd will automatically shut
|
|
down and save.
|
|
|
|
\section fishd-commands Commands
|
|
|
|
Fishd works by sending and receiving commands. Each command is ended
|
|
with a newline. These are the commands supported by fishd:
|
|
|
|
<pre>set KEY:VALUE
|
|
set_export KEY:VALUE
|
|
</pre>
|
|
|
|
These commands update the value of a variable. The only difference
|
|
between the two is that <tt>set_export</tt>-variables should be
|
|
exported to children of the process using them. When sending messages,
|
|
all values below 32 or above 127 must be escaped using C-style
|
|
backslash escapes. This means that the over the wire protocol is
|
|
ASCII. However, any conforming reader must also accept non-ascii
|
|
characters and interpret them as UTF-8. Lines containing invalid UTF-8
|
|
escape sequences must be ignored entirely.
|
|
|
|
<pre>erase KEY
|
|
</pre>
|
|
|
|
Erase the variable with the specified name.
|
|
|
|
<pre>barrier
|
|
barrier_reply
|
|
</pre>
|
|
|
|
A \c barrier command will result in a barrier_reply being added to
|
|
the end of the senders queue of unsent messages. These commands are
|
|
used to synchronize clients, since once the reply for a barrier
|
|
message returns, the sender can know that any updates available at the
|
|
time the original barrier request was sent have been received.
|
|
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <wchar.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <pwd.h>
|
|
#include <fcntl.h>
|
|
|
|
#ifdef HAVE_GETOPT_H
|
|
#include <getopt.h>
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <locale.h>
|
|
#include <signal.h>
|
|
|
|
#include "fallback.h"
|
|
#include "util.h"
|
|
|
|
#include "common.h"
|
|
#include "wutil.h"
|
|
#include "env_universal_common.h"
|
|
#include "halloc.h"
|
|
#include "halloc_util.h"
|
|
#include "path.h"
|
|
#include "print_help.h"
|
|
|
|
/**
|
|
Maximum length of socket filename
|
|
*/
|
|
#ifndef UNIX_PATH_MAX
|
|
#define UNIX_PATH_MAX 100
|
|
#endif
|
|
|
|
/**
|
|
Fallback if MSG_DONTWAIT isn't defined. That's actually prerry bad,
|
|
and may lead to strange fishd behaviour, but at least it should
|
|
work most of the time.
|
|
*/
|
|
#ifndef MSG_DONTWAIT
|
|
#define MSG_DONTWAIT 0
|
|
#endif
|
|
|
|
/**
|
|
Small greeting to show that fishd is running
|
|
*/
|
|
#define GREETING "# Fish universal variable daemon\n"
|
|
|
|
/**
|
|
Small not about not editing ~/.fishd manually. Inserted at the top of all .fishd files.
|
|
*/
|
|
#define SAVE_MSG "# This file is automatically generated by the fishd universal variable daemon.\n# Do NOT edit it directly, your changes will be overwritten.\n"
|
|
|
|
/**
|
|
The name of the save file. The hostname is appended to this.
|
|
*/
|
|
#define FILE "fishd."
|
|
|
|
/**
|
|
Maximum length of hostname. Longer hostnames are truncated
|
|
*/
|
|
#define HOSTNAME_LEN 32
|
|
|
|
/**
|
|
The string to append to the socket name to name the lockfile
|
|
*/
|
|
#define LOCKPOSTFIX ".lock"
|
|
|
|
/**
|
|
The timeout in seconds on the lockfile for critical section
|
|
*/
|
|
#define LOCKTIMEOUT 1
|
|
|
|
/**
|
|
Getopt short switches for fishd
|
|
*/
|
|
#define GETOPT_STRING "hv"
|
|
|
|
/**
|
|
The list of connections to clients
|
|
*/
|
|
static connection_t *conn;
|
|
|
|
/**
|
|
The socket to accept new clients on
|
|
*/
|
|
static int sock;
|
|
|
|
/**
|
|
Set to one when fishd should save and exit
|
|
*/
|
|
static int quit=0;
|
|
|
|
/**
|
|
Constructs the fish socket filename
|
|
*/
|
|
static char *get_socket_filename()
|
|
{
|
|
char *name;
|
|
const char *dir = getenv( "FISHD_SOCKET_DIR" );
|
|
char *uname = getenv( "USER" );
|
|
|
|
if( dir == NULL )
|
|
{
|
|
dir = "/tmp";
|
|
}
|
|
|
|
if( uname == NULL )
|
|
{
|
|
struct passwd *pw;
|
|
pw = getpwuid( getuid() );
|
|
uname = strdup( pw->pw_name );
|
|
}
|
|
|
|
name = (char *)malloc( strlen(dir)+ strlen(uname)+ strlen(SOCK_FILENAME) + 2 );
|
|
if( name == NULL )
|
|
{
|
|
wperror( L"get_socket_filename" );
|
|
exit( EXIT_FAILURE );
|
|
}
|
|
strcpy( name, dir );
|
|
strcat( name, "/" );
|
|
strcat( name, SOCK_FILENAME );
|
|
strcat( name, uname );
|
|
|
|
if( strlen( name ) >= UNIX_PATH_MAX )
|
|
{
|
|
debug( 1, L"Filename too long: '%s'", name );
|
|
exit( EXIT_FAILURE );
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
Signal handler for the term signal.
|
|
*/
|
|
static void handle_term( int signal )
|
|
{
|
|
quit=1;
|
|
}
|
|
|
|
|
|
/**
|
|
Acquire the lock for the socket
|
|
Returns the name of the lock file if successful or
|
|
NULL if unable to obtain lock.
|
|
The returned string must be free()d after unlink()ing the file to release
|
|
the lock
|
|
*/
|
|
static char *acquire_socket_lock( const char *sock_name )
|
|
{
|
|
int len = strlen( sock_name );
|
|
char *lockfile = (char *)malloc( len + strlen( LOCKPOSTFIX ) + 1 );
|
|
|
|
if( lockfile == NULL )
|
|
{
|
|
wperror( L"acquire_socket_lock" );
|
|
exit( EXIT_FAILURE );
|
|
}
|
|
strcpy( lockfile, sock_name );
|
|
strcpy( lockfile + len, LOCKPOSTFIX );
|
|
if ( !acquire_lock_file( lockfile, LOCKTIMEOUT, 1 ) )
|
|
{
|
|
free( lockfile );
|
|
lockfile = NULL;
|
|
}
|
|
return lockfile;
|
|
}
|
|
|
|
/**
|
|
Connects to the fish socket and starts listening for connections
|
|
*/
|
|
static int get_socket()
|
|
{
|
|
int s, len, doexit = 0;
|
|
int exitcode = EXIT_FAILURE;
|
|
struct sockaddr_un local;
|
|
char *sock_name = get_socket_filename();
|
|
|
|
/*
|
|
Start critical section protected by lock
|
|
*/
|
|
char *lockfile = acquire_socket_lock( sock_name );
|
|
if( lockfile == NULL )
|
|
{
|
|
debug( 0, L"Unable to obtain lock on socket, exiting" );
|
|
exit( EXIT_FAILURE );
|
|
}
|
|
debug( 4, L"Acquired lockfile: %s", lockfile );
|
|
|
|
local.sun_family = AF_UNIX;
|
|
strcpy( local.sun_path, sock_name );
|
|
len = sizeof(local);
|
|
|
|
debug(1, L"Connect to socket at %s", sock_name);
|
|
|
|
if( ( s = socket( AF_UNIX, SOCK_STREAM, 0 ) ) == -1 )
|
|
{
|
|
wperror( L"socket" );
|
|
doexit = 1;
|
|
goto unlock;
|
|
}
|
|
|
|
/*
|
|
First check whether the socket has been opened by another fishd;
|
|
if so, exit with success status
|
|
*/
|
|
if( connect( s, (struct sockaddr *)&local, len ) == 0 )
|
|
{
|
|
debug( 1, L"Socket already exists, exiting" );
|
|
doexit = 1;
|
|
exitcode = 0;
|
|
goto unlock;
|
|
}
|
|
|
|
unlink( local.sun_path );
|
|
if( bind( s, (struct sockaddr *)&local, len ) == -1 )
|
|
{
|
|
wperror( L"bind" );
|
|
doexit = 1;
|
|
goto unlock;
|
|
}
|
|
|
|
if( fcntl( s, F_SETFL, O_NONBLOCK ) != 0 )
|
|
{
|
|
wperror( L"fcntl" );
|
|
close( s );
|
|
doexit = 1;
|
|
} else if( listen( s, 64 ) == -1 )
|
|
{
|
|
wperror( L"listen" );
|
|
doexit = 1;
|
|
}
|
|
|
|
unlock:
|
|
(void)unlink( lockfile );
|
|
debug( 4, L"Released lockfile: %s", lockfile );
|
|
/*
|
|
End critical section protected by lock
|
|
*/
|
|
|
|
free( lockfile );
|
|
|
|
free( sock_name );
|
|
|
|
if( doexit )
|
|
{
|
|
exit( exitcode );
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
Event handler. Broadcasts updates to all clients.
|
|
*/
|
|
static void broadcast( int type, const wchar_t *key, const wchar_t *val )
|
|
{
|
|
connection_t *c;
|
|
message_t *msg;
|
|
|
|
if( !conn )
|
|
return;
|
|
|
|
msg = create_message( type, key, val );
|
|
|
|
/*
|
|
Don't merge these loops, or try_send_all can free the message
|
|
prematurely
|
|
*/
|
|
|
|
for( c = conn; c; c=c->next )
|
|
{
|
|
msg->count++;
|
|
c->unsent->push(msg);
|
|
}
|
|
|
|
for( c = conn; c; c=c->next )
|
|
{
|
|
try_send_all( c );
|
|
}
|
|
}
|
|
|
|
/**
|
|
Make program into a creature of the night.
|
|
*/
|
|
static void daemonize()
|
|
{
|
|
/*
|
|
Fork, and let parent exit
|
|
*/
|
|
switch( fork() )
|
|
{
|
|
case -1:
|
|
debug( 0, L"Could not put fishd in background. Quitting" );
|
|
wperror( L"fork" );
|
|
exit(1);
|
|
|
|
case 0:
|
|
{
|
|
/*
|
|
Make fishd ignore the HUP signal.
|
|
*/
|
|
struct sigaction act;
|
|
sigemptyset( & act.sa_mask );
|
|
act.sa_flags=0;
|
|
act.sa_handler=SIG_IGN;
|
|
sigaction( SIGHUP, &act, 0);
|
|
|
|
/*
|
|
Make fishd save and exit on the TERM signal.
|
|
*/
|
|
sigfillset( & act.sa_mask );
|
|
act.sa_flags=0;
|
|
act.sa_handler=&handle_term;
|
|
sigaction( SIGTERM, &act, 0);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
debug( 0, L"Parent process exiting (This is normal)" );
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Put ourself in out own processing group
|
|
*/
|
|
setsid();
|
|
|
|
/*
|
|
Close stdin and stdout. We only use stderr, anyway.
|
|
*/
|
|
close( 0 );
|
|
close( 1 );
|
|
|
|
}
|
|
|
|
/**
|
|
Get environment variable value. The resulting string needs to be free'd.
|
|
*/
|
|
static wchar_t *fishd_env_get( const wchar_t *key )
|
|
{
|
|
char *nres, *nkey;
|
|
wchar_t *res;
|
|
|
|
nkey = wcs2str( key );
|
|
nres = getenv( nkey );
|
|
free( nkey );
|
|
if( nres )
|
|
{
|
|
return str2wcs( nres );
|
|
}
|
|
else
|
|
{
|
|
res = env_universal_common_get( key );
|
|
if( res )
|
|
res = wcsdup( res );
|
|
|
|
return env_universal_common_get( key );
|
|
}
|
|
}
|
|
|
|
/**
|
|
Get the configuration directory. The resulting string needs to be
|
|
free'd. This is mostly the same code as path_get_config(), but had
|
|
to be rewritten to avoid dragging in additional library
|
|
dependencies.
|
|
*/
|
|
static wchar_t *fishd_get_config()
|
|
{
|
|
wchar_t *xdg_dir, *home;
|
|
int done = 0;
|
|
wchar_t *res = 0;
|
|
|
|
xdg_dir = fishd_env_get( L"XDG_CONFIG_HOME" );
|
|
if( xdg_dir )
|
|
{
|
|
res = wcsdupcat( xdg_dir, L"/fish" );
|
|
if( !create_directory( res ) )
|
|
{
|
|
done = 1;
|
|
}
|
|
else
|
|
{
|
|
free( res );
|
|
}
|
|
free( xdg_dir );
|
|
}
|
|
else
|
|
{
|
|
home = fishd_env_get( L"HOME" );
|
|
if( home )
|
|
{
|
|
res = wcsdupcat( home, L"/.config/fish" );
|
|
if( !create_directory( res ) )
|
|
{
|
|
done = 1;
|
|
}
|
|
else
|
|
{
|
|
free( res );
|
|
}
|
|
free( home );
|
|
}
|
|
}
|
|
|
|
if( done )
|
|
{
|
|
return res;
|
|
}
|
|
else
|
|
{
|
|
debug( 0, _(L"Unable to create a configuration directory for fish. Your personal settings will not be saved. Please set the $XDG_CONFIG_HOME variable to a directory where the current user has write access." ));
|
|
return 0;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
Load or save all variables
|
|
*/
|
|
static void load_or_save( int save)
|
|
{
|
|
char *name;
|
|
wchar_t *wdir = fishd_get_config();
|
|
char *dir;
|
|
char hostname[HOSTNAME_LEN];
|
|
connection_t c;
|
|
int fd;
|
|
|
|
if( !wdir )
|
|
{
|
|
return;
|
|
}
|
|
|
|
dir = wcs2str( wdir );
|
|
|
|
free( wdir );
|
|
|
|
gethostname( hostname, HOSTNAME_LEN );
|
|
|
|
name = (char *)malloc( strlen(dir)+ strlen(FILE)+ strlen(hostname) + 2 );
|
|
strcpy( name, dir );
|
|
strcat( name, "/" );
|
|
strcat( name, FILE );
|
|
strcat( name, hostname );
|
|
|
|
free( dir );
|
|
|
|
|
|
debug( 4, L"Open file for %s: '%s'",
|
|
save?"saving":"loading",
|
|
name );
|
|
|
|
fd = open( name, save?(O_CREAT | O_TRUNC | O_WRONLY):O_RDONLY, 0600);
|
|
|
|
free( name );
|
|
|
|
if( fd == -1 )
|
|
{
|
|
debug( 1, L"Could not open load/save file. No previous saves?" );
|
|
wperror( L"open" );
|
|
return;
|
|
}
|
|
debug( 4, L"File open on fd %d", c.fd );
|
|
|
|
connection_init( &c, fd );
|
|
|
|
if( save )
|
|
{
|
|
|
|
write_loop( c.fd, SAVE_MSG, strlen(SAVE_MSG) );
|
|
enqueue_all( &c );
|
|
}
|
|
else
|
|
read_message( &c );
|
|
|
|
connection_destroy( &c );
|
|
}
|
|
|
|
/**
|
|
Load variables from disk
|
|
*/
|
|
static void load()
|
|
{
|
|
load_or_save(0);
|
|
}
|
|
|
|
/**
|
|
Save variables to disk
|
|
*/
|
|
static void save()
|
|
{
|
|
load_or_save(1);
|
|
}
|
|
|
|
/**
|
|
Do all sorts of boring initialization.
|
|
*/
|
|
static void init()
|
|
{
|
|
|
|
sock = get_socket();
|
|
daemonize();
|
|
env_universal_common_init( &broadcast );
|
|
|
|
load();
|
|
}
|
|
|
|
/**
|
|
Main function for fishd
|
|
*/
|
|
int main( int argc, char ** argv )
|
|
{
|
|
int child_socket;
|
|
struct sockaddr_un remote;
|
|
socklen_t t;
|
|
int max_fd;
|
|
int update_count=0;
|
|
|
|
fd_set read_fd, write_fd;
|
|
|
|
halloc_util_init();
|
|
set_main_thread();
|
|
|
|
program_name=L"fishd";
|
|
wsetlocale( LC_ALL, L"" );
|
|
|
|
/*
|
|
Parse options
|
|
*/
|
|
while( 1 )
|
|
{
|
|
static struct option
|
|
long_options[] =
|
|
{
|
|
{
|
|
"help", no_argument, 0, 'h'
|
|
}
|
|
,
|
|
{
|
|
"version", no_argument, 0, 'v'
|
|
}
|
|
,
|
|
{
|
|
0, 0, 0, 0
|
|
}
|
|
}
|
|
;
|
|
|
|
int opt_index = 0;
|
|
|
|
int opt = getopt_long( argc,
|
|
argv,
|
|
GETOPT_STRING,
|
|
long_options,
|
|
&opt_index );
|
|
|
|
if( opt == -1 )
|
|
break;
|
|
|
|
switch( opt )
|
|
{
|
|
case 0:
|
|
break;
|
|
|
|
case 'h':
|
|
print_help( argv[0], 1 );
|
|
exit(0);
|
|
|
|
case 'v':
|
|
debug( 0, L"%ls, version %s\n", program_name, PACKAGE_VERSION );
|
|
exit( 0 );
|
|
|
|
case '?':
|
|
return 1;
|
|
|
|
}
|
|
}
|
|
|
|
init();
|
|
while(1)
|
|
{
|
|
connection_t *c;
|
|
int res;
|
|
|
|
t = sizeof( remote );
|
|
|
|
FD_ZERO( &read_fd );
|
|
FD_ZERO( &write_fd );
|
|
FD_SET( sock, &read_fd );
|
|
max_fd = sock+1;
|
|
for( c=conn; c; c=c->next )
|
|
{
|
|
FD_SET( c->fd, &read_fd );
|
|
max_fd = maxi( max_fd, c->fd+1);
|
|
|
|
if( ! c->unsent->empty() )
|
|
{
|
|
FD_SET( c->fd, &write_fd );
|
|
}
|
|
}
|
|
|
|
while( 1 )
|
|
{
|
|
res=select( max_fd, &read_fd, &write_fd, 0, 0 );
|
|
|
|
if( quit )
|
|
{
|
|
save();
|
|
exit(0);
|
|
}
|
|
|
|
if( res != -1 )
|
|
break;
|
|
|
|
if( errno != EINTR )
|
|
{
|
|
wperror( L"select" );
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if( FD_ISSET( sock, &read_fd ) )
|
|
{
|
|
if( (child_socket =
|
|
accept( sock,
|
|
(struct sockaddr *)&remote,
|
|
&t) ) == -1) {
|
|
wperror( L"accept" );
|
|
exit(1);
|
|
}
|
|
else
|
|
{
|
|
debug( 4, L"Connected with new child on fd %d", child_socket );
|
|
|
|
if( fcntl( child_socket, F_SETFL, O_NONBLOCK ) != 0 )
|
|
{
|
|
wperror( L"fcntl" );
|
|
close( child_socket );
|
|
}
|
|
else
|
|
{
|
|
connection_t *newc = (connection_t *)malloc( sizeof(connection_t));
|
|
connection_init( newc, child_socket );
|
|
newc->next = conn;
|
|
send( newc->fd, GREETING, strlen(GREETING), MSG_DONTWAIT );
|
|
enqueue_all( newc );
|
|
conn=newc;
|
|
}
|
|
}
|
|
}
|
|
|
|
for( c=conn; c; c=c->next )
|
|
{
|
|
if( FD_ISSET( c->fd, &write_fd ) )
|
|
{
|
|
try_send_all( c );
|
|
}
|
|
}
|
|
|
|
for( c=conn; c; c=c->next )
|
|
{
|
|
if( FD_ISSET( c->fd, &read_fd ) )
|
|
{
|
|
read_message( c );
|
|
|
|
/*
|
|
Occasionally we save during normal use, so that we
|
|
won't lose everything on a system crash
|
|
*/
|
|
update_count++;
|
|
if( update_count >= 64 )
|
|
{
|
|
save();
|
|
update_count = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
connection_t *prev=0;
|
|
c=conn;
|
|
|
|
while( c )
|
|
{
|
|
if( c->killme )
|
|
{
|
|
debug( 4, L"Close connection %d", c->fd );
|
|
|
|
while( ! c->unsent->empty() )
|
|
{
|
|
message_t *msg = c->unsent->front();
|
|
c->unsent->pop();
|
|
msg->count--;
|
|
if( !msg->count )
|
|
free( msg );
|
|
}
|
|
|
|
connection_destroy( c );
|
|
if( prev )
|
|
{
|
|
prev->next=c->next;
|
|
}
|
|
else
|
|
{
|
|
conn=c->next;
|
|
}
|
|
|
|
free(c);
|
|
|
|
c=(prev?prev->next:conn);
|
|
|
|
}
|
|
else
|
|
{
|
|
prev=c;
|
|
c=c->next;
|
|
}
|
|
}
|
|
|
|
if( !conn )
|
|
{
|
|
debug( 0, L"No more clients. Quitting" );
|
|
save();
|
|
env_universal_common_destroy();
|
|
break;
|
|
}
|
|
|
|
}
|
|
halloc_util_destroy();
|
|
}
|
|
|