@@ -25,6 +25,7 @@ import (
2525 "path/filepath"
2626 "regexp"
2727 "runtime"
28+ "sort"
2829 "strings"
2930 "sync"
3031
@@ -47,6 +48,28 @@ func resetPluginSearchPathsForTesting() {
4748 pluginSearchPaths = nil
4849}
4950
51+ // deduplicatePaths resolves paths to absolute paths and removes duplicates while preserving order.
52+ func deduplicatePaths (paths []string ) []string {
53+ seen := make (map [string ]bool )
54+ var result []string
55+ for _ , path := range paths {
56+ // Resolve to absolute path
57+ absPath , err := filepath .Abs (path )
58+ if err != nil {
59+ // If we can't resolve, use the original path
60+ absPath = path
61+ }
62+ // Clean the path to normalize it
63+ absPath = filepath .Clean (absPath )
64+ // Add if not seen before
65+ if ! seen [absPath ] {
66+ seen [absPath ] = true
67+ result = append (result , absPath )
68+ }
69+ }
70+ return result
71+ }
72+
5073// getPluginSearchPaths returns a list of directories to search for XRootD plugins.
5174// It includes standard library paths and paths from environment variables.
5275func getPluginSearchPaths () []string {
@@ -87,7 +110,6 @@ func getPluginSearchPaths() []string {
87110 }
88111 }
89112
90- appendEnvPaths ("XRD_PLUGINPATH" )
91113 appendEnvPaths ("LD_LIBRARY_PATH" )
92114 appendEnvPaths ("DYLD_LIBRARY_PATH" )
93115 appendEnvPaths ("DYLD_FALLBACK_LIBRARY_PATH" )
@@ -101,25 +123,7 @@ func getPluginSearchPaths() []string {
101123 searchPaths = append (searchPaths , getXRootDRPaths ()... )
102124
103125 // Resolve absolute paths and remove duplicates while preserving order
104- seen := make (map [string ]bool )
105- var result []string
106- for _ , path := range searchPaths {
107- // Resolve to absolute path
108- absPath , err := filepath .Abs (path )
109- if err != nil {
110- // If we can't resolve, use the original path
111- absPath = path
112- }
113- // Clean the path to normalize it
114- absPath = filepath .Clean (absPath )
115- // Add if not seen before
116- if ! seen [absPath ] {
117- seen [absPath ] = true
118- result = append (result , absPath )
119- }
120- }
121-
122- pluginSearchPaths = result
126+ pluginSearchPaths = deduplicatePaths (searchPaths )
123127 })
124128
125129 return pluginSearchPaths
@@ -210,6 +214,118 @@ func getXRootDRPaths() []string {
210214 return paths
211215}
212216
217+ // getClientPluginPaths parses XRootD client plugin configuration files and returns
218+ // directories containing absolute library paths from enabled plugins.
219+ // Checks in order: /etc/xrootd/client.plugins.d/, ~/.xrootd/client.plugins.d/,
220+ // and directory pointed to by XRD_PLUGINCONFDIR.
221+ func getClientPluginPaths () []string {
222+ paths := []string {}
223+ configDirs := []string {}
224+
225+ // Standard directories
226+ configDirs = append (configDirs , "/etc/xrootd/client.plugins.d/" )
227+
228+ // User directory
229+ if homeDir , err := os .UserHomeDir (); err == nil {
230+ configDirs = append (configDirs , filepath .Join (homeDir , ".xrootd" , "client.plugins.d" ))
231+ }
232+
233+ // XRD_PLUGINCONFDIR environment variable
234+ if pluginConfDir := os .Getenv ("XRD_PLUGINCONFDIR" ); pluginConfDir != "" {
235+ configDirs = append (configDirs , pluginConfDir )
236+ }
237+
238+ // Parse each directory
239+ for _ , dir := range configDirs {
240+ paths = append (paths , parseClientPluginDir (dir )... )
241+ }
242+
243+ return paths
244+ }
245+
246+ // parseClientPluginDir reads plugin configuration files from a directory
247+ // and extracts directories from absolute lib paths of enabled plugins.
248+ func parseClientPluginDir (dir string ) []string {
249+ paths := []string {}
250+
251+ entries , err := os .ReadDir (dir )
252+ if err != nil {
253+ return paths
254+ }
255+
256+ // Sort entries alphabetically as per xrootd behavior
257+ fileNames := make ([]string , 0 , len (entries ))
258+ for _ , entry := range entries {
259+ if ! entry .IsDir () {
260+ fileNames = append (fileNames , entry .Name ())
261+ }
262+ }
263+ sort .Strings (fileNames )
264+
265+ // Process files in alphabetical order
266+ for _ , fileName := range fileNames {
267+ filePath := filepath .Join (dir , fileName )
268+ libPath , enabled := parsePluginConfigFile (filePath )
269+
270+ // Only consider paths, not filenames; linker will be handed
271+ // filenames and will search in standard paths.
272+ if enabled {
273+ libDir := filepath .Dir (libPath )
274+ // Only add if libPath contains a directory component (not just a bare filename)
275+ if libDir != "." && libDir != "" {
276+ // For absolute paths, use as-is; for relative paths, resolve against config dir
277+ if ! filepath .IsAbs (libDir ) {
278+ libDir = filepath .Join (dir , libDir )
279+ }
280+ paths = append (paths , libDir )
281+ }
282+ }
283+ }
284+
285+ return paths
286+ }
287+
288+ // parsePluginConfigFile parses a single plugin configuration file
289+ // and returns the lib path and whether it's enabled.
290+ func parsePluginConfigFile (filePath string ) (string , bool ) {
291+ file , err := os .Open (filePath )
292+ if err != nil {
293+ return "" , false
294+ }
295+ defer file .Close ()
296+
297+ var libPath string
298+ enabled := false
299+
300+ scanner := bufio .NewScanner (file )
301+ for scanner .Scan () {
302+ line := strings .TrimSpace (scanner .Text ())
303+ // Skip empty lines and comments
304+ if line == "" || strings .HasPrefix (line , "#" ) {
305+ continue
306+ }
307+
308+ // Parse key=value pairs
309+ parts := strings .SplitN (line , "=" , 2 )
310+ if len (parts ) != 2 {
311+ continue
312+ }
313+
314+ key := strings .TrimSpace (parts [0 ])
315+ value := strings .TrimSpace (parts [1 ])
316+
317+ switch key {
318+ case "lib" :
319+ libPath = value
320+ case "enable" :
321+ // Accept various forms of enabled
322+ enabled = value == "true" || value == "1" || value == "yes"
323+ }
324+ }
325+
326+ return libPath , enabled
327+ }
328+
213329// getXRootDVersion runs 'xrootd -v' and extracts the major version number
214330func getXRootDVersion () string {
215331 xrootdVersionOnce .Do (func () {
@@ -242,6 +358,9 @@ func getPluginVariants(baseName string) []string {
242358
243359 // XRootD uses .so extension on all platforms, including macOS
244360 exts := []string {".so" }
361+ if runtime .GOOS == "darwin" {
362+ exts = append (exts , ".dylib" )
363+ }
245364
246365 // Strip any existing extension from baseName
247366 nameWithoutExt := baseName
@@ -272,10 +391,17 @@ func getPluginVariants(baseName string) []string {
272391 return variants
273392}
274393
275- // CheckPluginExists checks if a plugin exists in any of the standard library search paths.
394+ // checkPluginExists checks if a plugin exists in any of the standard library search paths.
395+ // If includeClientPaths is true, also includes XRootD client plugin configuration directories.
276396// It returns true if the plugin is found, false otherwise.
277- func CheckPluginExists (pluginName string ) bool {
397+ func checkPluginExists (pluginName string , includeClientPaths bool ) bool {
278398 searchPaths := getPluginSearchPaths ()
399+
400+ if includeClientPaths {
401+ searchPaths = append (searchPaths , getClientPluginPaths ()... )
402+ searchPaths = deduplicatePaths (searchPaths )
403+ }
404+
279405 variants := getPluginVariants (pluginName )
280406
281407 for _ , dir := range searchPaths {
@@ -298,22 +424,22 @@ func ValidateRequiredPlugins(isOrigin bool, xrdConfig *XrootdConfig) error {
298424 if isOrigin {
299425 // Check for libXrdHttpPelican if drop privileges is enabled
300426 if xrdConfig .Server .DropPrivileges {
301- if ! CheckPluginExists ("libXrdHttpPelican.so" ) {
427+ if ! checkPluginExists ("libXrdHttpPelican.so" , false ) {
302428 missingPlugins = append (missingPlugins , "libXrdHttpPelican.so" )
303429 }
304430 }
305431
306432 // Check for libXrdS3 if using S3 storage type
307433 if xrdConfig .Origin .StorageType == "s3" {
308- if ! CheckPluginExists ("libXrdS3.so" ) {
434+ if ! checkPluginExists ("libXrdS3.so" , false ) {
309435 missingPlugins = append (missingPlugins , "libXrdS3.so" )
310436 }
311437 }
312438 } else {
313439 // Cache-specific checks
314440 // Check for libXrdHttpPelican if drop privileges is enabled
315441 if xrdConfig .Server .DropPrivileges {
316- if ! CheckPluginExists ("libXrdHttpPelican.so" ) {
442+ if ! checkPluginExists ("libXrdHttpPelican.so" , false ) {
317443 missingPlugins = append (missingPlugins , "libXrdHttpPelican.so" )
318444 }
319445 }
@@ -322,7 +448,8 @@ func ValidateRequiredPlugins(isOrigin bool, xrdConfig *XrootdConfig) error {
322448 // The cache configuration writes client plugin settings to the cache-client.plugins.d directory
323449 // (see CheckCacheEnv in xrootd_config.go), which configures XRootD to use libXrdClPelican.so
324450 // for handling pelican:// protocol requests.
325- if ! CheckPluginExists ("libXrdClPelican.so" ) {
451+ // Include client plugin configuration paths for this check.
452+ if ! checkPluginExists ("libXrdClPelican.so" , true ) {
326453 missingPlugins = append (missingPlugins , "libXrdClPelican.so" )
327454 }
328455 }
0 commit comments