1
0
Files
irix-657m-src/eoe/cmd/mediad/Volume.C
2022-09-29 17:59:04 +03:00

921 lines
23 KiB
C

#include "Volume.H"
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <mediaclient.h>
#include <mntent.h>
#include <sys/dir.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "CallBack.H"
#include "Config.H"
#include "Device.H"
#include "DSReq.H"
#include "Event.H"
#include "Log.H"
#include "Partition.H"
#include "Task.H"
#include "strplus.H"
// mount_task is a schedulable task. All mounting is done from
// the mount task.
Task Volume::mount_task(mount_proc, NULL);
// Volumes can be enumerated. Volume::volumes keeps track of them.
Enumerable::Set Volume::volumes;
// The Volume constructor initializes a truckload of variables.
// XXX Security fix late in IRIX 6.5 release.
const char * Volume::_globalOptions = NULL;
Volume::Volume(const VolumeAddress& va, const mntent& mnt, const char *label)
: Enumerable(volumes),
_address(va),
_label(NULL),
_fsname(NULL),
_type(NULL),
_state(VS_NOT_MOUNTED),
_is_ignored(false),
_is_shared(false),
_needs_mount(false),
_forced_unmount(false),
_default_dir(NULL),
_config_dir(NULL),
_sub_dir(NULL),
_default_opts(NULL),
_config_opts(NULL),
_all_opts(NULL),
_subformat(NULL),
_config(new Config(config_proc, this))
{
if (label)
_label = strdupplus(label);
assert(mnt.mnt_fsname != NULL);
_fsname = strdupplus(mnt.mnt_fsname);
assert(mnt.mnt_dir != NULL);
_default_dir = strdupplus(mnt.mnt_dir);
assert(mnt.mnt_type != NULL);
_type = strdupplus(mnt.mnt_type);
if (mnt.mnt_opts != NULL)
_default_opts = strdupplus(mnt.mnt_opts);
// Get the config options from the config file. The call to
// set_ignored() may automatically schedule the Volume for
// mounting, but the Volume will not be mounted until the mount
// task runs.
set_config_dir(_config->mount_dir(va));
set_config_options(_config->mount_options(va));
set_ignored(_config->volume_is_ignored(va));
// set_shared(is_shared_heuristic());
}
// The Volume destructor deallocates a truckload of strings.
Volume::~Volume()
{
delete _config;
delete [] _all_opts;
delete [] _config_opts;
delete [] _default_opts;
delete [] _sub_dir;
delete [] _config_dir;
delete [] _default_dir;
delete [] _type;
delete [] _fsname;
delete [] _label;
}
// set_ignored sets whether this Volume is ignored. If
// the volume is ignored, it's dismounted, otherwise
// it's mounted.
void
Volume::set_ignored(bool value)
{
_is_ignored = value;
if (!_is_ignored && _state != VS_MOUNTED)
{ _needs_mount = true;
mount_task.schedule(Task::ASAP);
}
else if (_is_ignored && _state == VS_MOUNTED)
dismount(false);
}
// The heuristic for sharing is: if the volume is explicitly
// shared or hidden, share/hide it. Otherwise, if all partitions'
// devices are shared, share it. Otherwise, hide it.
bool
Volume::is_shared_heuristic() const
{
if (_config->mount_is_shared(address()))
return true;
if (_config->mount_is_unshared(address()))
return false;
// Check all devices. If any is not shared, do not export.
for (unsigned int i = 0; i < npartitions(); i++)
if (!_config->device_is_shared(partition(i)->device()->address()))
return false;
return true;
}
void
Volume::set_shared(bool value)
{
Event::Type t = Event::None;
bool _was_shared = _is_shared;
if (_was_shared && !value)
{ t = Event::Unexport;
if (_state == VS_MOUNTED)
unshare();
}
_is_shared = value;
if (!_was_shared && _is_shared)
{ t = Event::Export;
if (_state == VS_MOUNTED)
share();
}
// Generate an event for those lucky clients.
if (t != Event::None)
CallBack::activate(Event(t, NULL, this));
}
// Return the one true directory where this volume should
// be mounted. The heuristic is described in Volume.H.
const char *
Volume::dir() const
{
if (_config_dir)
return _config_dir;
else if (_sub_dir)
return _sub_dir;
else
return _default_dir;
}
// Set this volume's config directory. If the config directory
// has changed, force the volume to remount.
void
Volume::set_config_dir(const char *new_config_dir)
{
if (strcmpnull(_config_dir, new_config_dir) != 0)
{
if (is_mounted())
dismount(false);
delete [] _config_dir;
if (new_config_dir)
_config_dir = strdupplus(new_config_dir);
else
_config_dir = NULL;
}
}
// Set this Volume's subdirectory. Assumes that the Volume
// isn't mounted. (I.e., the subdirectory is only set once
// shortly after volume creation.) This method is called
// from a derived class's use_subdir() function.
void
Volume::set_sub_dir(const char *new_subdir)
{
assert(_state != VS_MOUNTED);
if (strcmpnull(_sub_dir, new_subdir) != 0)
{
delete [] _sub_dir;
if (new_subdir)
_sub_dir = strdupplus(new_subdir);
else
_sub_dir = NULL;
}
}
// Calculate whether this Volume needs a subdirectory, and
// construct it if needed. The actual construction is handed
// to the derived class's use_subdir() method.
void
Volume::calc_sub_dir()
{
assert(_state != VS_MOUNTED);
if (!_sub_dir && dir_collides())
use_subdir();
else
set_sub_dir(NULL);
}
// Test whether this Volume has the same base directory as another
// Volume.
bool
Volume::dir_collides()
{
const char *my_dir = base_dir();
assert(my_dir != NULL);
for (Volume *that = first(); that; that = that->next())
{ assert(that->base_dir() != NULL);
if (that != this && !strcmp(my_dir, that->base_dir()))
return true;
}
return false;
}
// Set the configuration options. If they've changed, than dismount
// the volume and delete _all_opts so that _all_opts will be
// recomputed before remounting.
void
Volume::set_config_options(const char *new_config_opts)
{
if (strcmpnull(_config_opts, new_config_opts) != NULL)
{
if (is_mounted())
dismount(false);
delete [] _config_opts;
if (new_config_opts)
_config_opts = strdupplus(new_config_opts);
else
_config_opts = NULL;
// Zap _all_opts so _all_opts will be recomputed next time
// it's used.
delete [] _all_opts;
_all_opts = NULL;
}
}
// Volume::options() returns the mount options string. It lazily
// builds it as needed. The only option that gets special handling
// is the rwopt (read-only vs. read-write). The options string is the
// concatenation of rwopt, the config options, and the default options,
// with duplicates removed.
const char *
Volume::options()
{
if (_all_opts)
return _all_opts;
// Resolve read write vs. read only.
//
// If default_opts includes "ro", then volume is ro.
// If config_opts includes "ro", then volume is ro.
// If any device is read-only or any medium is write-protected,
// then volume is ro.
// Otherwise, volume is rw.
static const char rw[] = "rw";
static const char ro[] = "ro";
const char *rwopt;
mntent mnt;
if ((mnt.mnt_opts = _default_opts) && hasmntopt(&mnt, ro))
rwopt = ro;
else if ((mnt.mnt_opts = _config_opts) && hasmntopt(&mnt, ro))
rwopt = ro;
else
{
rwopt = rw;
const Partition *p;
for (unsigned i = 0; p = partition(i); i++)
{ Device *device = p->device();
if (device->feature_rdonly() || device->is_write_protected())
{ rwopt = ro;
break;
}
}
}
// Now glom all options into one string: the rwopt,
// the config options and the default options.
// We don't try to resolve conflicting options here
// except for identical options and rw vs. ro.
char options[100];
strcpy(options, rwopt);
if (_config_opts)
{
char tempbuf[256];
strcpy(tempbuf, _config_opts);
for (char *p = tempbuf, *opt; *(opt = mntopt(&p)); )
{ if (!strcmp(opt, ro) || !strcmp(opt, rw))
continue; // ro, rw already handled.
// Append option.
strcat(options, ",");
strcat(options, opt);
}
}
if (_default_opts)
{
char tempbuf[256];
strcpy(tempbuf, _default_opts);
for (char *p = tempbuf, *opt; *(opt = mntopt(&p)); )
{ if (!strcmp(opt, ro) || !strcmp(opt, rw))
continue; // ro, rw already handled.
// Extract the option's name. If an option by the same
// name is already there, disregard this option.
char *eq = strchr(opt, '=');
if (eq)
*eq = '\0';
mntent temp_mntent;
temp_mntent.mnt_opts = options;
if (hasmntopt(&temp_mntent, opt))
continue;
if (eq)
*eq = '=';
// Append option.
strcat(options, ",");
strcat(options, opt);
}
}
// XXX Security fix late in IRIX 6.5 release.
if (_globalOptions != NULL) {
strcat(options, ",");
strcat(options, _globalOptions);
}
_all_opts = strdupplus(options);
return _all_opts;
}
// Utility function used by Volume::options(). Returns the
// first comma-delimited substring of a string. Modifies
// the argument string in place and moves the pointer past
// the comma.
char *
Volume::mntopt(char **p)
{
char *cp = *p;
char *retstr;
while (*cp && isspace(*cp))
cp++;
retstr = cp;
while (*cp && *cp != ',')
cp++;
if (*cp) {
*cp = '\0';
cp++;
}
*p = cp;
return (retstr);
}
// Volume::mount() mounts a Volume. It also tries to export the
// filesystem.
void
Volume::mount()
{
assert(!_is_ignored);
assert(_needs_mount);
_needs_mount = false;
// Weirdly, asking for the options string can open the /dev/scsi
// device. So we ask for the options here, then close all devices.
(void) options();
DSReq::close_all();
// Create the mount point, if necessary.
if (make_dir(dir()) != 0)
return;
// Build the mount command and run it.
const char *mount_argv[8] = {
"/etc/mount", "-t", type(), "-o", options(), fsname(), dir(), NULL
};
int mount_status = execute(mount_argv);
if (mount_status != 0)
{
// On the off chance that the mount failed because the device
// was already mounted, try to dismount it and retry the mount.
const char *umount_argv[8] = { "/etc/umount", fsname(), NULL };
execute(umount_argv); // umount doesn't return non zero with
// bad filesys so just have to try again.
mount_status = execute(mount_argv);
if (mount_status != 0)
{
_state = VS_MOUNT_FAILED;
Log::critical("The file system on device: %s cannot be mounted",
fsname());
return;
}
}
_state = VS_MOUNTED;
// Do the export fu.
if (_is_shared)
share();
// Look for inst subformat on this volume.
find_inst_subformat();
find_photoCD_subformat();
// Send an event to everybody who cares.
CallBack::activate(Event(Event::Mount, NULL, this));
}
// Volume::dismount() dismounts a filesystem. Inverse of Volume::mount().
// If the force argument is set, uses "umount -k". This should only
// be used if the disk has been hardware ejected.
void
Volume::dismount(bool force)
{
assert(_state == VS_MOUNTED);
// Do the unexport fu.
if (_is_shared)
unshare();
if (force)
{
// set _forced_unmount for use by our callback forced_unmount().
_forced_unmount = true;
CallBack::activate(Event(Event::ForceUnmount, NULL, this));
_forced_unmount = false;
const char *fuser_argv[8] = {
"/sbin/fuser", "-kc", dir(), NULL
};
if (execute(fuser_argv) != 0)
{
Log::critical("Can't kill processes using device %s prior to dismount", fsname());
return;
}
}
const char *umount_argv[8] = {
"/etc/umount", dir(), NULL
};
if (execute(umount_argv) != 0)
{
Log::critical("The file system on device: %s cannot be dismounted",
fsname());
return;
}
_state = VS_NOT_MOUNTED;
// Remove the subdirectory, if any.
if (_sub_dir)
remove_dir(_sub_dir);
// Drop the subformat, if any.
_subformat = NULL;
// Send an event to everybody who cares.
CallBack::activate(Event(Event::Dismount, NULL, this));
}
void
Volume::share()
{
assert(_is_shared);
// First, try exporting using options in /etc/exports.
const char *exportfs_argv[8] = {
"/usr/etc/exportfs", dir(), NULL
};
if (execute(exportfs_argv) == 0)
return;
// Second, try exporting read-only.
const char *exportfs2_argv[8] = {
"/usr/etc/exportfs", "-i", "-o", "ro", dir(), NULL
};
(void) execute(exportfs2_argv);
}
void
Volume::unshare()
{
const char *exportfs_argv[8] = {
"/usr/etc/exportfs", "-u", dir(), NULL
};
(void) execute(exportfs_argv);
}
int
Volume::execute(const char *argv[8])
{
int status = 0;
#define STR(p) ((p) ? (p) : "")
Log::debug("executing command %s %s %s %s %s %s %s %s",
STR(argv[0]), STR(argv[1]), STR(argv[2]), STR(argv[3]),
STR(argv[4]), STR(argv[5]), STR(argv[6]), STR(argv[7]));
assert(argv[0][0] == '/');
int pid = fork();
if (pid < 0)
{ Log::perror("fork failed");
return -1;
}
if (pid == 0)
{
// Child
execv(argv[0], (char *const *) argv);
Log::perror("%s exec failed", argv[0]);
_exit(1);
}
else
{
// Parent
do
{
if (waitpid(pid, &status, NULL) != pid)
{ Log::perror("%s wait failed", argv[0]);
return -1;
}
} while (!WIFEXITED(status));
if (status != 0)
Log::debug("command failed with status %d", status);
}
return status;
}
// find_photoCD_subformat() looks for a PhotoCD. If it finds one,
// it sets the _subformat field. This method must be called when
// the volume is mounted. We look for the presence of a file "info.pcd"
// inside the directory "photo_cd", to ensure that this is a photo CD.
void
Volume::find_photoCD_subformat()
{
assert(_state == VS_MOUNTED);
// Only ISO 9660 CDs can be PhotoCDs.
if (strcmp(type(), "iso9660") != 0)
return;
char path[PATH_MAX];
(void) sprintf(path, "%s/photo_cd/info.pcd", dir());
struct stat statbuf;
if (stat(path, &statbuf) == 0){
Log::debug("This is a photo CD volume.");
_subformat = MC_SBFMT_PHOTOCD;
}
return;
}
// find_inst_subformat looks for an installation volume.
// This is kind of a kludge, but I don't know of a better place
// to put this.
void
Volume::find_inst_subformat()
{
assert(_state == VS_MOUNTED);
if (has_inst_files())
{
Log::debug("This is an inst volume.");
_subformat = MC_SBFMT_INST;
}
}
bool
Volume::has_inst_files() const
{
assert(_state == VS_MOUNTED);
char dist[PATH_MAX];
(void) sprintf(dist, "%s/.distribution", dir());
struct stat statbuf_unused;
if (stat(dist, &statbuf_unused) == 0)
return true;
(void) sprintf(dist, "%s/dist", dir());
DIR *dir = opendir(dist);
if (!dir)
return false;
struct direct *entry;
bool found = false;
while (entry = readdir(dir))
{ int l = entry->d_namlen;
if (l > 4 && !strcmp(entry->d_name + l - 4, ".idb"))
{ found = true;
break;
}
}
closedir(dir);
return found;
}
// devices_are_shared() returns true if all partitions are on shared devices.
bool
Volume::devices_are_shared() const
{
for (unsigned int i = 0; i < npartitions(); i++)
if (!partition(i)->device()->is_shared())
return false;
return true;
}
// A slight embellishment to mkdir(2). Recursively creates
// parent directories and logs errors to syslog.
int
Volume::make_dir(const char *path)
{
assert(path != NULL && path[0] == '/'); // Absolute paths only
// Does the directory exist?
struct stat statbuf;
if (stat(path, &statbuf) != 0)
{
if (errno != ENOENT)
Log::perror("can't access mount directory \"%s\"", path);
char parent[PATH_MAX];
strcpy(parent, path);
char *p = strrchr(parent, '/');
assert(p != NULL);
if (p == parent)
p++; // Root is "/", not "".
*p = '\0'; // Trim last component off path
int err = make_dir(parent);
if (err != 0)
return err;
if (mkdir(path, 0777) != 0)
{ Log::perror("can't create mount directory \"%s\"", path);
return errno;
}
}
return 0;
}
// A slight embellishment to rmdir(2). Logs errors to syslog.
int
Volume::remove_dir(const char *path)
{
if (rmdir(path) != 0)
{ Log::perror("can't remove subdirectory \"%s\"", path);
return errno;
}
return 0;
}
//////////////////////////////////////////////////////////////////////////////
// Mount Task
// This is the mount task. All Volumes are mounted from this task.
// When we get here, we know that all Volumes on a newly inserted
// Device have been found, so it's safe to calculate subdirectories
// here.
//
// We don't mount all Volumes. If two Volumes "overlap", we only
// mount the "first" one. Let me explain what I mean by "overlap"
// and "first".
//
// "Overlap" means one of two things: either that some partition of
// Volume A has the same sectors as some partition of Volume B or
// that some partition of Volume A is on the same CD-ROM device as
// some partition of Volume B. Because CD-ROMs have to use different
// modes to access different kinds of data, and our mount_* programs
// don't coordinate which mode is selected, we never mount two
// filesystems from the same CD-ROM.
//
// "First" means the Volume that precedes overlapping volumes
// based on these sort rules:
//
// 1. Primary sort key: a Volume mentioned in a "mount" command in
// the config file precedes an unmentioned volume.
//
// 2. Secondary sort key: volume type (ASCII order, modulo the
// Adobe Type On Call CD hack).
//
// 3. Tertiary sort key: volume partition number. WholeDisk
// is -1, so WholeDisk precedes other partitions.
void
Volume::mount_proc(Task& /* task */, void * /* closure */ )
{
Volume *vol;
// Calculate all volumes' sharing heuristic.
for (vol = first(); vol; vol = vol->next())
vol->set_shared(vol->is_shared_heuristic());
// Find volumes that can't be mounted; don't bother mounting them.
for (vol = first(); vol; vol = vol->next())
if (vol->needs_mount() && vol->cant_mount())
vol->_needs_mount = false;
else if (vol->is_mounted() && vol->cant_mount())
vol->dismount(false);
// Calculate the subdirectories and mount all remaining volumes.
for (vol = first(); vol; vol = vol->next())
if (vol->_needs_mount)
{ vol->calc_sub_dir();
vol->mount();
vol->_needs_mount = false;
}
}
// Test whether a volume can't be mounted because it's preceded
// by an overlapping volume.
bool
Volume::cant_mount() const
{
for (Volume *that = first(); that; that = that->next())
if (that != this && overlaps(*that))
{
bool this_is_mentioned = _config->volume_is_mentioned(address());
bool that_is_mentioned = _config->volume_is_mentioned(that->address());
if (that_is_mentioned && !this_is_mentioned)
return true;
if (this_is_mentioned && !that_is_mentioned)
continue;
int delta = typecmp(that->type(), type());
adobe_type_on_call_hack(delta, that);
if (delta < 0)
return true;
if (delta > 0)
continue;
int my_partno = partition(0)->index();
int his_partno = that->partition(0)->index();
if (his_partno < my_partno)
return true;
}
return false;
}
int
Volume::typecmp(const char *name0, const char *name1) const
{
// Stupid special case -- iso9660 precedes hfs. Otherwise
// it's alphabetical.
if (!strcmp(name0, "iso9660") && !strcmp(name1, "hfs"))
return -1;
if (!strcmp(name0, "hfs") && !strcmp(name1, "iso9660"))
return 1;
return strcmp(name0, name1);
}
// Oh, my God! This is so embarassing.
//
// Sometime in 1994/1995 or so, Adobe figured out that the old mediad
// defaults mounting a hybrid HFS/ISO CD-ROM as HFS (unlike all the
// other Unix vendors that prefer ISO). As far as I can tell, that
// was not planned, it's just a side effect of the way cdromd was
// written. Anyway, because of that, Adobe put their SGI
// distribution on the HFS part of their Type On Call CD-ROM, which
// is the same CD for Mac, Windoze, Sun and SGI. Then I went and
// changed the default to ISO and broke it.
//
// So the hack is, if we have a CD with both an ISO partition and an
// HFS, and the volume label of each half matches "TOC_*",
// then we prefer to mount it as HFS, not ISO. I sure hope Adobe
// doesn't change its volume label so it stops matching that regexp.
void
Volume::adobe_type_on_call_hack(int& delta, const Volume *that) const
{
assert(overlaps(*that));
if (is_toc() && that->is_toc())
{
// This is a Type On Call CD.
if (!strcmp(type(), "iso9660") && !strcmp(that->type(), "hfs"))
delta = -1; // prefer this
else if (!strcmp(type(), "hfs") && !strcmp(that->type(), "iso9660"))
delta = +1; // prefer that
}
}
// Check whether the volume label starts with "TOC_".
bool
Volume::is_toc() const
{
const char *lab = label();
return lab != NULL && strncmp(lab, "TOC_", 4) == 0;
}
// Test whether two volumes overlap. Any two volumes on the same
// CD-ROM always overlap.
bool
Volume::overlaps(const Volume& that) const
{
// Iterate over all partitions in both volumes.
const Partition *my_part, *his_part;
for (unsigned int i = 0; my_part = partition(i); i++)
{
__uint64_t my_start = my_part->start_sector();
__uint64_t my_end = my_start + my_part->n_sectors();
for (unsigned int j = 0; his_part = that.partition(j); j++)
{
Device *my_device = my_part->device();
Device *his_device = his_part->device();
if (my_device != his_device)
continue;
// Problems with CD-ROM mode select mean we never mount
// more than one partition of a CD-ROM. So we declare
// that any two partitions on the same CD-ROM overlap.
// I have a dream of a day when this restriction can
// be removed.
if (my_device->filesys_cdda())
return true;
// Test whether this pair of partitions overlaps.
__uint64_t his_start = his_part->start_sector();
__uint64_t his_end = his_start + his_part->n_sectors();
if (my_start < his_end && my_end > his_start)
return true;
}
}
return false;
}
//////////////////////////////////////////////////////////////////////////////
// Config Task
// This is the config task. Whenever the config file changes, this
// task sets the appropriate fields in the Volume. The Volume's
// set_* methods cleverly dismount or remount the Volume if
// it's needed.
void
Volume::config_proc(Config& config, void *closure)
{
Volume *vol = (Volume *) closure;
const VolumeAddress& va = vol->address();
// Set the mount directory/options.
vol->set_config_dir(config.mount_dir(va));
vol->set_config_options(config.mount_options(va));
// Set whether the volume is ignored. If it is not ignored, and
// if one of the preceding set_ methods dismounted the volume,
// set_ignored() will schedule it for remounting.
vol->set_ignored(config.volume_is_ignored(va));
// Schedule the mount proc. The mount proc may figure out that
// there's more to be done.
mount_task.schedule(Task::ASAP);
}