OpenSolaris_b135/cmd/format/analyze.c

Compare this file to the similar file:
Show the results in this format:

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * This file contains routines to analyze the surface of a disk.
 */
#include "global.h"
#include "analyze.h"
#include <stdlib.h>
#include <errno.h>
#include "misc.h"
#include "defect.h"
#include "label.h"
#include "param.h"
#include "checkdev.h"


/*
 * These global variables control the surface analysis process.  They
 * are set from a command in the defect menu.
 */
int	scan_entire = 1;		/* scan whole disk flag */
diskaddr_t	scan_lower = 0;			/* lower bound */
diskaddr_t	scan_upper = 0;			/* upper bound */
int	scan_correct = 1;		/* correct errors flag */
int	scan_stop = 0;			/* stop after error flag */
int	scan_loop = 0;			/* loop forever flag */
int	scan_passes = 2;		/* number of passes */
int	scan_random = 0;		/* random patterns flag */
uint_t	scan_size = 0;			/* sectors/scan operation */
int	scan_auto = 1;			/* scan after format flag */
int	scan_restore_defects = 1;	/* restore defect list after writing */
int	scan_restore_label = 1;		/* restore label after writing */

/*
 * These are summary variables to print out info after analysis.
 * Values less than 0 imply they are invalid.
 */
offset_t	scan_cur_block = -1;		/* current block */
int64_t		scan_blocks_fixed = -1;		/* # blocks repaired */

/*
 * This variable is used to tell whether the most recent surface
 * analysis error was caused by a media defect or some other problem.
 */
int	media_error;			/* error was caused by defect */

int	disk_error;			/* disk errors during analysis */

/*
 * These are the data patterns used if random patterns are not chosen.
 * They are designed to show pattern dependent errors.
 */
static unsigned int	scan_patterns[] = {
	0xc6dec6de,
	0x6db6db6d,
	0x00000000,
	0xffffffff,
	0xaaaaaaaa,
};
#define	NPATTERNS	5		/* number of predefined patterns */

/*
 * These are the data patterns from the SunFed requirements document.
 */
static unsigned int purge_patterns[] = {	/* patterns to be written */
	0xaaaaaaaa,		/* 10101010... */
	0x55555555,		/* 01010101...  == UUUU... */
	0xaaaaaaaa,		/* 10101010... */
	0xaaaaaaaa,		/* 10101010... */
};

static unsigned int alpha_pattern =  0x40404040;   /* 10000000...  == @@@@... */

/* Function prototypes */
#ifdef	__STDC__

static int	scan_repair(diskaddr_t bn, int mode);
static int	analyze_blocks(int flags, diskaddr_t blkno, uint_t blkcnt,
		unsigned data, int init, int driver_flags, int *xfercntp);
static int	handle_error_conditions(void);
static int	verify_blocks(int flags, diskaddr_t blkno, uint_t blkcnt,
		unsigned data, int driver_flags, int *xfercntp);
#else	/* __STDC__ */

static int	scan_repair();
static int	analyze_blocks();
static int	handle_error_conditions();
static int	verify_blocks();

#endif	/* __STDC__ */

/*
 * This routine performs a surface analysis based upon the global
 * parameters.  It is called from several commands in the defect menu,
 * and from the format command in the command menu (if post-format
 * analysis is enable).
 */
int
do_scan(flags, mode)
	int	flags, mode;
{
	diskaddr_t	start, end, curnt;
	int	pass, needinit, data;
	uint_t	size;
	int	status, founderr, i, j;
	int	error = 0;
	int	pattern = 0;
	int	xfercnt;

	/*
	 * Check to be sure we aren't correcting without a defect list
	 * if the controller can correct the defect.
	 */
	if (scan_correct && !EMBEDDED_SCSI && (cur_ops->op_repair != NULL) &&
			(cur_list.list == NULL)) {
		err_print("Current Defect List must be initialized ");
		err_print("to do automatic repair.\n");
		return (-1);
	}
	/*
	 * Define the bounds of the scan.
	 */
	if (scan_entire) {
		start = 0;
	    if (cur_label == L_TYPE_SOLARIS) {
		if (cur_ctype->ctype_flags & CF_SCSI)
			end = datasects() - 1;
		else
			end = physsects() - 1;
	    } else if (cur_label == L_TYPE_EFI) {
		end = cur_parts->etoc->efi_last_lba;
	    }
	} else {
		start = scan_lower;
		end = scan_upper;
	}
	/*
	 * Make sure the user knows if we are scanning over a mounted
	 * partition.
	 */
	if ((flags & (SCAN_PATTERN | SCAN_WRITE)) &&
	    (checkmount(start, end))) {
		err_print("Cannot do analysis on a mounted partition.\n");
		return (-1);
	}

	/*
	 * Make sure the user knows if we are scanning over a
	 * partition being used for swapping.
	 */
	if ((flags & (SCAN_PATTERN | SCAN_WRITE)) &&
	    (checkswap(start, end))) {
		err_print("Cannot do analysis on a partition \
		    which is currently being used for swapping.\n");
		return (-1);
	}

	/*
	 * Check to see if any partitions used for svm, vxvm, ZFS zpool
	 * or live upgrade are on the disk.
	 */
	if ((flags & (SCAN_PATTERN | SCAN_WRITE)) &&
	    (checkdevinuse(cur_disk->disk_name, (diskaddr_t)-1,
	    (diskaddr_t)-1, 0, 0))) {
		err_print("Cannot do analysis on a partition "
		    "while it in use as described above.\n");
		return (-1);
	}

	/*
	 * If we are scanning destructively over certain sectors,
	 * we mark the defect list and/or label dirty so it will get rewritten.
	 */
	if (flags & (SCAN_PATTERN | SCAN_WRITE)) {
	    if (cur_label == L_TYPE_SOLARIS) {
		if (start < (diskaddr_t)totalsects() &&
				end >= (diskaddr_t)datasects()) {
			if (!EMBEDDED_SCSI) {
				cur_list.flags |= LIST_DIRTY;
			}
			if (cur_disk->disk_flags & DSK_LABEL)
				cur_flags |= LABEL_DIRTY;
		}
	    }
	    if (start == 0) {
		if (cur_disk->disk_flags & DSK_LABEL)
			cur_flags |= LABEL_DIRTY;
	    }
	}
	/*
	 * Initialize the summary info on sectors repaired.
	 */
	scan_blocks_fixed = 0;
	/*
	 * Loop through the passes of the scan. If required, loop forever.
	 */
	for (pass = 0; pass < scan_passes || scan_loop; pass++) {
		/*
		 * Determine the data pattern to use if pattern testing
		 * is to be done.
		 */
		if (flags & SCAN_PATTERN) {
			if (scan_random)
				data = (int)mrand48();
			else
				data = scan_patterns[pass % NPPATTERNS];

			if (flags & SCAN_PURGE) {
				flags &= ~(SCAN_PURGE_READ_PASS
						| SCAN_PURGE_ALPHA_PASS);
				switch (pattern % (NPPATTERNS + 1)) {
				case NPPATTERNS:
					pattern = 0;
					if (!error) {
					    fmt_print(
"\nThe last %d passes were successful, running alpha pattern pass", NPPATTERNS);
					    flags |= SCAN_PURGE_ALPHA_PASS;
					    data = alpha_pattern;
					} else {
					    data = purge_patterns[pattern];
					    pattern++;
					};
					break;
				case READPATTERN:
					flags |=  SCAN_PURGE_READ_PASS;
				default:
					data = purge_patterns[pattern];
					pattern++;
					break;
				}
			}
			fmt_print("\n        pass %d", pass);
			fmt_print(" - pattern = 0x%x", data);
		} else
			fmt_print("\n        pass %d", pass);

		fmt_print("\n");
		/*
		 * Mark the pattern buffer as corrupt, since it
		 * hasn't been initialized.
		 */
		needinit = 1;
		/*
		 * Print the first block number to the log file if
		 * logging is on so there is some record of what
		 * analysis was performed.
		 */
		if (log_file) {
			pr_dblock(log_print, start);
			log_print("\n");
		}
		/*
		 * Loop through this pass, each time analyzing an amount
		 * specified by the global parameters.
		 */
		xfercnt = 0;
		for (curnt = start; curnt <= end; curnt += size) {
			if ((end - curnt) < scan_size)
				size = end - curnt + 1;
			else
				size = scan_size;
			/*
			 * Print out where we are, so we don't look dead.
			 * Also store it in summary info for logging.
			 */
			scan_cur_block = curnt;
			nolog_print("   ");
			pr_dblock(nolog_print, curnt);
			nolog_print("  \015");
			(void) fflush(stdout);
			disk_error = 0;
			/*
			 * Do the actual analysis.
			 */
			status = analyze_blocks(flags, curnt, size,
			    (unsigned)data, needinit, (F_ALLERRS | F_SILENT),
			    &xfercnt);
			/*
			 * If there were no errors, the pattern buffer is
			 * still initialized, and we just loop to next chunk.
			 */
			needinit = 0;
			if (!status)
				continue;
			/*
			 * There was an error. Check if surface analysis
			 * can be continued.
			 */
			if (handle_error_conditions()) {
				scan_blocks_fixed = scan_cur_block = -1;
				return (-1);
			}
			/*
			 * There was an error. Mark the pattern buffer
			 * corrupt so it will get reinitialized.
			 */
			needinit = 1;
			/*
			 * If it was not a media error, ignore it.
			 */
			if (!media_error)
				continue;
			/*
			 * Loop 5 times through each sector of the chunk,
			 * analyzing them individually.
			 */
			nolog_print("   ");
			pr_dblock(nolog_print, curnt);
			nolog_print("  \015");
			(void) fflush(stdout);
			founderr = 0;
			for (j = 0; j < size * 5; j++) {
				i = j % size;
				disk_error = 0;
				status = analyze_blocks(flags, (curnt + i), 1,
				    (unsigned)data, needinit, F_ALLERRS, NULL);
				needinit = 0;
				if (!status)
					continue;
				/*
				 * There was an error. Check if surface analysis
				 * can be continued.
				 */
				if (handle_error_conditions()) {
					scan_blocks_fixed = scan_cur_block = -1;
					return (-1);
				}
				/*
				 * An error occurred.  Mark the buffer
				 * corrupt and see if it was media
				 * related.
				 */
				needinit = 1;
				if (!media_error)
					continue;
				/*
				 * We found a bad sector. Print out a message
				 * and fix it if required.
				 */
				founderr = 1;
				if (scan_correct && (flags != SCAN_VALID)) {
					if (scan_repair(curnt+i, mode)) {
						error = -1;
					}
				} else
					err_print("\n");
				/*
				 * Stop after the error if required.
				 */
				if (scan_stop)
					goto out;
			}
			/*
			 * Mark the pattern buffer corrupt to be safe.
			 */
			needinit = 1;
			/*
			 * We didn't find an individual sector that was bad.
			 * Print out a warning.
			 */
			if (!founderr) {
				err_print("Warning: unable to pinpoint ");
				err_print("defective block.\n");
			}
		}
		/*
		 * Print the end of each pass to the log file.
		 */
		enter_critical();
		if (log_file) {
			pr_dblock(log_print, scan_cur_block);
			log_print("\n");
		}
		scan_cur_block = -1;
		exit_critical();
		fmt_print("\n");

		/*
		 * alternate the read and write for SCAN_VERIFY test
		 */
		if (flags & SCAN_VERIFY) {
			flags ^= SCAN_VERIFY_READ_PASS;
		}
	}
out:
	/*
	 * We got here either by giving up after an error or falling
	 * through after all passes were completed.
	 */
	fmt_print("\n");
	enter_critical();
	/*
	 * If the defect list is dirty, write it to disk,
	 * if scan_restore_defects (the default) is true.
	 */
	if (!EMBEDDED_SCSI && (cur_list.flags & LIST_DIRTY) &&
				(scan_restore_defects)) {
		cur_list.flags = 0;
		write_deflist(&cur_list);
		}
	/*
	 * If the label is dirty, write it to disk.
	 * if scan_restore_label (the default) is true.
	 */
	if ((cur_flags & LABEL_DIRTY) && (scan_restore_label)) {
		cur_flags &= ~LABEL_DIRTY;
		(void) write_label();
	}
	/*
	 * If we dropped down to here after an error, we need to write
	 * the final block number to the log file for record keeping.
	 */
	if (log_file && scan_cur_block >= 0) {
		pr_dblock(log_print, scan_cur_block);
		log_print("\n");
	}
	fmt_print("Total of %lld defective blocks repaired.\n",
		scan_blocks_fixed);
	/*
	 * Reinitialize the logging variables so they don't get used
	 * when they are not really valid.
	 */
	scan_blocks_fixed = scan_cur_block = -1;
	exit_critical();
	return (error);
}


/*
 * This routine is called to repair a bad block discovered
 * during a scan operation.  Return 0 for success, 1 for failure.
 * (This has been extracted out of do_scan(), to simplify it.)
 */
static int
scan_repair(bn, mode)
	diskaddr_t	bn;
	int	mode;
{
	int	status;
	int	result = 1;
	char	*buf;
	int	buf_is_good;
	int	i;

	if (cur_ops->op_repair == NULL) {
		err_print("Warning: Controller does ");
		err_print("not support repairing.\n\n");
		return (result);
	}

	buf = malloc(cur_blksz);
	if (buf == NULL) {
		err_print("Warning: no memory.\n\n");
		return (result);
	}
	enter_critical();

	/*
	 * Determine if the error appears to be hard or soft.  We
	 * already assume there's an error.  If we can get any
	 * good data out of the sector, write that data back
	 * after the repair.
	 */
	buf_is_good = 0;
	for (i = 0; i < 5; i++) {
		status = (*cur_ops->op_rdwr)(DIR_READ, cur_file, bn, 1,
				buf, F_SILENT, NULL);
		if (status == 0) {
			buf_is_good = 1;
			break;
		}
	}

	fmt_print("Repairing %s error on %llu (",
				buf_is_good ? "soft" : "hard", bn);
	pr_dblock(fmt_print, bn);
	fmt_print(")...");

	status = (*cur_ops->op_repair)(bn, mode);
	if (status) {
		/*
		 * If the repair failed, we note it and will return the
		 * failure. However, the analysis goes on.
		 */
		fmt_print("failed.\n\n");
	} else {
		/*
		 * The repair worked.  Write the good data we could
		 * recover from the failed block, if possible.
		 * If not, zero the block.  In doing so, try to
		 * determine if the new block appears ok.
		 */
		if (!buf_is_good) {
			bzero(buf, cur_blksz);
			fmt_print("Warning: Block %llu zero-filled.\n", bn);
		} else {
			fmt_print("ok.\n");
		}
		status = (*cur_ops->op_rdwr)(DIR_WRITE, cur_file, bn,
					1, buf, (F_SILENT | F_ALLERRS), NULL);
		if (status == 0) {
			status = (*cur_ops->op_rdwr)(DIR_READ, cur_file, bn,
					1, buf, (F_SILENT | F_ALLERRS), NULL);
		}
		if (status) {
			fmt_print("The new block also appears defective.\n");
		}
		fmt_print("\n");
		/*
		 * add the defect to the list and write the list out.
		 * Also, kill the working list so it will get resynced
		 * with the current list.
		 *
		 * For embedded scsi, we don't require a defect list.
		 * However, if we have one, add the defect if the
		 * list includes the grown list.  If not, kill it
		 * to force a resync if we need the list later.
		 */
		if (EMBEDDED_SCSI) {
			if (cur_list.list != NULL) {
				if (cur_list.flags & LIST_PGLIST) {
					add_ldef(bn, &cur_list);
				} else {
					kill_deflist(&cur_list);
				}
			}
		/*
		 * The next "if" statement reflects the fix for
		 * bug id 1026096 where format keeps adding the
		 * same defect to the defect list.
		 */
		} else if (cur_ctype->ctype_flags & CF_WLIST) {
			kill_deflist(&cur_list);
			(*cur_ops->op_ex_cur)(&cur_list);
			fmt_print("Current list updated\n");
		} else {
			add_ldef(bn, &cur_list);
			write_deflist(&cur_list);
		}
		kill_deflist(&work_list);

		/* Log the repair.  */
		scan_blocks_fixed++;

		/* return ok */
		result = 0;
	}

	exit_critical();
	free(buf);
	return (result);
}


/*
 * This routine analyzes a set of sectors on the disk.  It simply returns
 * an error if a defect is found.  It is called by do_scan().
 */
static int
analyze_blocks(flags, blkno, blkcnt, data, init, driver_flags, xfercntp)
	int	flags, driver_flags, init;
	uint_t	blkcnt;
	register unsigned data;
	diskaddr_t	blkno;
	int	*xfercntp;
{
	int		corrupt = 0;
	int		status;
	register diskaddr_t	i, nints;
	register unsigned *ptr = (uint_t *)pattern_buf;

	media_error = 0;
	if (flags & SCAN_VERIFY) {
		return (verify_blocks(flags, blkno, blkcnt, data,
		    driver_flags, xfercntp));
	}

	/*
	 * Initialize the pattern buffer if necessary.
	 */
	nints = (diskaddr_t)blkcnt * cur_blksz / sizeof (int);
	if ((flags & SCAN_PATTERN) && init) {
		for (i = 0; i < nints; i++)
			*((int *)((int *)pattern_buf + i)) = data;
	}
	/*
	 * Lock out interrupts so we can insure valid data will get
	 * restored. This is necessary because there are modes
	 * of scanning that corrupt the disk data then restore it at
	 * the end of the analysis.
	 */
	enter_critical();
	/*
	 * If the disk data is valid, read it into the data buffer.
	 */
	if (flags & SCAN_VALID) {
		status = (*cur_ops->op_rdwr)(DIR_READ, cur_file, blkno,
		    blkcnt, (caddr_t)cur_buf, driver_flags, xfercntp);
		if (status)
			goto bad;
	}
	/*
	 * If we are doing pattern testing, write and read the pattern
	 * from the pattern buffer.
	 */
	if (flags & SCAN_PATTERN) {
		/*
		 * If the disk data was valid, mark it corrupt so we know
		 * to restore it later.
		 */
		if (flags & SCAN_VALID)
			corrupt++;
		/*
		 * Only write if we're not on the read pass of SCAN_PURGE.
		 */
		if (!(flags & SCAN_PURGE_READ_PASS)) {
			status = (*cur_ops->op_rdwr)(DIR_WRITE, cur_file, blkno,
			    blkcnt, (caddr_t)pattern_buf, driver_flags,
			    xfercntp);
			if (status)
			    goto bad;
		}
		/*
		 * Only read if we are on the read pass of SCAN_PURGE, if we
		 * are purging.
		 */
		if ((!(flags & SCAN_PURGE)) || (flags & SCAN_PURGE_READ_PASS)) {
			status = (*cur_ops->op_rdwr)(DIR_READ, cur_file, blkno,
			    blkcnt, (caddr_t)pattern_buf, driver_flags,
			    xfercntp);
			if (status)
			    goto bad;
		}
	}
	/*
	 * If we are doing a data compare, make sure the pattern
	 * came back intact.
	 * Only compare if we are on the read pass of SCAN_PURGE, or
	 * we wrote random data instead of the expected data pattern.
	 */
	if ((flags & SCAN_COMPARE) || (flags & SCAN_PURGE_READ_PASS)) {
		for (i = nints, ptr = (uint_t *)pattern_buf; i; i--)
			if (*ptr++ != data) {
				err_print("Data miscompare error (expecting ");
				err_print("0x%x, got 0x%x) at ", data,
					*((int *)((int *)pattern_buf +
					(nints - i))));
				pr_dblock(err_print, blkno);
				err_print(", offset = 0x%llx.\n",
					(nints - i) * sizeof (int));
				goto bad;
			}
	}
	/*
	 * If we are supposed to write data out, do so.
	 */
	if (flags & SCAN_WRITE) {
		status = (*cur_ops->op_rdwr)(DIR_WRITE, cur_file, blkno,
		    blkcnt, (caddr_t)cur_buf, driver_flags, xfercntp);
		if (status)
			goto bad;
	}
	exit_critical();
	/*
	 * No errors occurred, return ok.
	 */
	return (0);
bad:
	/*
	 * There was an error.  If the data was corrupted, we write it
	 * out from the data buffer to restore it.
	 */
	if (corrupt) {
		if ((*cur_ops->op_rdwr)(DIR_WRITE, cur_file, blkno,
				blkcnt, (caddr_t)cur_buf, F_NORMAL, xfercntp))
		err_print("Warning: unable to restore original data.\n");
	}
	exit_critical();
	/*
	 * Return the error.
	 */
	return (-1);
}


/*
 * This routine analyzes a set of sectors on the disk. It simply returns
 * an error if a defect is found.  It is called by analyze_blocks().
 * For simplicity, this is done as a separate function instead of
 * making the analyze_block routine complex.
 *
 * This routine implements the 'verify' command.  It writes the disk
 * by writing unique data for each block; after the write pass, it
 * reads the data and verifies for correctness. Note that the entire
 * disk (or the range of disk) is fully written first and then read.
 * This should eliminate any caching effect on the drives.
 */
static int
verify_blocks(int flags,
		diskaddr_t blkno,
		uint_t blkcnt,
		unsigned data,
		int driver_flags,
		int *xfercntp)
{
	int		status, i, nints;
	unsigned	*ptr = (uint_t *)pattern_buf;

	nints = cur_blksz / sizeof (int);

	/*
	 * Initialize the pattern buffer if we are in write pass.
	 * Use the block number itself as data, each block has unique
	 * buffer data that way.
	 */
	if (!(flags & SCAN_VERIFY_READ_PASS)) {
		for (data = blkno; data < blkno + blkcnt; data++) {
			for (i = 0; i < nints; i++) {
				*ptr++ = data;
			}
		}
		ptr = (uint_t *)pattern_buf;
	}

	/*
	 * Only write if we're not on the read pass of SCAN_VERIFY.
	 */
	if (!(flags & SCAN_VERIFY_READ_PASS)) {
		status = (*cur_ops->op_rdwr)(DIR_WRITE, cur_file, blkno,
		    blkcnt, (caddr_t)pattern_buf, driver_flags, xfercntp);
		if (status)
			goto bad;
	} else {
		/*
		 * Only read if we are on the read pass of SCAN_VERIFY
		 */
		status = (*cur_ops->op_rdwr)(DIR_READ, cur_file, blkno,
		    blkcnt, (caddr_t)pattern_buf, driver_flags, xfercntp);
		if (status)
			goto bad;
		/*
		 * compare and make sure the pattern came back intact.
		 */
		for (data = blkno; data < blkno + blkcnt; data++) {
			for (i = 0; i < nints; i++) {
				if (*ptr++ != data) {
					ptr--;
					err_print("Data miscompare error "
					    "(expecting 0x%x, got 0x%x) at ",
					    data, *ptr);
					pr_dblock(err_print, blkno);
					err_print(", offset = 0x%x.\n",
					    (ptr - (uint_t *)pattern_buf) *
					    sizeof (int));
					goto bad;
				}
			}
		}
	}
	/*
	 * No errors occurred, return ok.
	 */
	return (0);
bad:
	return (-1);
}


static int
handle_error_conditions()
{

	/*
	 * Check if the errno is ENXIO.
	 */
	if (errno == ENXIO) {
		fmt_print("\n\nWarning:Cannot access drive, ");
		fmt_print("aborting surface analysis.\n");
		return (-1);
	}
	/*
	 * check for disk errors
	 */
	switch (disk_error) {
	case DISK_STAT_RESERVED:
	case DISK_STAT_UNAVAILABLE:
		fmt_print("\n\nWarning:Drive may be reserved ");
		fmt_print("or has been removed, ");
		fmt_print("aborting surface analysis.\n");
		return (-1);
	case DISK_STAT_NOTREADY:
		fmt_print("\n\nWarning: Drive not ready, ");
		fmt_print("aborting surface analysis.\n");
		return (-1);
	case DISK_STAT_DATA_PROTECT:
		fmt_print("\n\nWarning: Drive is write protected, ");
		fmt_print("aborting surface analysis.\n");
		return (-1);
	default:
		break;
	}
	return (0);
}