xref: /dragonfly/sbin/hammer/cmd_cleanup.c (revision 235099c3)
1 /*
2  * Copyright (c) 2008 The DragonFly Project.  All rights reserved.
3  *
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@backplane.com>
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in
15  *    the documentation and/or other materials provided with the
16  *    distribution.
17  * 3. Neither the name of The DragonFly Project nor the names of its
18  *    contributors may be used to endorse or promote products derived
19  *    from this software without specific, prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
25  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32  * SUCH DAMAGE.
33  *
34  * $DragonFly: src/sbin/hammer/cmd_cleanup.c,v 1.6 2008/10/07 22:28:41 thomas Exp $
35  */
36 /*
37  * Clean up a specific HAMMER filesystem or all HAMMER filesystems.
38  *
39  * Each filesystem is expected to have a <mount>/snapshots directory.
40  * No cleanup will be performed on any filesystem that does not.  If
41  * no filesystems are specified the 'df' program is run and any HAMMER
42  * or null-mounted hammer PFS's are extracted.
43  *
44  * The snapshots directory may contain a config file called 'config'.  If
45  * no config file is present one will be created with the following
46  * defaults:
47  *
48  *	snapshots 1d 60d	(0d 0d for /tmp, /var/tmp, /usr/obj)
49  *	prune     1d 5m
50  *	reblock   1d 5m
51  *	recopy    30d 5m
52  *
53  * All hammer commands create and maintain cycle files in the snapshots
54  * directory.
55  */
56 
57 #include "hammer.h"
58 
59 struct didpfs {
60 	struct didpfs *next;
61 	uuid_t		uuid;
62 };
63 
64 static void do_cleanup(const char *path);
65 static int strtosecs(char *ptr);
66 static const char *dividing_slash(const char *path);
67 static int check_period(const char *snapshots_path, const char *cmd, int arg1,
68 			time_t *savep);
69 static void save_period(const char *snapshots_path, const char *cmd,
70 			time_t savet);
71 static int check_softlinks(const char *snapshots_path);
72 static void cleanup_softlinks(const char *path, const char *snapshots_path,
73 			int arg2, char *arg3);
74 static int check_expired(const char *fpath, int arg2);
75 
76 static int create_snapshot(const char *path, const char *snapshots_path,
77 			      int arg1, int arg2);
78 static int cleanup_prune(const char *path, const char *snapshots_path,
79 			      int arg1, int arg2, int snapshots_disabled);
80 static int cleanup_reblock(const char *path, const char *snapshots_path,
81 			      int arg1, int arg2);
82 static int cleanup_recopy(const char *path, const char *snapshots_path,
83 			      int arg1, int arg2);
84 
85 static void runcmd(int *resp, const char *ctl, ...);
86 
87 #define WS	" \t\r\n"
88 
89 struct didpfs *FirstPFS;
90 
91 void
92 hammer_cmd_cleanup(char **av, int ac)
93 {
94 	char *fstype, *fs, *path;
95 	struct statfs *stfsbuf;
96 	int mntsize, i;
97 
98 	tzset();
99 	if (ac == 0) {
100 		mntsize = getmntinfo(&stfsbuf, MNT_NOWAIT);
101 		if (mntsize > 0) {
102 			for (i=0; i < mntsize; i++) {
103 				/*
104 				 * We will cleanup in the case fstype is hammer.
105 				 * If we have null-mounted PFS, we check the
106 				 * mount source. If it looks like a PFS, we
107 				 * proceed to cleanup also.
108 				 */
109 				fstype = stfsbuf[i].f_fstypename;
110 				fs = stfsbuf[i].f_mntfromname;
111 				if ((strcmp(fstype, "hammer") == 0) ||
112 				    ((strcmp(fstype, "null") == 0) &&
113 				     (strstr(fs, "/@@0x") != NULL ||
114 				      strstr(fs, "/@@-1") != NULL))) {
115 					path = stfsbuf[i].f_mntonname;
116 					do_cleanup(path);
117 				}
118 			}
119 		}
120 
121 	} else {
122 		while (ac) {
123 			do_cleanup(*av);
124 			--ac;
125 			++av;
126 		}
127 	}
128 }
129 
130 static
131 void
132 do_cleanup(const char *path)
133 {
134 	struct hammer_ioc_pseudofs_rw pfs;
135 	union hammer_ioc_mrecord_any mrec_tmp;
136 	char *snapshots_path;
137 	char *config_path;
138 	struct stat st;
139 	char *cmd;
140 	char *ptr;
141 	int arg1;
142 	int arg2;
143 	char *arg3;
144 	time_t savet;
145 	char buf[256];
146 	FILE *fp;
147 	struct didpfs *didpfs;
148 	int snapshots_disabled = 0;
149 	int prune_warning = 0;
150 	int fd;
151 	int r;
152 
153 	bzero(&pfs, sizeof(pfs));
154 	bzero(&mrec_tmp, sizeof(mrec_tmp));
155 	pfs.ondisk = &mrec_tmp.pfs.pfsd;
156 	pfs.bytes = sizeof(mrec_tmp.pfs.pfsd);
157 	pfs.pfs_id = -1;
158 
159 	printf("cleanup %-20s -", path);
160 	fd = open(path, O_RDONLY);
161 	if (fd < 0) {
162 		printf(" unable to access directory: %s\n", strerror(errno));
163 		return;
164 	}
165 	if (ioctl(fd, HAMMERIOC_GET_PSEUDOFS, &pfs) != 0) {
166 		printf(" not a HAMMER filesystem: %s\n", strerror(errno));
167 		return;
168 	}
169 	close(fd);
170 	if (pfs.version != HAMMER_IOC_PSEUDOFS_VERSION) {
171 		printf(" unrecognized HAMMER version\n");
172 		return;
173 	}
174 
175 	/*
176 	 * Make sure we have not already handled this PFS.  Several nullfs
177 	 * mounts might alias the same PFS.
178 	 */
179 	for (didpfs = FirstPFS; didpfs; didpfs = didpfs->next) {
180 		if (bcmp(&didpfs->uuid, &mrec_tmp.pfs.pfsd.unique_uuid, sizeof(uuid_t)) == 0) {
181 			printf(" PFS #%d already handled\n", pfs.pfs_id);
182 			return;
183 		}
184 	}
185 	didpfs = malloc(sizeof(*didpfs));
186 	didpfs->next = FirstPFS;
187 	FirstPFS = didpfs;
188 	didpfs->uuid = mrec_tmp.pfs.pfsd.unique_uuid;
189 
190 	/*
191 	 * Figure out where the snapshot directory is.
192 	 */
193 	if (mrec_tmp.pfs.pfsd.snapshots[0] == '/') {
194 		asprintf(&snapshots_path, "%s", mrec_tmp.pfs.pfsd.snapshots);
195 	} else if (mrec_tmp.pfs.pfsd.snapshots[0]) {
196 		printf(" WARNING: pfs-slave's snapshots dir is not absolute\n");
197 		return;
198 	} else if (mrec_tmp.pfs.pfsd.mirror_flags & HAMMER_PFSD_SLAVE) {
199 		printf(" WARNING: must configure snapshot dir for PFS slave\n");
200 		printf("\tWe suggest <fs>/var/slaves/<name> where "
201 		       "<fs> is the base HAMMER fs\n");
202 		printf("\tcontaining the slave\n");
203 		return;
204 	} else {
205 		asprintf(&snapshots_path,
206 			 "%s%ssnapshots", path, dividing_slash(path));
207 	}
208 
209 	/*
210 	 * Create a snapshot directory if necessary, and a config file if
211 	 * necessary.
212 	 */
213 	if (stat(snapshots_path, &st) < 0) {
214 		if (mkdir(snapshots_path, 0755) != 0) {
215 			free(snapshots_path);
216 			printf(" unable to create snapshot dir \"%s\": %s\n",
217 				snapshots_path, strerror(errno));
218 			return;
219 		}
220 	}
221 	asprintf(&config_path, "%s/config", snapshots_path);
222 	if ((fp = fopen(config_path, "r")) == NULL) {
223 		fp = fopen(config_path, "w");
224 		if (fp == NULL) {
225 			printf(" cannot create %s: %s\n",
226 				config_path, strerror(errno));
227 			return;
228 		}
229 		if (strcmp(path, "/tmp") == 0 ||
230 		    strcmp(path, "/var/tmp") == 0 ||
231 		    strcmp(path, "/usr/obj") == 0) {
232 			fprintf(fp, "snapshots 0d 0d\n");
233 		} else {
234 			fprintf(fp, "snapshots 1d 60d\n");
235 		}
236 		fprintf(fp,
237 			"prune     1d 5m\n"
238 			"reblock   1d 5m\n"
239 			"recopy    30d 10m\n");
240 		fclose(fp);
241 		fp = fopen(config_path, "r");
242 	}
243 	if (fp == NULL) {
244 		printf(" cannot access %s: %s\n",
245 		       config_path, strerror(errno));
246 		return;
247 	}
248 
249 	if (flock(fileno(fp), LOCK_EX|LOCK_NB) == -1) {
250 		if (errno == EWOULDBLOCK)
251 			printf(" PFS #%d locked by other process\n", pfs.pfs_id);
252 		else
253 			printf(" can not lock %s: %s\n", config_path, strerror(errno));
254 		fclose(fp);
255 		return;
256 	}
257 
258 	printf(" handle PFS #%d using %s\n", pfs.pfs_id, snapshots_path);
259 
260 	/*
261 	 * Process the config file
262 	 */
263 	while (fgets(buf, sizeof(buf), fp) != NULL) {
264 		cmd = strtok(buf, WS);
265 		arg1 = 0;
266 		arg2 = 0;
267 		arg3 = NULL;
268 		if ((ptr = strtok(NULL, WS)) != NULL) {
269 			arg1 = strtosecs(ptr);
270 			if ((ptr = strtok(NULL, WS)) != NULL) {
271 				arg2 = strtosecs(ptr);
272 				arg3 = strtok(NULL, WS);
273 			}
274 		}
275 
276 		printf("%20s - ", cmd);
277 		fflush(stdout);
278 
279 		r = 1;
280 		if (strcmp(cmd, "snapshots") == 0) {
281 			if (arg1 == 0) {
282 				if (arg2 && check_softlinks(snapshots_path)) {
283 					printf("only removing old snapshots\n");
284 					prune_warning = 1;
285 					cleanup_softlinks(path, snapshots_path,
286 							  arg2, arg3);
287 				} else {
288 					printf("disabled\n");
289 					snapshots_disabled = 1;
290 				}
291 			} else
292 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
293 				printf("run\n");
294 				cleanup_softlinks(path, snapshots_path,
295 						  arg2, arg3);
296 				r = create_snapshot(path, snapshots_path,
297 						  arg1, arg2);
298 			} else {
299 				printf("skip\n");
300 			}
301 		} else if (arg1 == 0) {
302 			/*
303 			 * The commands following this check can't handle
304 			 * a period of 0, so call the feature disabled and
305 			 * ignore the directive.
306 			 */
307 			printf("disabled\n");
308 		} else if (strcmp(cmd, "prune") == 0) {
309 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
310 				if (prune_warning) {
311 					printf("run - WARNING snapshot "
312 					       "softlinks present "
313 					       "but snapshots disabled\n");
314 				} else {
315 					printf("run\n");
316 				}
317 				r = cleanup_prune(path, snapshots_path,
318 					      arg1, arg2, snapshots_disabled);
319 			} else {
320 				printf("skip\n");
321 			}
322 		} else if (strcmp(cmd, "reblock") == 0) {
323 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
324 				printf("run");
325 				fflush(stdout);
326 				if (VerboseOpt)
327 					printf("\n");
328 				r = cleanup_reblock(path, snapshots_path,
329 						arg1, arg2);
330 			} else {
331 				printf("skip\n");
332 			}
333 		} else if (strcmp(cmd, "recopy") == 0) {
334 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
335 				printf("run");
336 				fflush(stdout);
337 				if (VerboseOpt)
338 					printf("\n");
339 				r = cleanup_recopy(path, snapshots_path,
340 					       arg1, arg2);
341 			} else {
342 				printf("skip\n");
343 			}
344 		} else {
345 			printf("unknown directive\n");
346 			r = 1;
347 		}
348 		if (r == 0)
349 			save_period(snapshots_path, cmd, savet);
350 	}
351 	fclose(fp);
352 	usleep(1000);
353 }
354 
355 static
356 int
357 strtosecs(char *ptr)
358 {
359 	int val;
360 
361 	val = strtol(ptr, &ptr, 0);
362 	switch(*ptr) {
363 	case 'd':
364 		val *= 24;
365 		/* fall through */
366 	case 'h':
367 		val *= 60;
368 		/* fall through */
369 	case 'm':
370 		val *= 60;
371 		/* fall through */
372 	case 's':
373 		break;
374 	default:
375 		errx(1, "illegal suffix converting %s\n", ptr);
376 		break;
377 	}
378 	return(val);
379 }
380 
381 static const char *
382 dividing_slash(const char *path)
383 {
384 	int len = strlen(path);
385 	if (len && path[len-1] == '/')
386 		return("");
387 	else
388 		return("/");
389 }
390 
391 /*
392  * Check whether the desired period has elapsed since the last successful
393  * run.  The run may take a while and cross a boundary so we remember the
394  * current time_t so we can save it later on.
395  *
396  * Periods in minutes, hours, or days are assumed to have been crossed
397  * if the local time crosses a minute, hour, or day boundary regardless
398  * of how close the last operation actually was.
399  */
400 static int
401 check_period(const char *snapshots_path, const char *cmd, int arg1,
402 	time_t *savep)
403 {
404 	char *check_path;
405 	struct tm tp1;
406 	struct tm tp2;
407 	FILE *fp;
408 	time_t baset, lastt;
409 	char buf[256];
410 
411 	time(savep);
412 	localtime_r(savep, &tp1);
413 
414 	/*
415 	 * Retrieve the start time of the last successful operation.
416 	 */
417 	asprintf(&check_path, "%s/.%s.period", snapshots_path, cmd);
418 	fp = fopen(check_path, "r");
419 	free(check_path);
420 	if (fp == NULL)
421 		return(1);
422 	if (fgets(buf, sizeof(buf), fp) == NULL) {
423 		fclose(fp);
424 		return(1);
425 	}
426 	fclose(fp);
427 
428 	lastt = strtol(buf, NULL, 0);
429 	localtime_r(&lastt, &tp2);
430 
431 	/*
432 	 * Normalize the times.  e.g. if asked to do something on a 1-day
433 	 * interval the operation will be performed as soon as the day
434 	 * turns over relative to the previous operation, even if the previous
435 	 * operation ran a few seconds ago just before midnight.
436 	 */
437 	if (arg1 % 60 == 0) {
438 		tp1.tm_sec = 0;
439 		tp2.tm_sec = 0;
440 	}
441 	if (arg1 % (60 * 60) == 0) {
442 		tp1.tm_min = 0;
443 		tp2.tm_min = 0;
444 	}
445 	if (arg1 % (24 * 60 * 60) == 0) {
446 		tp1.tm_hour = 0;
447 		tp2.tm_hour = 0;
448 	}
449 
450 	baset = mktime(&tp1);
451 	lastt = mktime(&tp2);
452 
453 #if 0
454 	printf("%lld vs %lld\n", (long long)(baset - lastt), (long long)arg1);
455 #endif
456 
457 	if ((int)(baset - lastt) >= arg1)
458 		return(1);
459 	return(0);
460 }
461 
462 /*
463  * Store the start time of the last successful operation.
464  */
465 static void
466 save_period(const char *snapshots_path, const char *cmd,
467 			time_t savet)
468 {
469 	char *ocheck_path;
470 	char *ncheck_path;
471 	FILE *fp;
472 
473 	asprintf(&ocheck_path, "%s/.%s.period", snapshots_path, cmd);
474 	asprintf(&ncheck_path, "%s/.%s.period.new", snapshots_path, cmd);
475 	fp = fopen(ncheck_path, "w");
476 	if (fp) {
477 		fprintf(fp, "0x%08llx\n", (long long)savet);
478 		if (fclose(fp) == 0)
479 			rename(ncheck_path, ocheck_path);
480 		remove(ncheck_path);
481 	} else {
482 		fprintf(stderr, "hammer: Unable to create period-file %s: %s\n",
483 			ncheck_path, strerror(errno));
484 	}
485 }
486 
487 /*
488  * Simply count the number of softlinks in the snapshots dir
489  */
490 static int
491 check_softlinks(const char *snapshots_path)
492 {
493 	struct dirent *den;
494 	struct stat st;
495 	DIR *dir;
496 	char *fpath;
497 	int res = 0;
498 
499 	if ((dir = opendir(snapshots_path)) != NULL) {
500 		while ((den = readdir(dir)) != NULL) {
501 			if (den->d_name[0] == '.')
502 				continue;
503 			asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
504 			if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode))
505 				++res;
506 			free(fpath);
507 		}
508 		closedir(dir);
509 	}
510 	return(res);
511 }
512 
513 /*
514  * Clean up expired softlinks in the snapshots dir
515  */
516 static void
517 cleanup_softlinks(const char *path __unused, const char *snapshots_path,
518 		  int arg2, char *arg3)
519 {
520 	struct dirent *den;
521 	struct stat st;
522 	DIR *dir;
523 	char *fpath;
524 	int anylink = 0;
525 
526 	if (arg3 != NULL && strstr(arg3, "any") != NULL)
527 		anylink = 1;
528 
529 	if ((dir = opendir(snapshots_path)) != NULL) {
530 		while ((den = readdir(dir)) != NULL) {
531 			if (den->d_name[0] == '.')
532 				continue;
533 			asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
534 			if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode) &&
535 			    (anylink || strncmp(den->d_name, "snap-", 5) == 0)
536 			) {
537 				if (check_expired(den->d_name, arg2)) {
538 					if (VerboseOpt) {
539 						printf("    expire %s\n",
540 							fpath);
541 					}
542 					remove(fpath);
543 				}
544 			}
545 			free(fpath);
546 		}
547 		closedir(dir);
548 	}
549 }
550 
551 /*
552  * Take a softlink path in the form snap-yyyymmdd-hhmm and the
553  * expiration in seconds (arg2) and return non-zero if the softlink
554  * has expired.
555  */
556 static int
557 check_expired(const char *fpath, int arg2)
558 {
559 	struct tm tm;
560 	time_t t;
561 	int year;
562 	int month;
563 	int day = 0;
564 	int hour = 0;
565 	int minute = 0;
566 	int r;
567 
568 	while (*fpath && *fpath != '-' && *fpath != '.')
569 		++fpath;
570 	if (*fpath)
571 		++fpath;
572 
573 	r = sscanf(fpath, "%4d%2d%2d-%2d%2d",
574 		   &year, &month, &day, &hour, &minute);
575 
576 	if (r >= 3) {
577 		bzero(&tm, sizeof(tm));
578 		tm.tm_isdst = -1;
579 		tm.tm_min = minute;
580 		tm.tm_hour = hour;
581 		tm.tm_mday = day;
582 		tm.tm_mon = month - 1;
583 		tm.tm_year = year - 1900;
584 		t = mktime(&tm);
585 		if (t == (time_t)-1)
586 			return(0);
587 		t = time(NULL) - t;
588 		if ((int)t > arg2)
589 			return(1);
590 	}
591 	return(0);
592 }
593 
594 /*
595  * Issue a snapshot.
596  */
597 static int
598 create_snapshot(const char *path __unused, const char *snapshots_path,
599 		  int arg1 __unused, int arg2 __unused)
600 {
601 	int r;
602 
603 	runcmd(&r, "hammer snapshot %s %s", path, snapshots_path);
604 	return(r);
605 }
606 
607 static int
608 cleanup_prune(const char *path __unused, const char *snapshots_path,
609 		  int arg1 __unused, int arg2, int snapshots_disabled)
610 {
611 	/*
612 	 * If snapshots have been disabled run prune-everything instead
613 	 * of prune.
614 	 */
615 	if (snapshots_disabled && arg2) {
616 		runcmd(NULL, "hammer -c %s/.prune.cycle -t %d prune-everything %s",
617 			snapshots_path, arg2, path);
618 	} else if (snapshots_disabled) {
619 		runcmd(NULL, "hammer prune-everything %s", path);
620 	} else if (arg2) {
621 		runcmd(NULL, "hammer -c %s/.prune.cycle -t %d prune %s",
622 			snapshots_path, arg2, snapshots_path);
623 	} else {
624 		runcmd(NULL, "hammer prune %s", snapshots_path);
625 	}
626 	return(0);
627 }
628 
629 static int
630 cleanup_reblock(const char *path, const char *snapshots_path,
631 		  int arg1 __unused, int arg2)
632 {
633 	if (VerboseOpt == 0) {
634 		printf(".");
635 		fflush(stdout);
636 	}
637 
638 	/*
639 	 * When reblocking the B-Tree always reblock everything in normal
640 	 * mode.
641 	 */
642 	runcmd(NULL,
643 	       "hammer -c %s/.reblock-1.cycle -t %d reblock-btree %s",
644 	       snapshots_path, arg2, path);
645 	if (VerboseOpt == 0) {
646 		printf(".");
647 		fflush(stdout);
648 	}
649 
650 	/*
651 	 * When reblocking the inodes always reblock everything in normal
652 	 * mode.
653 	 */
654 	runcmd(NULL,
655 	       "hammer -c %s/.reblock-2.cycle -t %d reblock-inodes %s",
656 	       snapshots_path, arg2, path);
657 	if (VerboseOpt == 0) {
658 		printf(".");
659 		fflush(stdout);
660 	}
661 
662 	/*
663 	 * When reblocking the directories always reblock everything in normal
664 	 * mode.
665 	 */
666 	runcmd(NULL,
667 	       "hammer -c %s/.reblock-4.cycle -t %d reblock-dirs %s",
668 	       snapshots_path, arg2, path);
669 	if (VerboseOpt == 0) {
670 		printf(".");
671 		fflush(stdout);
672 	}
673 
674 	/*
675 	 * Do not reblock all the data in normal mode.
676 	 */
677 	runcmd(NULL,
678 	       "hammer -c %s/.reblock-3.cycle -t %d reblock-data %s 95",
679 	       snapshots_path, arg2, path);
680 	if (VerboseOpt == 0)
681 		printf("\n");
682 	return(0);
683 }
684 
685 static int
686 cleanup_recopy(const char *path, const char *snapshots_path,
687 		  int arg1 __unused, int arg2)
688 {
689 	if (VerboseOpt == 0) {
690 		printf(".");
691 		fflush(stdout);
692 	}
693 	runcmd(NULL,
694 	       "hammer -c %s/.recopy-1.cycle -t %d reblock-btree %s",
695 	       snapshots_path, arg2, path);
696 	if (VerboseOpt == 0) {
697 		printf(".");
698 		fflush(stdout);
699 	}
700 	runcmd(NULL,
701 	       "hammer -c %s/.recopy-2.cycle -t %d reblock-inodes %s",
702 	       snapshots_path, arg2, path);
703 	if (VerboseOpt == 0) {
704 		printf(".");
705 		fflush(stdout);
706 	}
707 	runcmd(NULL,
708 	       "hammer -c %s/.recopy-4.cycle -t %d reblock-dirs %s",
709 	       snapshots_path, arg2, path);
710 	if (VerboseOpt == 0) {
711 		printf(".");
712 		fflush(stdout);
713 	}
714 	runcmd(NULL,
715 	       "hammer -c %s/.recopy-3.cycle -t %d reblock-data %s",
716 	       snapshots_path, arg2, path);
717 	if (VerboseOpt == 0)
718 		printf("\n");
719 	return(0);
720 }
721 
722 static
723 void
724 runcmd(int *resp, const char *ctl, ...)
725 {
726 	va_list va;
727 	char *cmd;
728 	char *arg;
729 	char **av;
730 	int n;
731 	int nmax;
732 	int res;
733 	pid_t pid;
734 
735 	/*
736 	 * Generate the command
737 	 */
738 	va_start(va, ctl);
739 	vasprintf(&cmd, ctl, va);
740 	va_end(va);
741 	if (VerboseOpt)
742 		printf("    %s\n", cmd);
743 
744 	/*
745 	 * Break us down into arguments.  We do not just use system() here
746 	 * because it blocks SIGINT and friends.
747 	 */
748 	n = 0;
749 	nmax = 16;
750 	av = malloc(sizeof(char *) * nmax);
751 
752 	for (arg = strtok(cmd, WS); arg; arg = strtok(NULL, WS)) {
753 		if (n == nmax - 1) {
754 			nmax += 16;
755 			av = realloc(av, sizeof(char *) * nmax);
756 		}
757 		av[n++] = arg;
758 	}
759 	av[n++] = NULL;
760 
761 	/*
762 	 * Run the command.
763 	 */
764 	RunningIoctl = 1;
765 	if ((pid = fork()) == 0) {
766 		if (VerboseOpt < 2) {
767 			int fd = open("/dev/null", O_RDWR);
768 			dup2(fd, 1);
769 			close(fd);
770 		}
771 		execvp(av[0], av);
772 		_exit(127);
773 	} else if (pid < 0) {
774 		res = 127;
775 	} else {
776 		int status;
777 
778 		while (waitpid(pid, &status, 0) != pid)
779 			;
780 		res = WEXITSTATUS(status);
781 	}
782 	RunningIoctl = 0;
783 	if (DidInterrupt)
784 		_exit(1);
785 
786 	free(cmd);
787 	free(av);
788 	if (resp)
789 		*resp = res;
790 }
791