rucio-clients 37.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rucio-clients might be problematic. Click here for more details.

Files changed (104) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/exception.py +1208 -0
  56. rucio/common/extra.py +31 -0
  57. rucio/common/logging.py +420 -0
  58. rucio/common/pcache.py +1409 -0
  59. rucio/common/plugins.py +185 -0
  60. rucio/common/policy.py +93 -0
  61. rucio/common/schema/__init__.py +200 -0
  62. rucio/common/schema/generic.py +416 -0
  63. rucio/common/schema/generic_multi_vo.py +395 -0
  64. rucio/common/stomp_utils.py +423 -0
  65. rucio/common/stopwatch.py +55 -0
  66. rucio/common/test_rucio_server.py +154 -0
  67. rucio/common/types.py +483 -0
  68. rucio/common/utils.py +1688 -0
  69. rucio/rse/__init__.py +96 -0
  70. rucio/rse/protocols/__init__.py +13 -0
  71. rucio/rse/protocols/bittorrent.py +194 -0
  72. rucio/rse/protocols/cache.py +111 -0
  73. rucio/rse/protocols/dummy.py +100 -0
  74. rucio/rse/protocols/gfal.py +708 -0
  75. rucio/rse/protocols/globus.py +243 -0
  76. rucio/rse/protocols/http_cache.py +82 -0
  77. rucio/rse/protocols/mock.py +123 -0
  78. rucio/rse/protocols/ngarc.py +209 -0
  79. rucio/rse/protocols/posix.py +250 -0
  80. rucio/rse/protocols/protocol.py +361 -0
  81. rucio/rse/protocols/rclone.py +365 -0
  82. rucio/rse/protocols/rfio.py +145 -0
  83. rucio/rse/protocols/srm.py +338 -0
  84. rucio/rse/protocols/ssh.py +414 -0
  85. rucio/rse/protocols/storm.py +195 -0
  86. rucio/rse/protocols/webdav.py +594 -0
  87. rucio/rse/protocols/xrootd.py +302 -0
  88. rucio/rse/rsemanager.py +881 -0
  89. rucio/rse/translation.py +260 -0
  90. rucio/vcsversion.py +11 -0
  91. rucio/version.py +45 -0
  92. rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
  93. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
  94. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
  95. rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
  96. rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
  97. rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
  98. rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
  99. rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
  100. rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
  101. rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
  102. rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  103. rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  104. rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
rucio/common/pcache.py ADDED
@@ -0,0 +1,1409 @@
1
+ #!/usr/bin/env python
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import errno
17
+ import fcntl
18
+ import getopt
19
+ import os
20
+ import re
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ from socket import gethostname
26
+ from typing import TYPE_CHECKING, Any, Optional
27
+ from urllib.parse import urlencode
28
+ from urllib.request import urlopen
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import Iterable, Iterator
32
+ from types import FrameType
33
+ from urllib.parse import _QueryType
34
+
35
+ from _typeshed import StrOrBytesPath
36
+
37
+
38
+ # The pCache Version
39
+ pcacheversion = "4.2.3"
40
+
41
+ # Log message levels
42
+ DEBUG, INFO, WARN, ERROR = "DEBUG", "INFO ", "WARN ", "ERROR"
43
+
44
+ # filename for locking
45
+ LOCK_NAME = ".LOCK"
46
+
47
+ MAXFD = 1024
48
+
49
+ # Session ID
50
+ sessid = "%s.%s" % (int(time.time()), os.getpid())
51
+
52
+
53
+ # Run a command with a timeout
54
+ def run_cmd(args: "subprocess._CMD", timeout: int = 0) -> tuple[int, Optional[bytes]]:
55
+
56
+ class Alarm(Exception):
57
+ pass
58
+
59
+ def alarm_handler(signum: int, frame: Optional["FrameType"]) -> None:
60
+ raise Alarm
61
+
62
+ # Execute the command as a subprocess
63
+ try:
64
+ p = subprocess.Popen(args=args,
65
+ shell=False,
66
+ stdout=subprocess.PIPE,
67
+ stderr=subprocess.STDOUT)
68
+
69
+ except:
70
+ return (-2, None)
71
+
72
+ # Set the timer if a timeout value was given
73
+ if (timeout > 0):
74
+ signal.signal(signal.SIGALRM, alarm_handler)
75
+ signal.alarm(timeout)
76
+
77
+ # Wait for the command to complete
78
+ try:
79
+
80
+ # Collect the output when the command completes
81
+ stdout = p.communicate()[0][:-1]
82
+
83
+ # Command completed in time, cancel the alarm
84
+ if (timeout > 0):
85
+ signal.alarm(0)
86
+
87
+ # Command timed out
88
+ except Alarm:
89
+
90
+ # The pid of our spawn
91
+ pids = [p.pid]
92
+
93
+ # The pids of the spawn of our spawn
94
+ pids.extend(get_process_children(p.pid))
95
+
96
+ # Terminate all of the evil spawn
97
+ for pid in pids:
98
+ try:
99
+ os.kill(pid, signal.SIGKILL)
100
+ except OSError:
101
+ pass
102
+
103
+ # Return a timeout error
104
+ return (-1, None)
105
+
106
+ return (p.returncode, stdout)
107
+
108
+
109
+ def get_process_children(pid: int) -> list[int]:
110
+
111
+ # Get a list of all pids associated with a given pid
112
+ p = subprocess.Popen(args='ps --no-headers -o pid --ppid %d' % pid,
113
+ shell=True,
114
+ stdout=subprocess.PIPE,
115
+ stderr=subprocess.PIPE)
116
+
117
+ # Wait and fetch the stdout
118
+ stdout, stderr = p.communicate()
119
+
120
+ # Return a list of pids as tuples
121
+ return [int(pr) for pr in stdout.split()]
122
+
123
+
124
+ def unitize(x: int) -> str:
125
+
126
+ suff = 'BKMGTPEZY'
127
+
128
+ while ((x >= 1024) and suff):
129
+ y = x / 1024.0
130
+ suff = suff[1:]
131
+ return "%.4g%s" % (y, suff[0])
132
+
133
+
134
+ class Pcache:
135
+
136
+ def usage(self) -> None:
137
+ msg = """Usage: %s [flags] copy_prog [copy_flags] input output""" % self.progname
138
+ sys.stderr.write("%s\n" % msg) # py3, py2
139
+ # print>>sys.stderr, " flags are: "
140
+ # "s:r:m:Cy:A:R:t:r:g:fFl:VvqpH:S:",
141
+ # "scratch-dir=",
142
+ # "storage-root=",
143
+ # "max-space=",
144
+ # "clean",
145
+ # "hysterisis=",
146
+ # "accept=",
147
+ # "reject=",
148
+ # "timeout=",
149
+ # "retry=",
150
+ # "force",
151
+ # "flush-cache",
152
+ # "guid=",
153
+ # "log=",
154
+ # "version",
155
+ # "verbose",
156
+ # "debug",
157
+ # "quiet",
158
+ # "panda",
159
+ # "hostname",
160
+ # "sitename"
161
+
162
+ def __init__(self):
163
+ os.umask(0)
164
+ self.storage_root = "/pnfs"
165
+ self.scratch_dir = "/scratch/"
166
+ self.pcache_dir = self.scratch_dir + "pcache/"
167
+ self.log_file = self.pcache_dir + "pcache.log"
168
+ self.max_space = "80%"
169
+ self.percent_max = None
170
+ self.bytes_max = None
171
+ # self.max_space = "10T"
172
+ self.hysterisis = 0.75
173
+ # self.hysterisis = 0.9
174
+ self.clean = False
175
+ self.transfer_timeout_as_str: str = "600"
176
+ self.max_retries = 3
177
+ self.guid = None
178
+ self.accept_patterns: list[re.Pattern] = []
179
+ self.reject_patterns: list[re.Pattern] = []
180
+ self.force = False
181
+ self.flush = False
182
+ self.verbose = False
183
+ self.quiet = False
184
+ self.debug = False
185
+ self.hostname = None
186
+ self.sitename = None # XXX can we get this from somewhere?
187
+ self.update_panda = False
188
+ self.panda_url = "https://pandaserver.cern.ch:25443/server/panda/"
189
+ self.local_src = None
190
+ self.skip_download = False
191
+
192
+ # internal variables
193
+ self.sleep_interval = 15
194
+ self.force = False
195
+ self.locks = {}
196
+ self.deleted_guids = []
197
+ self.version = pcacheversion
198
+
199
+ def parse_args(self, args: list[str]) -> None:
200
+ # handle pcache flags and leave the rest in self.args
201
+
202
+ try:
203
+ opts, args = getopt.getopt(args,
204
+ "s:x:m:Cy:A:R:t:r:g:fFl:VvdqpPH:S:L:X:",
205
+ ["scratch-dir=",
206
+ "storage-root=",
207
+ "max-space=",
208
+ "clean",
209
+ "hysterisis=",
210
+ "accept=",
211
+ "reject=",
212
+ "timeout=",
213
+ "retry=",
214
+ "force",
215
+ "flush-cache",
216
+ "guid=",
217
+ "log=",
218
+ "version",
219
+ "verbose",
220
+ "debug",
221
+ "quiet",
222
+ "print-stats",
223
+ "panda",
224
+ "hostname",
225
+ "sitename",
226
+ "local-src"])
227
+
228
+ # XXXX cache, stats, reset, clean, delete, inventory
229
+ # TODO: move checksum/size validation from lsm to pcache
230
+ except getopt.GetoptError as err:
231
+ sys.stderr.write("%s\n" % str(err))
232
+ self.usage()
233
+ self.fail(100)
234
+
235
+ for opt, arg in opts:
236
+ if opt in ("-s", "--scratch-dir"):
237
+ self.scratch_dir = arg
238
+ # Make sure scratch_dir endswith /
239
+ if not self.scratch_dir.endswith("/"):
240
+ self.scratch_dir += "/"
241
+ self.pcache_dir = self.scratch_dir + "pcache/"
242
+ self.log_file = self.pcache_dir + "pcache.log"
243
+ elif opt in ("-x", "--storage-root"):
244
+ self.storage_root = arg
245
+ elif opt in ("-m", "--max-space"):
246
+ self.max_space = arg
247
+ elif opt in ("-y", "--hysterisis"):
248
+ if arg.endswith('%'):
249
+ self.hysterisis = float(arg[:-1]) / 100
250
+ else:
251
+ self.hysterisis = float(arg)
252
+ elif opt in ("-A", "--accept"):
253
+ self.accept_patterns.append(re.compile(arg))
254
+ elif opt in ("-R", "--reject"):
255
+ self.reject_patterns.append(re.compile(arg))
256
+ elif opt in ("-t", "--timeout"):
257
+ self.transfer_timeout_as_str = arg
258
+ elif opt in ("-f", "--force"):
259
+ self.force = True
260
+ elif opt in ("-F", "--flush-cache"):
261
+ self.flush = True
262
+ elif opt in ("-C", "--clean"):
263
+ self.clean = True
264
+ elif opt in ("-g", "--guid"):
265
+ if arg == 'None':
266
+ self.guid = None
267
+ else:
268
+ self.guid = arg
269
+ elif opt in ("-r", "--retry"):
270
+ self.max_retries = int(arg)
271
+ elif opt in ("-V", "--version"):
272
+ print(str(self.version))
273
+ sys.exit(0)
274
+ elif opt in ("-l", "--log"):
275
+ self.log_file = arg
276
+ elif opt in ("-v", "--verbose"):
277
+ self.verbose = True
278
+ elif opt in ("-d", "--debug"):
279
+ self.debug = True
280
+ elif opt in ("-q", "--quiet"):
281
+ self.quiet = True
282
+ elif opt in ("-p", "--print-stats"):
283
+ self.print_stats()
284
+ sys.exit(0)
285
+ elif opt in ("-P", "--panda"):
286
+ self.update_panda = True
287
+ elif opt in ("-H", "--hostname"):
288
+ self.hostname = arg
289
+ elif opt in ("-S", "--sitename"):
290
+ self.sitename = arg
291
+ elif opt in ("-L", "--local-src"):
292
+ self.local_src = str(arg)
293
+ elif opt in ("-X", "--skip-download"):
294
+ if str(arg) in ('True', 'true') or arg:
295
+ self.skip_download = True
296
+
297
+ # Treatment of limits on pcache size
298
+ self._convert_max_space()
299
+
300
+ # Convert timeout to seconds
301
+ mult = 1
302
+ t = self.transfer_timeout_as_str
303
+ suff = t[-1]
304
+ if suff in ('H', 'h'):
305
+ mult = 3600
306
+ t = t[:-1]
307
+ elif suff in ('M', 'm'):
308
+ mult = 60
309
+ t = t[:1]
310
+ elif suff in ('S', 's'):
311
+ mult = 1
312
+ t = t[:-1]
313
+ self.transfer_timeout: int = mult * int(t)
314
+
315
+ # Set host and name
316
+ if self.hostname is None:
317
+ self.hostname = gethostname()
318
+ if self.sitename is None:
319
+ self.sitename = os.environ.get("SITE", "") # XXXX
320
+
321
+ # All done
322
+ self.args = args
323
+
324
+ def _convert_max_space(self) -> None:
325
+ '''
326
+ Added by Rucio team. Converts max allowed space usage of pcache into units used by this tool.
327
+ :input self.max_space: limit set by user
328
+ :output self.percent_max: max percentage of pcache space used
329
+ :output self.bytes_max: max size in bytes of pcache space used
330
+ '''
331
+
332
+ # Convert max_space arg to percent_max or bytes_max
333
+ if self.max_space.endswith('%'):
334
+ self.percent_max = float(self.max_space[:-1])
335
+ self.bytes_max = None
336
+ else: # handle suffix
337
+ self.percent_max = None
338
+ m = self.max_space.upper()
339
+ index = "BKMGTPEZY".find(m[-1])
340
+ if index >= 0:
341
+ self.bytes_max = float(m[:-1]) * (1024**index)
342
+ else: # Numeric value w/o units (exception if invalid)
343
+ self.bytes_max = float(self.max_space)
344
+
345
+ def clean_pcache(self, max_space: Optional[str] = None) -> None:
346
+ '''
347
+ Added by Rucio team. Cleans pcache in case it is over limit.
348
+ Used for tests of the pcache functionality. Can be called without other init.
349
+ '''
350
+
351
+ self.t0 = time.time()
352
+ self.progname = "pcache"
353
+
354
+ # set max. occupancy of pcache:
355
+ if max_space:
356
+ self.max_space = max_space
357
+ self._convert_max_space()
358
+
359
+ # Fail on extra args
360
+ if not self.scratch_dir:
361
+ self.usage()
362
+ self.fail(100)
363
+
364
+ # hardcoded pcache dir
365
+ self.pcache_dir = self.scratch_dir + '/pcache/'
366
+
367
+ # clean pcache
368
+ self.maybe_start_cleaner_thread()
369
+
370
+ def check_and_link(
371
+ self,
372
+ src: str = '',
373
+ dst: str = '',
374
+ dst_prefix: str = '',
375
+ scratch_dir: str = '/scratch/',
376
+ storage_root: Optional[str] = None,
377
+ force: bool = False,
378
+ guid: Optional[str] = None,
379
+ log_file: Optional[str] = None,
380
+ version: str = '',
381
+ hostname: Optional[str] = None,
382
+ sitename: Optional[str] = None,
383
+ local_src: Optional[str] = None
384
+ ):
385
+ '''
386
+ Added by Rucio team. Replacement for the main method.
387
+ Checks whether a file is in pcache:
388
+ - if yes: creates a hardlink to the file in pcahce
389
+ - if not:
390
+ - returns 500 and leaves it to Rucio
391
+ - Rucio downloads a file
392
+ Makes hardlink in pcache to downloaded file:
393
+ - needs :param local_src: path to downloaded file
394
+ '''
395
+ self.t0 = time.time()
396
+ self.progname = "pcache"
397
+ self.pcache_dir = scratch_dir + '/pcache/'
398
+ self.src = src
399
+ self.dst = dst
400
+ self.dst_prefix = dst_prefix
401
+ self.sitename = sitename
402
+ self.hostname = hostname
403
+ self.guid = guid
404
+ if log_file:
405
+ self.log_file = log_file
406
+ self.local_src = local_src
407
+ self.version = version
408
+ self.storage_root = storage_root
409
+
410
+ # Cache dir may have been wiped
411
+ if ((not os.path.exists(self.pcache_dir)) and self.update_panda):
412
+ self.panda_flush_cache()
413
+
414
+ # Create the pCache directory
415
+ if (self.mkdir_p(self.pcache_dir)):
416
+ self.fail(101)
417
+
418
+ self.log(INFO, "%s %s invoked as: API", self.progname, self.version)
419
+
420
+ # Fail on extra args
421
+ if not scratch_dir:
422
+ self.usage()
423
+ self.fail(100)
424
+
425
+ # If the source is lfn:, execute original command, no further action
426
+ if (self.src.startswith('lfn:')):
427
+ # status = os.execvp(self.copy_util, self.args)
428
+ os._exit(1)
429
+
430
+ # If the destination is a local file, do some rewrites
431
+ if (self.dst.startswith('file:')):
432
+ self.dst_prefix = 'file:'
433
+ self.dst = self.dst[5:]
434
+ # Leave one '/' on dst
435
+ while ((len(self.dst) > 1) and (self.dst[1] == '/')):
436
+ self.dst_prefix += '/'
437
+ self.dst = self.dst[1:]
438
+
439
+ # load file into pcache
440
+ self.create_pcache_dst_dir()
441
+ # XXXX TODO _ dst_dir can get deleted before lock!
442
+ waited = False
443
+
444
+ # If another transfer is active, lock_dir will block
445
+ if (self.lock_dir(self.pcache_dst_dir, blocking=False)):
446
+ waited = True
447
+ self.log(INFO, "%s locked, waiting", self.pcache_dst_dir)
448
+ self.lock_dir(self.pcache_dst_dir, blocking=True)
449
+
450
+ if (waited):
451
+ self.log(INFO, "waited %.2f secs", time.time() - self.t0)
452
+
453
+ if force:
454
+ self.empty_dir(self.pcache_dst_dir)
455
+
456
+ # The name of the cached version of this file
457
+ cache_file = self.pcache_dst_dir + "data"
458
+
459
+ # Check if the file is in cache or we need to transfer it down
460
+ if (os.path.exists(cache_file)):
461
+ exit_status = 0
462
+ copy_status = None
463
+ self.log(INFO, "check_and_link: file found in cache")
464
+ self.log(INFO, "cache hit %s", self.src)
465
+ self.update_stats("cache_hits")
466
+ self.finish()
467
+ if (os.path.exists(self.dst)):
468
+ copy_status = 1
469
+ elif self.local_src:
470
+ exit_status = 1
471
+ copy_status = None
472
+ self.log(INFO, "check_and_link: local replica found, linking to pcache")
473
+ self.finish()
474
+ else:
475
+ self.log(INFO, "check_and_link: %s file not found in pcache and was not downloaded yet", self.src)
476
+ return (500, None)
477
+
478
+ self.unlock_dir(self.pcache_dst_dir)
479
+ self.log(INFO, "total time %.2f secs", time.time() - self.t0)
480
+
481
+ # in case that the pcache is over limit
482
+ self.maybe_start_cleaner_thread()
483
+
484
+ # Return if the file was cached, copied or an error (and its code)
485
+ return (exit_status, copy_status)
486
+
487
+ def main(self, args: list[str]) -> tuple[int, Optional[int]]:
488
+
489
+ # args
490
+ self.cmdline = ' '.join(args)
491
+ self.t0 = time.time()
492
+ self.progname = args[0] or "pcache"
493
+
494
+ # Must have a list of arguments
495
+ if (self.parse_args(args[1:])):
496
+ self.usage()
497
+ self.fail(100)
498
+
499
+ # Cache dir may have been wiped
500
+ if ((not os.path.exists(self.pcache_dir)) and self.update_panda):
501
+ self.panda_flush_cache()
502
+
503
+ # Create the pCache directory
504
+ if (self.mkdir_p(self.pcache_dir)):
505
+ self.fail(101)
506
+
507
+ self.log(INFO, "%s %s invoked as: %s", self.progname, self.version, self.cmdline)
508
+
509
+ # Are we flushing the cache
510
+ if (self.flush):
511
+ if (self.args):
512
+ sys.stderr.write("--flush not compatible with other options")
513
+ self.fail(100)
514
+ else:
515
+ self.flush_cache()
516
+ sys.exit(0)
517
+
518
+ # Are we cleaning the cache
519
+ if (self.clean):
520
+ # size = self.do_cache_inventory()
521
+ self.maybe_start_cleaner_thread()
522
+ if (len(self.args) < 1):
523
+ sys.exit(0)
524
+
525
+ # Fail on extra args
526
+ if (len(self.args) < 3):
527
+ self.usage()
528
+ self.fail(100)
529
+
530
+ self.copy_util = self.args[0]
531
+ self.copy_args = self.args[1:-2]
532
+ self.src = self.args[-2]
533
+ self.dst = self.args[-1]
534
+ self.dst_prefix = ''
535
+
536
+ # If the source is lfn:, execute original command, no further action
537
+ if (self.src.startswith('lfn:')):
538
+ # status = os.execvp(self.copy_util, self.args)
539
+ os._exit(1)
540
+
541
+ # If the destination is a local file, do some rewrites
542
+ if (self.dst.startswith('file:')):
543
+ self.dst_prefix = 'file:'
544
+ self.dst = self.dst[5:]
545
+ # Leave one '/' on dst
546
+ while ((len(self.dst) > 1) and (self.dst[1] == '/')):
547
+ self.dst_prefix += '/'
548
+ self.dst = self.dst[1:]
549
+
550
+ # Execute original command, no further action
551
+ if (not (self.dst.startswith(self.scratch_dir) and self.accept(self.src) and (not self.reject(self.src)))):
552
+ os.execvp(self.copy_util, self.args) # noqa: S606
553
+ os._exit(1)
554
+
555
+ # XXXX todo: fast-path - try to acquire lock
556
+ # first, if that succeeds, don't call
557
+ # create_pcache_dst_dir
558
+
559
+ # load file into pcache
560
+ self.create_pcache_dst_dir()
561
+ # XXXX TODO _ dst_dir can get deleted before lock!
562
+ waited = False
563
+
564
+ # If another transfer is active, lock_dir will block
565
+ if (self.lock_dir(self.pcache_dst_dir, blocking=False)):
566
+ waited = True
567
+ self.log(INFO, "%s locked, waiting", self.pcache_dst_dir)
568
+ self.lock_dir(self.pcache_dst_dir, blocking=True)
569
+
570
+ if (waited):
571
+ self.log(INFO, "waited %.2f secs", time.time() - self.t0)
572
+
573
+ if (self.force):
574
+ self.empty_dir(self.pcache_dst_dir)
575
+
576
+ # The name of the cached version of this file
577
+ cache_file = self.pcache_dst_dir + "data"
578
+
579
+ # Check if the file is in cache or we need to transfer it down
580
+ if (os.path.exists(cache_file)):
581
+ exit_status = 1
582
+ copy_status = None
583
+ self.log(INFO, "cache hit %s", self.src)
584
+ self.update_stats("cache_hits")
585
+ self.finish()
586
+ else:
587
+ if self.skip_download:
588
+ return (500, None)
589
+ self.update_stats("cache_misses")
590
+ exit_status, copy_status = self.pcache_copy_in()
591
+ if ((exit_status == 0) or (exit_status == 2)):
592
+ self.finish()
593
+
594
+ self.unlock_dir(self.pcache_dst_dir)
595
+ self.log(INFO, "total time %.2f secs", time.time() - self.t0)
596
+
597
+ self.maybe_start_cleaner_thread()
598
+
599
+ # Return if the file was cached, copied or an error (and its code)
600
+ return (exit_status, copy_status)
601
+
602
+ def finish(self, local_src: Optional[str] = None) -> None:
603
+ cache_file = self.pcache_dst_dir + "data"
604
+ self.update_mru()
605
+ if self.local_src:
606
+ if (self.make_hard_link(self.local_src, cache_file)):
607
+ self.fail(102)
608
+ else:
609
+ if (self.make_hard_link(cache_file, self.dst)):
610
+ self.fail(102)
611
+
612
+ def pcache_copy_in(self) -> tuple[int, Optional[int]]:
613
+
614
+ cache_file = self.pcache_dst_dir + "data"
615
+
616
+ # Record source URL
617
+ try:
618
+ fname = self.pcache_dst_dir + "src"
619
+ f = open(fname, 'w')
620
+ f.write(self.src + '\n')
621
+ f.close()
622
+ self.chmod(fname, 0o666)
623
+ except:
624
+ pass
625
+
626
+ # Record GUID if given
627
+ if (self.guid):
628
+ try:
629
+ fname = self.pcache_dst_dir + "guid"
630
+ f = open(fname, 'w')
631
+ f.write(self.guid + '\n')
632
+ f.close()
633
+ self.chmod(fname, 0o666)
634
+ except:
635
+ pass
636
+
637
+ # Try to transfer the file up the the number of retries allowed
638
+ retry = 0
639
+ while (retry <= self.max_retries):
640
+
641
+ # Is this is a retry attempt, log it as such
642
+ if (retry > 0):
643
+ self.log(INFO, "do_transfer: retry %s", retry)
644
+
645
+ # Do the transfer. exit_status will be either
646
+ # 0 - success
647
+ # 3 - Transfer command failed. copy_status has the return code
648
+ # 4 - Transfer command timed out
649
+ # 5 - Transfer command was not found
650
+ exit_status, copy_status = self.do_transfer()
651
+
652
+ # If success, stop trying, otherwise increment the retry count
653
+ if (exit_status == 0):
654
+ break
655
+ retry += 1
656
+
657
+ # Did the transfer succeed
658
+ if (exit_status == 0):
659
+
660
+ # If we succeeded on a retry, return status 2 and the retries
661
+ if (retry == 0):
662
+ copy_status = None
663
+ else:
664
+ exit_status = 2
665
+ copy_status = retry
666
+
667
+ # Update the cache information
668
+ if self.local_src:
669
+ self.update_cache_size(os.stat(self.local_src).st_size)
670
+ else:
671
+ self.update_cache_size(os.stat(cache_file).st_size)
672
+
673
+ # Update the panda cache
674
+ if (self.guid and self.update_panda):
675
+ self.panda_add_cache_files((self.guid,))
676
+
677
+ return (exit_status, copy_status)
678
+
679
+ def create_pcache_dst_dir(self) -> None:
680
+
681
+ d = self.src
682
+ if self.storage_root is not None:
683
+ index = d.find(self.storage_root)
684
+
685
+ if (index >= 0):
686
+ d = d[index:]
687
+ else:
688
+ index = d.find("SFN=")
689
+ if (index >= 0):
690
+ d = d[index + 4:]
691
+
692
+ # self.log(INFO, '%s', self.storage_root)
693
+ # self.log(INFO, '%s', d)
694
+ # XXXX any more patterns to look for?
695
+ d = os.path.normpath(self.pcache_dir + "CACHE/" + d)
696
+ if (not d.endswith('/')):
697
+ d += '/'
698
+
699
+ self.pcache_dst_dir = d
700
+ status = self.mkdir_p(d)
701
+ if (status):
702
+ self.log(ERROR, "mkdir %s %s", d, status)
703
+ self.fail(103)
704
+
705
+ def get_disk_usage(self) -> int:
706
+ p = os.popen("df -P %s | tail -1" % self.pcache_dir, 'r') # noqa: S605
707
+ data = p.read()
708
+ status = p.close()
709
+ if status:
710
+ self.log(ERROR, "get_disk_usage: df command failed, status=%s", status)
711
+ sys.exit(1)
712
+ tok = data.split()
713
+ percent = tok[-2]
714
+ if not percent.endswith('%'):
715
+ self.log(ERROR, "get_disk_usage: cannot parse df output: %s", data)
716
+ sys.exit(1)
717
+ percent = int(percent[:-1])
718
+ return percent
719
+
720
+ def over_limit(self, factor: float = 1.0) -> bool:
721
+ if self.percent_max:
722
+ return self.get_disk_usage() > factor * self.percent_max
723
+ if self.bytes_max:
724
+ cache_size = self.get_cache_size()
725
+ if cache_size is not None:
726
+ return cache_size > factor * self.bytes_max
727
+ return False
728
+
729
+ def clean_cache(self) -> None:
730
+ t0 = time.time()
731
+ cache_size = self.get_cache_size()
732
+
733
+ if cache_size is not None:
734
+ self.log(INFO, "starting cleanup, cache size=%s, usage=%s%%",
735
+ unitize(cache_size),
736
+ self.get_disk_usage())
737
+
738
+ for link in self.list_by_mru():
739
+ try:
740
+ d = os.readlink(link)
741
+
742
+ except OSError as e:
743
+ self.log(ERROR, "readlink: %s", e)
744
+ continue
745
+
746
+ self.log(DEBUG, "deleting %s", d)
747
+
748
+ if os.path.exists(d):
749
+ self.empty_dir(d)
750
+ else:
751
+ self.log(WARN, "Attempt to delete missing file %s", d)
752
+ self.flush_cache()
753
+ break
754
+
755
+ # empty_dir should also delete MRU symlink, but
756
+ # mop up here in there is some problem with the
757
+ # backlink
758
+
759
+ try:
760
+ os.unlink(link)
761
+
762
+ except OSError as e:
763
+ if e.errno != errno.ENOENT:
764
+ self.log(ERROR, "unlink: %s", e)
765
+
766
+ if not self.over_limit(self.hysterisis):
767
+ break
768
+
769
+ self.log(INFO, "cleanup complete, cache size=%s, usage=%s%%, time=%.2f secs",
770
+ self.get_cache_size(),
771
+ self.get_disk_usage(),
772
+ time.time() - t0)
773
+
774
+ def list_by_mru(self) -> "Iterator[str]":
775
+ mru_dir = self.pcache_dir + "MRU/"
776
+ for root, dirs, files in os.walk(mru_dir):
777
+ dirs.sort()
778
+ for d in dirs:
779
+ path = os.path.join(root, d)
780
+ if os.path.islink(path):
781
+ dirs.remove(d)
782
+ yield path
783
+ if files:
784
+ files.sort()
785
+ for file in files:
786
+ path = os.path.join(root, file)
787
+ yield path
788
+
789
+ def flush_cache(self) -> None:
790
+ # Delete everything in CACHE, MRU, and reset stats
791
+ self.log(INFO, "flushing cache")
792
+ if self.update_panda:
793
+ self.panda_flush_cache()
794
+ self.reset_stats()
795
+ ts = '.' + str(time.time())
796
+ for d in "CACHE", "MRU":
797
+ d = self.pcache_dir + d
798
+ try:
799
+ os.rename(d, d + ts)
800
+ os.system("rm -rf %s &" % (d + ts)) # noqa: S605
801
+ except OSError as e:
802
+ if e.errno != errno.ENOENT:
803
+ self.log(ERROR, "%s: %s", d, e)
804
+
805
+ def do_transfer(self) -> tuple[int, Optional[int]]:
806
+
807
+ # Cache file and transfer file locations
808
+ cache_file = self.pcache_dst_dir + "data"
809
+ xfer_file = self.pcache_dst_dir + "xfer"
810
+
811
+ # Remove any transfer file with the same name
812
+ try:
813
+ os.unlink(xfer_file)
814
+ except OSError as e:
815
+ if e.errno != errno.ENOENT:
816
+ self.log(ERROR, "unlink: %s", e)
817
+
818
+ # Build the copy command with the destination into the xfer location
819
+ args = self.args[:]
820
+ args[-1] = self.dst_prefix + xfer_file
821
+
822
+ # Save the current time for timing output
823
+ t0 = time.time()
824
+
825
+ # Do the copy with a timeout
826
+ if self.local_src:
827
+ return (0, None)
828
+ else:
829
+ copy_status, copy_output = run_cmd(args, self.transfer_timeout)
830
+
831
+ # Did the command timeout
832
+ if (copy_status == -1):
833
+ self.log(ERROR, "copy command timed out, elapsed time = %.2f sec", time.time() - t0)
834
+ self.cleanup_failed_transfer()
835
+ return (4, None)
836
+ elif (copy_status == -2):
837
+ self.log(ERROR, "copy command was not found")
838
+ self.cleanup_failed_transfer()
839
+ return (5, None)
840
+
841
+ # Display any output from the copy
842
+ if (copy_output):
843
+ print('%s' % copy_output)
844
+
845
+ # Did the copy succeed (good status and an existing file)
846
+ if ((copy_status > 0) or (not os.path.exists(xfer_file))):
847
+ self.log(ERROR, "copy command failed, elapsed time = %.2f sec", time.time() - t0)
848
+ self.cleanup_failed_transfer()
849
+ return (3, copy_status)
850
+
851
+ self.log(INFO, "copy command succeeded, elapsed time = %.2f sec", time.time() - t0)
852
+
853
+ try:
854
+ os.rename(xfer_file, cache_file)
855
+ # self.log(INFO, "rename %s %s", xfer_file, cache_file)
856
+ except OSError: # Fatal error if we can't do this
857
+ self.log(ERROR, "rename %s %s", xfer_file, cache_file)
858
+ try:
859
+ os.unlink(xfer_file)
860
+ except:
861
+ pass
862
+ self.fail(104)
863
+
864
+ # Make the file readable to all
865
+ self.chmod(cache_file, 0o666)
866
+
867
+ # Transfer completed, return the transfer command status
868
+ return (0, None)
869
+
870
+ def maybe_start_cleaner_thread(self) -> None:
871
+ if not self.over_limit():
872
+ return
873
+ # exit immediately if another cleaner is active
874
+ cleaner_lock = os.path.join(self.pcache_dir, ".clean")
875
+ if self.lock_file(cleaner_lock, blocking=False):
876
+ self.log(INFO, "cleanup not starting: %s locked", cleaner_lock)
877
+ return
878
+ # see http://www.faqs.org/faqs/unix-faq/faq/part3/section-13.html
879
+ # for explanation of double-fork
880
+ pid = os.fork()
881
+ if pid: # parent
882
+ os.waitpid(pid, 0)
883
+ return
884
+ else: # child
885
+ self.daemonize()
886
+ pid = os.fork()
887
+ if pid:
888
+ os._exit(0)
889
+ # grandchild
890
+ self.clean_cache()
891
+ self.unlock_file(cleaner_lock)
892
+ os._exit(0)
893
+
894
+ def make_hard_link(self, src: "StrOrBytesPath", dst: "StrOrBytesPath") -> Optional[int]:
895
+ self.log(INFO, "linking %s to %s", src, dst)
896
+ try:
897
+ if os.path.exists(dst):
898
+ os.unlink(dst)
899
+ os.link(src, dst)
900
+ except OSError as e:
901
+ self.log(ERROR, "make_hard_link: %s", e)
902
+ ret = e.errno
903
+ if ret == errno.ENOENT:
904
+ try:
905
+ stat_info = os.stat(src)
906
+ self.log(INFO, "stat(%s) = %s", src, stat_info)
907
+ except:
908
+ self.log(INFO, "cannot stat %s", src)
909
+ try:
910
+ stat_info = os.stat(dst)
911
+ self.log(INFO, "stat(%s) = %s", dst, stat_info)
912
+ except:
913
+ self.log(INFO, "cannot stat %s", dst)
914
+ return ret
915
+
916
+ def reject(self, name: str) -> bool:
917
+ for pat in self.reject_patterns:
918
+ if pat.search(name):
919
+ return True
920
+ return False
921
+
922
+ def accept(self, name: str) -> bool:
923
+ if not self.accept_patterns:
924
+ return True
925
+ for pat in self.accept_patterns:
926
+ if pat.search(name):
927
+ return True
928
+ return False
929
+
930
+ def get_stat(self, stats_dir: str, stat_name: str) -> int:
931
+ filename = os.path.join(self.pcache_dir, stats_dir, stat_name)
932
+ try:
933
+ f = open(filename, 'r')
934
+ data = int(f.read().strip())
935
+ f.close()
936
+ except:
937
+ data = 0
938
+ return data
939
+
940
+ def print_stats(self) -> None:
941
+ print(("Cache size: %s", unitize(self.get_stat("CACHE", "size"))))
942
+ print(("Cache hits: %s", self.get_stat("stats", "cache_hits")))
943
+ print(("Cache misses: %s", self.get_stat("stats", "cache_misses")))
944
+
945
+ def reset_stats(self) -> None:
946
+ stats_dir = os.path.join(self.pcache_dir, "stats")
947
+ try:
948
+ for f in os.listdir(stats_dir):
949
+ try:
950
+ os.unlink(os.path.join(stats_dir, f))
951
+ except:
952
+ pass
953
+ except:
954
+ pass
955
+ # XXXX error handling
956
+ pass
957
+
958
+ def update_stat_file(self, stats_dir: str, name: str, delta: int) -> None: # internal
959
+ stats_dir = os.path.join(self.pcache_dir, stats_dir)
960
+ self.mkdir_p(stats_dir)
961
+ self.lock_dir(stats_dir)
962
+ stats_file = os.path.join(stats_dir, name)
963
+ try:
964
+ f = open(stats_file, 'r')
965
+ data = f.read()
966
+ f.close()
967
+ value = int(data)
968
+ except:
969
+ # XXXX
970
+ value = 0
971
+ value += delta
972
+ try:
973
+ f = open(stats_file, 'w')
974
+ f.write("%s\n" % value)
975
+ f.close()
976
+ self.chmod(stats_file, 0o666)
977
+ except:
978
+ pass
979
+ # XXX
980
+ self.unlock_dir(stats_dir)
981
+
982
+ def update_stats(self, name: str, delta: int = 1) -> None:
983
+ return self.update_stat_file("stats", name, delta)
984
+
985
+ def update_cache_size(self, bytes_: int) -> None:
986
+ return self.update_stat_file("CACHE", "size", bytes_)
987
+
988
+ def get_cache_size(self) -> Optional[int]:
989
+ filename = os.path.join(self.pcache_dir, "CACHE", "size")
990
+ size = 0
991
+
992
+ try:
993
+ f = open(filename)
994
+ data = f.read()
995
+ size = int(data)
996
+ except:
997
+ pass
998
+
999
+ # If we could not fetch the size, do a reinventory
1000
+ if size == 0:
1001
+ size = self.do_cache_inventory()
1002
+
1003
+ # The size should never be negative, so lets cleanup and start over
1004
+ if size is not None and size < 0:
1005
+ self.log(WARN, "CACHE corruption found. Negative CACHE size: %d", size)
1006
+ self.flush_cache()
1007
+ size = 0
1008
+
1009
+ return size
1010
+
1011
+ def do_cache_inventory(self) -> Optional[int]:
1012
+
1013
+ inventory_lock = os.path.join(self.pcache_dir, ".inventory")
1014
+ if self.lock_file(inventory_lock, blocking=False):
1015
+ return
1016
+
1017
+ size = 0
1018
+
1019
+ self.log(INFO, "starting inventory")
1020
+
1021
+ for root, dirs, files in os.walk(self.pcache_dir):
1022
+ for f in files:
1023
+ if f == "data":
1024
+ fullname = os.path.join(root, f)
1025
+ try:
1026
+ size += os.stat(fullname).st_size
1027
+ except OSError as e:
1028
+ self.log(ERROR, "stat(%s): %s", fullname, e)
1029
+
1030
+ filename = os.path.join(self.pcache_dir, "CACHE", "size")
1031
+
1032
+ try:
1033
+ f = open(filename, 'w')
1034
+ f.write("%s\n" % size)
1035
+ f.close()
1036
+ self.chmod(filename, 0o666)
1037
+ except:
1038
+ pass # XXXX
1039
+
1040
+ self.unlock_file(inventory_lock)
1041
+ self.log(INFO, "inventory complete, cache size %s", size)
1042
+ return size
1043
+
1044
+ def daemonize(self) -> None:
1045
+ if self.debug:
1046
+ return
1047
+ try:
1048
+ os.setsid()
1049
+ except OSError:
1050
+ pass
1051
+ try:
1052
+ os.chdir("/")
1053
+ except OSError:
1054
+ pass
1055
+ try:
1056
+ os.umask(0)
1057
+ except OSError:
1058
+ pass
1059
+ n = os.open("/dev/null", os.O_RDWR)
1060
+ i, o, e = sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno()
1061
+ os.dup2(n, i)
1062
+ os.dup2(n, o)
1063
+ os.dup2(n, e)
1064
+ try:
1065
+ import resource # Resource usage information.
1066
+ maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1067
+ if (maxfd == resource.RLIM_INFINITY):
1068
+ maxfd = MAXFD
1069
+ except:
1070
+ try:
1071
+ maxfd = os.sysconf("SC_OPEN_MAX")
1072
+ except:
1073
+ maxfd = MAXFD # use default value
1074
+
1075
+ for fd in range(0, maxfd + 1):
1076
+ try:
1077
+ os.close(fd)
1078
+ except:
1079
+ pass
1080
+
1081
+ # Panda server callback functions
1082
+ def do_http_post(self, url: str, data: "_QueryType") -> None:
1083
+ # see http://www.faqs.org/faqs/unix-faq/faq/part3/section-13.html
1084
+ # for explanation of double-fork (is it overkill here?)
1085
+ pid = os.fork()
1086
+ if pid: # parent
1087
+ os.waitpid(pid, 0)
1088
+ return
1089
+ else: # child
1090
+ self.daemonize()
1091
+ pid = os.fork()
1092
+ if pid:
1093
+ os._exit(0)
1094
+ # grandchild
1095
+ retry = 0
1096
+ # This will retry for up to 1 hour, at 2 minute intervals
1097
+ while retry < 30:
1098
+ try:
1099
+ u = urlopen(url, data=urlencode(data)) # type: ignore
1100
+ ret = u.read()
1101
+ u.close()
1102
+ self.log(INFO, "http post to %s, retry %s, data='%s', return='%s'",
1103
+ url, retry, data, ret)
1104
+ if ret == "True":
1105
+ break
1106
+ except:
1107
+ exc, msg, tb = sys.exc_info()
1108
+ self.log(ERROR, "post to %s, data=%s, error=%s", url, data, msg)
1109
+ retry += 1
1110
+ time.sleep(120)
1111
+ # finished, don't keep the child thread around!
1112
+ os._exit(0)
1113
+
1114
+ def panda_flush_cache(self) -> None:
1115
+ self.do_http_post(self.panda_url + "flushCacheDB",
1116
+ data={"site": self.sitename,
1117
+ "node": self.hostname})
1118
+
1119
+ def panda_add_cache_files(self, guids: "Iterable[str]") -> None:
1120
+ self.do_http_post(self.panda_url + "addFilesToCacheDB",
1121
+ data={"site": self.sitename,
1122
+ "node": self.hostname,
1123
+ "guids": ','.join(guids)})
1124
+
1125
+ def panda_del_cache_files(self, guids: "Iterable[str]") -> None:
1126
+ self.do_http_post(self.panda_url + "deleteFilesFromCacheDB",
1127
+ data={"site": self.sitename,
1128
+ "node": self.hostname,
1129
+ "guids": ','.join(guids)})
1130
+
1131
+ # Locking functions
1132
+ def lock_dir(self, d: str, create: bool = True, blocking: bool = True) -> Optional[int]:
1133
+ lock_name = os.path.join(d, LOCK_NAME)
1134
+ lock_status = self.lock_file(lock_name, blocking)
1135
+ if (not lock_status): # succeeded
1136
+ return
1137
+ if ((lock_status == errno.ENOENT) and create):
1138
+ mkdir_status = self.mkdir_p(d)
1139
+ if (mkdir_status):
1140
+ self.log(ERROR, "mkdir %s %s", d, mkdir_status)
1141
+ self.fail(105)
1142
+ lock_status = self.lock_file(lock_name, blocking)
1143
+ return lock_status
1144
+
1145
+ def unlock_dir(self, d: str) -> Optional[Any]:
1146
+ return self.unlock_file(os.path.join(d, LOCK_NAME))
1147
+
1148
+ def lock_file(self, name: str, blocking: bool = True) -> Optional[int]:
1149
+ if name in self.locks:
1150
+ self.log(DEBUG, "lock_file: %s already locked", name)
1151
+ return
1152
+ try:
1153
+ f = open(name, 'w')
1154
+ except OSError as e:
1155
+ self.log(ERROR, "open: %s", e)
1156
+ return e.errno
1157
+
1158
+ self.locks[name] = f
1159
+ flag = fcntl.LOCK_EX
1160
+ if not blocking:
1161
+ flag |= fcntl.LOCK_NB
1162
+ while True:
1163
+ try:
1164
+ status = fcntl.lockf(f, flag)
1165
+ break
1166
+ except OSError as e:
1167
+ if e.errno in (errno.EAGAIN, errno.EACCES) and not blocking:
1168
+ f.close()
1169
+ del self.locks[name]
1170
+ return e.errno
1171
+ if e.errno != errno.EINTR:
1172
+ status = e.errno
1173
+ self.log(ERROR, "lockf: %s", e)
1174
+ self.fail(106)
1175
+ return status
1176
+
1177
+ def unlock_file(self, name: str) -> Optional[Any]:
1178
+ f = self.locks.get(name)
1179
+ if not f:
1180
+ self.log(DEBUG, "unlock_file: %s not locked", name)
1181
+ return
1182
+
1183
+ # XXXX does this create a possible race condition?
1184
+ if 0:
1185
+ try:
1186
+ os.unlink(name)
1187
+ except:
1188
+ pass
1189
+ status = fcntl.lockf(f, fcntl.LOCK_UN)
1190
+ f.close()
1191
+ del self.locks[name]
1192
+ return status
1193
+
1194
+ def unlock_all(self) -> None:
1195
+ for filename, f in list(self.locks.items()):
1196
+ try:
1197
+ f.close()
1198
+ os.unlink(filename)
1199
+ except:
1200
+ pass
1201
+
1202
+ # Cleanup functions
1203
+ def delete_file_and_parents(self, name: str) -> None:
1204
+ try:
1205
+ os.unlink(name)
1206
+ except OSError as e:
1207
+ if e.errno != errno.ENOENT:
1208
+ self.log(ERROR, "unlink: %s", e)
1209
+ self.fail(107)
1210
+ self.delete_parents_recursive(name)
1211
+
1212
+ def delete_parents_recursive(self, name: str) -> None: # internal
1213
+ try:
1214
+ dirname = os.path.dirname(name)
1215
+ if not os.listdir(dirname):
1216
+ os.rmdir(dirname)
1217
+ self.delete_parents_recursive(dirname)
1218
+ except OSError as e:
1219
+ self.log(DEBUG, "delete_parents_recursive: %s", e)
1220
+
1221
+ def update_mru(self) -> None:
1222
+ now = time.time()
1223
+ link_to_mru = self.pcache_dst_dir + "mru"
1224
+ if os.path.exists(link_to_mru):
1225
+ link = os.readlink(link_to_mru)
1226
+ self.delete_file_and_parents(link)
1227
+
1228
+ try:
1229
+ os.unlink(link_to_mru)
1230
+ except OSError as e:
1231
+ if e.errno != errno.ENOENT:
1232
+ self.log(ERROR, "unlink: %s", e)
1233
+ self.fail(108)
1234
+
1235
+ mru_dir = self.pcache_dir + "MRU/" + time.strftime("%Y/%m/%d/%H/%M/",
1236
+ time.localtime(now))
1237
+
1238
+ self.mkdir_p(mru_dir)
1239
+
1240
+ # getting symlink
1241
+ name = "%.3f" % (now % 60)
1242
+ ext = 0
1243
+ while True:
1244
+ if ext:
1245
+ link_from_mru = "%s%s-%s" % (mru_dir, name, ext)
1246
+ else:
1247
+ link_from_mru = "%s%s" % (mru_dir, name)
1248
+
1249
+ try:
1250
+ os.symlink(self.pcache_dst_dir, link_from_mru)
1251
+ break
1252
+ except OSError as e:
1253
+ if e.errno == errno.EEXIST:
1254
+ ext += 1 # add an extension & retry if file exists
1255
+ continue
1256
+ else:
1257
+ self.log(ERROR, "symlink: %s %s", e, link_from_mru)
1258
+ self.fail(109)
1259
+
1260
+ while True:
1261
+ try:
1262
+ os.symlink(link_from_mru, link_to_mru)
1263
+ break
1264
+ except OSError as e:
1265
+ if e.errno == errno.EEXIST:
1266
+ try:
1267
+ os.unlink(link_to_mru)
1268
+ except OSError as e:
1269
+ if e.errno != errno.ENOENT:
1270
+ self.log(ERROR, "unlink: %s %s", e, link_to_mru)
1271
+ self.fail(109)
1272
+ else:
1273
+ self.log(ERROR, "symlink: %s %s", e, link_from_mru)
1274
+ self.fail(109)
1275
+
1276
+ def cleanup_failed_transfer(self) -> None:
1277
+ try:
1278
+ os.unlink(self.pcache_dir + 'xfer')
1279
+ except:
1280
+ pass
1281
+
1282
+ def empty_dir(self, d: str) -> None:
1283
+ status = None
1284
+ bytes_deleted = 0
1285
+ for name in os.listdir(d):
1286
+ size = 0
1287
+ fullname = os.path.join(d, name)
1288
+ if name == "data":
1289
+ try:
1290
+ size = os.stat(fullname).st_size
1291
+ except OSError as e:
1292
+ if e.errno != errno.ENOENT:
1293
+ self.log(WARN, "stat: %s", e)
1294
+ elif name == "guid":
1295
+ try:
1296
+ guid = open(fullname).read().strip()
1297
+ self.deleted_guids.append(guid)
1298
+ except:
1299
+ pass # XXXX
1300
+ elif name == "mru" and os.path.islink(fullname):
1301
+ try:
1302
+ mru_file = os.readlink(fullname)
1303
+ os.unlink(fullname)
1304
+ self.delete_file_and_parents(mru_file)
1305
+ except OSError as e:
1306
+ if e.errno != errno.ENOENT:
1307
+ self.log(WARN, "empty_dir: %s", e)
1308
+ try:
1309
+ if self.debug:
1310
+ print(("UNLINK %s", fullname))
1311
+ os.unlink(fullname)
1312
+ bytes_deleted += size
1313
+ except OSError as e:
1314
+ if e.errno != errno.ENOENT:
1315
+ self.log(WARN, "empty_dir2: %s", e)
1316
+ # self.fail()
1317
+ self.update_cache_size(-bytes_deleted)
1318
+ self.delete_parents_recursive(d)
1319
+ return status
1320
+
1321
+ def chmod(self, path: str, mode: int) -> None:
1322
+ try:
1323
+ os.chmod(path, mode)
1324
+ except OSError as e:
1325
+ if e.errno != errno.EPERM: # Cannot chmod files we don't own!
1326
+ self.log(ERROR, "chmod %s %s", path, e)
1327
+
1328
+ def mkdir_p(self, d: str, mode: int = 0o777) -> Optional[int]:
1329
+ # Thread-safe
1330
+ try:
1331
+ os.makedirs(d, mode)
1332
+ return 0
1333
+ except OSError as e:
1334
+ if e.errno == errno.EEXIST:
1335
+ pass
1336
+ else:
1337
+ # Don't use log here, log dir may not exist
1338
+ sys.stderr.write("%s\n" % str(e))
1339
+ return e.errno
1340
+
1341
+ def log(self, level: str, msg: str, *args) -> None:
1342
+
1343
+ # Disable all logging
1344
+ if (self.quiet):
1345
+ return
1346
+
1347
+ if ((level == DEBUG) and (not self.debug)):
1348
+ return
1349
+
1350
+ msg = "%s %s %s %s %s\n" % (time.strftime("%F %H:%M:%S"),
1351
+ sessid,
1352
+ self.hostname,
1353
+ level,
1354
+ str(msg) % args)
1355
+
1356
+ try:
1357
+ f = open(self.log_file, "a+", 0o666)
1358
+ f.write(msg)
1359
+ f.close()
1360
+
1361
+ except Exception as e:
1362
+ sys.stderr.write("%s\n" % str(e))
1363
+ sys.stderr.write(msg)
1364
+ sys.stderr.flush()
1365
+
1366
+ if (self.debug or self.verbose or (level == ERROR)):
1367
+ sys.stderr.write(msg)
1368
+ sys.stderr.flush()
1369
+
1370
+ def fail(self, errcode: int = 1) -> None:
1371
+ self.unlock_all()
1372
+ sys.exit(errcode)
1373
+
1374
+ ##################################################################################
1375
+
1376
+ # pCache exit_status will be
1377
+ #
1378
+ # 0 - File was transferred into cache and is ready
1379
+ # 1 - File is cached and ready to use
1380
+ # 2 - File was transferred but with a retry (copy_status has the retry count)
1381
+ # 3 - Transfer command failed (copy_status has the transfer return code)
1382
+ # 4 - Transfer command timed out
1383
+ #
1384
+ # 100 - Usage error
1385
+ # 101 - Cache directory does not exist
1386
+ # 102 - Cache hard link error
1387
+ # 103 - Cache destination mkdir error
1388
+ # 104 - Cache rename error
1389
+ # 105 - Cache locking error
1390
+ # 106 - Cache file locking error
1391
+ # 107 - Cache cleanup error
1392
+ # 108 - Cache MRU update error
1393
+ # 109 - Cache MRU link error
1394
+ # 500 - Is file in pcache? No other action
1395
+
1396
+
1397
+ if (__name__ == "__main__"):
1398
+
1399
+ # Load pCache
1400
+ p = Pcache()
1401
+
1402
+ # Save the passed arguments
1403
+ args = sys.argv
1404
+
1405
+ # Find the file
1406
+ exit_status, copy_status = p.main(args)
1407
+
1408
+ # Take us home percy...
1409
+ sys.exit(exit_status)