From b19f4584736c99ed9b8586c4e6b71d01f8325854 Mon Sep 17 00:00:00 2001 From: Nils <> Date: Sat, 5 Mar 2022 11:35:34 +0100 Subject: [PATCH] Change nsmd to follow XDG Base Directory Specification. closes #gh-15 . Repair and extend lockfiles as well. fixes #gh-31 --- src/file.cpp | 34 ++++------- src/file.h | 3 +- src/nsmd.cpp | 162 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 151 insertions(+), 48 deletions(-) diff --git a/src/file.cpp b/src/file.cpp index dd01621..0480af9 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -65,32 +65,18 @@ exists ( const char *name ) return 0 == stat( name, &st ); } -bool -acquire_lock ( int *lockfd, const char *filename ) -{ - struct flock fl; - - fl.l_type = F_WRLCK; - fl.l_whence = SEEK_SET; - fl.l_start = 0; - fl.l_len = 0; - - *lockfd = ::creat( filename, 0777 ); - - if ( fcntl( *lockfd, F_SETLK, &fl ) != 0 ) - return false; - - return true; -} - -void -release_lock ( int *lockfd, const char *filename ) -{ - unlink( filename ); - ::close( *lockfd ); +char * +simple_hash( const char *s ) +{ //djb2 + unsigned long hashAddress = 5381; + for ( int counter = 0; s[counter]!='\0'; counter++ ) { + hashAddress = ( (hashAddress << 5) + hashAddress ) + s[counter]; + } - *lockfd = 0; + char *result = NULL; + asprintf( &result, "%lu", hashAddress % 65521 ); + return result; } int diff --git a/src/file.h b/src/file.h index d7b3470..eee7048 100644 --- a/src/file.h +++ b/src/file.h @@ -26,8 +26,7 @@ unsigned long modification_time ( const char *file ); bool newer ( const char *file1, const char *file2 ); unsigned long size ( const char *file ); int exists ( const char *name ); -bool acquire_lock ( int *lockfd, const char *filename ); -void release_lock ( int *lockfd, const char *filename ); +char * simple_hash( const char *s); int backwards_fgetc ( FILE *fp ); char * backwards_afgets ( FILE *fp ); void touch ( int fd ); diff --git a/src/nsmd.cpp b/src/nsmd.cpp index 02c6508..284249c 100644 --- a/src/nsmd.cpp +++ b/src/nsmd.cpp @@ -62,13 +62,13 @@ static lo_address gui_addr; static bool gui_is_active = false; static int signal_fd; -static int session_lock_fd = 0; static char *session_root; +static char *lockfile_directory; #define NSM_API_VERSION_MAJOR 1 #define NSM_API_VERSION_MINOR 1 #define NSM_API_VERSION_PATCH 1 -#define VERSION_STRING "1.5.3" +#define VERSION_STRING "1.6.0" #define ERR_OK 0 #define ERR_GENERAL_ERROR -1 @@ -665,6 +665,7 @@ get_client_project_path ( const char *session_path, Client *c ) return client_project_path; } + bool launch ( const char *executable, const char *client_id ) { @@ -1238,6 +1239,44 @@ command_client_to_quit ( Client *c ) } } +char * +get_lock_file_name( const char * session_name ) +{ + char *session_root_hash = simple_hash( session_root ); // To avoid collisions of two nsmd running on different session_root with the same simple-named session. + + char *session_lock; + asprintf( &session_lock, "%s/%s%s", lockfile_directory, session_name, session_root_hash ); //lockfile_directory and session_name are variables in the current context. + + free(session_root_hash); + return session_lock; +} + + +void +write_lock_file( const char *filename, const char * session_path ) +{ + //Not a GNU lockfile, which features were never used by nsmd anyway, + //but simply a file with information about the NSM Server and the loaded session + FILE *fp = fopen( filename, "w" ); + + if ( !fp ) { + FATAL( "Failed to write lock file to %s with error: %s", filename, strerror( errno ) ); + } + + fprintf( fp, "%s\n%s\n%d\n", session_path, osc_server->url(), getpid()); + MESSAGE( "Created lock file %s", filename ); + + fclose( fp ); +} + +void +delete_lock_file( const char *filename ) +{ + unlink( filename ); + MESSAGE( "Deleted lock file %s", filename ); +} + + void close_session ( ) { @@ -1259,13 +1298,11 @@ close_session ( ) if ( session_path ) { - char *session_lock; - asprintf( &session_lock, "%s/.lock", session_path ); - - release_lock( &session_lock_fd, session_lock ); + char * session_lock = get_lock_file_name(session_name); + delete_lock_file( session_lock ); + MESSAGE( "Session %s was closed.", session_path ); free(session_lock); - free(session_path); session_path = NULL; free(session_name); @@ -1305,17 +1342,29 @@ tell_all_clients_session_is_loaded ( void ) int load_session_file ( const char * path ) { + + if ( session_path && session_name ) { + //We are already in a session. This is switch, or load during duplicate etc. + MESSAGE ( "Instructed to load %s while %s is still open. This is a normal operation. Attempting to switch clients intelligently, if they support it. Otherwise closing and re-opening.", path, session_path ); + char * session_lock = get_lock_file_name(session_name); + delete_lock_file( session_lock ); + } + + set_name( path ); //Do this first so we have the name for lockfiles and log messages + char *session_file = NULL; asprintf( &session_file, "%s/session.nsm", path ); - char *session_lock = NULL; - asprintf( &session_lock, "%s/.lock", path ); - if ( ! acquire_lock( &session_lock_fd, session_lock ) ) + //Check if the lockfile already exists, which means another nsmd currently has loaded the session we want to load. + char * session_lock = get_lock_file_name(session_name); + struct stat st_lockfile_exists_check; + if ( stat (session_lock, &st_lockfile_exists_check) == 0) { + WARNING( "Session %s is already loaded from another nsmd and locked by file %s", session_name, session_lock ); + free( session_file ); free( session_lock ); - WARNING( "Session is locked by another process" ); return ERR_SESSION_LOCKED; } @@ -1332,7 +1381,6 @@ load_session_file ( const char * path ) session_path = strdup( path ); - set_name( path ); std::list new_clients; @@ -1456,7 +1504,11 @@ load_session_file ( const char * path ) tell_all_clients_session_is_loaded(); - MESSAGE( "Loaded." ); + //We already checked if the logfile exists above, and it didn't. + //We also tested for write permissions to our XDG run-dir, which we confirmed to have. + //We can create the lockfile now. + write_lock_file( session_lock, session_path ); + MESSAGE( "Session %s was loaded.", session_path); new_clients.clear(); @@ -1567,7 +1619,9 @@ OSC_HANDLER( duplicate ) osc_server->send( gui_addr, "/nsm/gui/session/session", &argv[0]->s ); - MESSAGE( "Attempting to open %s", spath ); + MESSAGE( "Attempting to open during DUPLICATE: %s", spath ); + + //The original session is still open. load_session_file will close it, and possibly ::switch:: if ( !load_session_file( spath ) ) { @@ -1653,6 +1707,11 @@ OSC_HANDLER( new ) set_name( session_path ); + char * session_lock = get_lock_file_name(session_name); + write_lock_file( session_lock, session_path ); + free ( session_lock ); + + osc_server->send( sender_addr, "/reply", path, "Created." ); if ( gui_is_active ) @@ -2474,7 +2533,8 @@ handle_signal_clean_exit ( int signal ) MESSAGE( "Caught SIGNAL %i. Stopping nsmd.", signal); // We want a clean exit even when things go wrong. close_session(); - free(session_root); + free( session_root ); + free( lockfile_directory ); exit(0); } @@ -2572,7 +2632,7 @@ int main(int argc, char *argv[]) " --help Show this screen\n" " --version Show version\n" " --osc-port portnum OSC port number [Default: provided by system].\n" - " --session-root path Base path for sessions [Default: ~/NSM Sessions].\n" + " --session-root path Base path for sessions [Default: $XDG_DATA_HOME/nsm/].\n" " --load-session name Load existing session [Example: \"My Song\"].\n" " --gui-url url Connect to running legacy-gui [Example: osc.udp://mycomputer.localdomain:38356/].\n" " --detach Detach from console.\n" @@ -2587,19 +2647,77 @@ int main(int argc, char *argv[]) } } - if ( !session_root ) + + //Get the XDG runtime directory for lockfiles + //https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + //Unlike $XDG_DATA_HOME the runtime env var must be set, usually to /run/user/1000/ + struct stat rundir_check; + lockfile_directory = getenv( "XDG_RUNTIME_DIR" ); + if ( stat( lockfile_directory, &rundir_check ) != 0 && S_ISDIR(rundir_check.st_mode)) { + FATAL( "Failed to access $XDG_RUNTIME_DIR directory %s with error: %s", lockfile_directory, strerror( errno ) ); + } + else { + asprintf( &lockfile_directory, "%s/%s", lockfile_directory, "nsm"); + + //Create the 'nsm' subdirectory. This may fail on it's own. + struct stat st_lockfile_dir_mkdir; + if ( stat( lockfile_directory, &st_lockfile_dir_mkdir ) ) + { + if ( mkdir( lockfile_directory, 0771 ) ) + { + FATAL( "Failed to create lock file directory %s with error: %s", lockfile_directory, strerror( errno ) ); + } + } + MESSAGE( "Using %s for lock-files.", lockfile_directory ); + } + + + + if ( !session_root ) { + /* The user gave no specific session directory. We use the default. + * The default dir follows the XDG Basedir Specifications: + * It is used by looking up environment variables. + * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + * $XDG_DATA_HOME defines the base directory relative to which user-specific data files + * should be stored. If $XDG_DATA_HOME is either not set or empty, a default equal to + * $HOME/.local/share should be used. + * + * Up to version 1.5.3 the default dir was ~/NSM Sessions . + * If this old directory exists we will use it but write mild warning to the log. + * Moving old sessions is left to the user, or an external GUI. + */ + struct stat st_session_root; + asprintf( &session_root, "%s/%s", getenv( "HOME" ), "NSM Sessions" ); + if ( stat( session_root, &st_session_root ) == 0 && S_ISDIR(st_session_root.st_mode)) { + WARNING ( "An old session directory was detected in %s. You can continue to use it but it is recommended to move your sessions to $XDG_DATA_HOME/nsm-sessions/. If you don't know where that is simply rename your current session-directory and start nsmd, which will tell you the new directory.", session_root); + } + else { + const char *xdg_data_home = getenv( "XDG_DATA_HOME" ); + if ( xdg_data_home ) { + //If $XDG_DATA_HOME is explicitly set by the user we assume it to exist. We don't want to recursively create system directories. + //If the xdg-dir does not exist yet we FATAL out just below. + asprintf( &session_root, "%s/%s", xdg_data_home, "nsm" ); + } + else { + //If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. + asprintf( &session_root, "%s/.local/share/nsm", getenv( "HOME" )); + } + } + } - struct stat st; + struct stat st_session_root_mkdir; - if ( stat( session_root, &st ) ) + if ( stat( session_root, &st_session_root_mkdir ) ) { if ( mkdir( session_root, 0771 ) ) { - FATAL( "Failed to create session directory: %s", strerror( errno ) ); + FATAL( "Failed to create session directory %s with error: %s", session_root, strerror( errno ) ); } } + + MESSAGE( "Session root is: %s", session_root ); osc_server = new OSC::Endpoint(); @@ -2660,7 +2778,7 @@ int main(int argc, char *argv[]) { char *spath; asprintf( &spath, "%s/%s", session_root, load_session); // Build the session path. --load-session works with --session-root - MESSAGE( "LOAD SESSION %s", spath); + MESSAGE( "Loading session given by parameter %s", spath); load_session_file( spath ); free ( spath ); } @@ -2697,7 +2815,7 @@ int main(int argc, char *argv[]) //Without a signal handler clients will remain active ("zombies") without nsmd as parent. //MESSAGE ( "End of Program"); - //free(session_root);// This was not executed if nsmd received a stop signal. It is now handled by doExit() + //free(session_root);// This was not executed if nsmd received a stop signal. It is now handled by handle_signal_clean_exit() //osc_server->run(); return 0;