]>
git.ipfire.org Git - thirdparty/xfsprogs-dev.git/blob - scrub/xfs_scrub_all.in
3 # SPDX-License-Identifier: GPL-2.0-or-later
4 # Copyright (C) 2018-2024 Oracle. All rights reserved.
6 # Author: Darrick J. Wong <djwong@kernel.org>
8 # Run online scrubbers in parallel, but avoid thrashing.
17 from io
import TextIOWrapper
23 '''Return /dev/null in subprocess writable format.'''
25 from subprocess
import DEVNULL
28 return open(os
.devnull
, 'wb')
31 '''Map mountpoints to physical disks.'''
32 def find_xfs_mounts(bdev
, fs
, lastdisk
):
33 '''Attach lastdisk to each fs found under bdev.'''
34 if bdev
['fstype'] == 'xfs' and bdev
['mountpoint'] is not None:
35 mnt
= bdev
['mountpoint']
39 fs
[mnt
] = set([lastdisk
])
40 if 'children' not in bdev
:
42 for child
in bdev
['children']:
43 find_xfs_mounts(child
, fs
, lastdisk
)
46 cmd
=['lsblk', '-o', 'NAME,KNAME,TYPE,FSTYPE,MOUNTPOINT', '-J']
47 result
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
49 if result
.returncode
!= 0:
51 sarray
= [x
.decode(sys
.stdout
.encoding
) for x
in result
.stdout
.readlines()]
52 output
= ' '.join(sarray
)
53 bdevdata
= json
.loads(output
)
55 # The lsblk output had better be in disks-then-partitions order
56 for bdev
in bdevdata
['blockdevices']:
57 lastdisk
= bdev
['kname']
58 find_xfs_mounts(bdev
, fs
, lastdisk
)
63 '''Generator function that yields lines of a program's stdout.'''
64 p
= subprocess
.Popen(cmd
, stdout
= subprocess
.PIPE
)
65 for line
in TextIOWrapper(p
.stdout
, encoding
="utf-8"):
68 def remove_killfunc(killfuncs
, fn
):
69 '''Ensure fn is not in killfuncs.'''
75 def run_killable(cmd
, stdout
, killfuncs
):
76 '''Run a killable program. Returns program retcode or -1 if we can't
79 proc
= subprocess
.Popen(cmd
, stdout
= stdout
)
80 killfuncs
.add(proc
.terminate
)
82 remove_killfunc(killfuncs
, proc
.terminate
)
83 return proc
.returncode
87 # systemd doesn't like unit instance names with slashes in them, so it
88 # replaces them with dashes when it invokes the service. Filesystem paths
89 # need a special --path argument so that dashes do not get mangled.
90 def path_to_serviceunit(path
):
91 '''Convert a pathname into a systemd service unit name.'''
93 cmd
= ['systemd-escape', '--template', '@scrub_svcname@',
96 proc
= subprocess
.Popen(cmd
, stdout
= subprocess
.PIPE
)
98 for line
in proc
.stdout
:
99 return line
.decode(sys
.stdout
.encoding
).strip()
103 def systemctl_stop(unitname
):
104 '''Stop a systemd unit.'''
105 cmd
= ['systemctl', 'stop', unitname
]
106 x
= subprocess
.Popen(cmd
)
109 def systemctl_start(unitname
, killfuncs
):
110 '''Start a systemd unit and wait for it to complete.'''
112 cmd
= ['systemctl', 'start', unitname
]
114 proc
= subprocess
.Popen(cmd
, stdout
= DEVNULL())
115 stop_fn
= lambda: systemctl_stop(unitname
)
116 killfuncs
.add(stop_fn
)
118 ret
= proc
.returncode
120 if stop_fn
is not None:
121 remove_killfunc(killfuncs
, stop_fn
)
125 remove_killfunc(killfuncs
, stop_fn
)
128 # If systemctl-start returns 1, it's possible that the service failed
129 # or that dbus/systemd restarted and the client program lost its
130 # connection -- according to the systemctl man page, 1 means "unit not
133 # Either way, we switch to polling the service status to try to wait
134 # for the service to end. As of systemd 249, the is-active command
135 # returns any of the following states: active, reloading, inactive,
136 # failed, activating, deactivating, or maintenance. Apparently these
137 # strings are not localized.
140 for l
in backtick(['systemctl', 'is-active', unitname
]):
142 remove_killfunc(killfuncs
, stop_fn
)
145 remove_killfunc(killfuncs
, stop_fn
)
148 remove_killfunc(killfuncs
, stop_fn
)
153 def run_scrub(mnt
, cond
, running_devs
, mntdevs
, killfuncs
):
154 '''Run a scrub process.'''
155 global retcode
, terminate
157 print("Scrubbing %s..." % mnt
)
164 # Try it the systemd way
165 unitname
= path_to_serviceunit(path
)
166 if unitname
is not None:
167 ret
= systemctl_start(unitname
, killfuncs
)
168 if ret
== 0 or ret
== 1:
169 print("Scrubbing %s done, (err=%d)" % (mnt
, ret
))
177 # Invoke xfs_scrub manually
178 cmd
= ['@sbindir@/xfs_scrub']
179 cmd
+= '@scrub_args@'.split()
181 ret
= run_killable(cmd
, None, killfuncs
)
183 print("Scrubbing %s done, (err=%d)" % (mnt
, ret
))
191 print("Unable to start scrub tool.")
194 running_devs
-= mntdevs
200 '''Find mounts, schedule scrub runs.'''
202 a
= (mnt
, cond
, running_devs
, devs
, killfuncs
)
203 thr
= threading
.Thread(target
= run_scrub
, args
= a
)
205 global retcode
, terminate
207 parser
= argparse
.ArgumentParser( \
208 description
= "Scrub all mounted XFS filesystems.")
209 parser
.add_argument("-V", help = "Report version and exit.", \
210 action
= "store_true")
211 args
= parser
.parse_args()
214 print("xfs_scrub_all version @pkg_version@")
219 # Tail the journal if we ourselves aren't a service...
221 if 'SERVICE_MODE' not in os
.environ
:
223 cmd
=['journalctl', '--no-pager', '-q', '-S', 'now', \
224 '-f', '-u', 'xfs_scrub@*', '-o', \
226 journalthread
= subprocess
.Popen(cmd
)
230 # Schedule scrub jobs...
233 cond
= threading
.Condition()
235 if len(running_devs
) == 0:
236 mnt
, devs
= fs
.popitem()
237 running_devs
.update(devs
)
244 if dev
in running_devs
:
248 running_devs
.update(devs
)
256 except KeyboardInterrupt:
258 print("Terminating...")
260 while len(killfuncs
) > 0:
266 if journalthread
is not None:
267 journalthread
.terminate()
269 # See the service mode comments in xfs_scrub.c for why we do this.
270 if 'SERVICE_MODE' in os
.environ
:
277 if __name__
== '__main__':