#!/usr/bin/perl # # sessmaint This script is a general management interface for the session # management commands provided by the dirstack and oldjobs # scripts. Those commands store session information in a # sessions directory. sessmaint provides a set of maintenance # operations to handle those files. # # usage: # sessmaint [-... | -help | -Version] # # Revision History # 1.0 Initial revision. 150719 # 1.1 Added licensing info. 180531 # # Written by Wayne Morrison, 150719. # # Copyright 2015 Wayne Morrison # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # use strict; use Cwd; use Getopt::Long qw(:config no_ignore_case_always); # # Version information. # my $NAME = "sessmaint"; my $VERS = "$NAME version: 1.1"; ############################################################################ # # Options fields. # my %opts = (); # Options. # # Command line arguments. # my @opts = ( 'conf=s', # Configuration file. 'dir=s', # Sessions directory. 'list=s', # List files for a session. 'sessions', # List sessions. 'clear', # Delete all unused job files. 'clone', # Clone one session to another. 'prune', # Delete non-recent job files. 'save', # Save active job files. 'zap', # Delete non-standard sessions. 'test', # Set up for tests. 'info', # Give file info. 'verbose', # Give verbose output. 'help', # Give a help message. 'Version', # Display the program version. ); my $clear = 0; # -clear flag. my $clone = 0; # -clone flag. my $infoflag = 0; # -info flag. my $lister = ''; # -list flag. my $prune = 0; # -prune flag. my $save = 0; # -save flag. my $sessionflag = 0; # -sessions flag. my $zap = 0; # -zap flag. my $verbose = 0; # -verbose flag. my $testing = 0; # -test flag. ############################################################################ my $DEFCONF = 'sessions'; # Default sessions file. my $DEFDIR = "~/.sessions"; # Default sessions directory. my $sessdir; # Sessions directory. my $sessions; # Sessions file. my %allsessions = (); # All session names. my %stdsessions = (); # Hashed names of common sessions. my @stdsessions = (); # Names of common sessions. my %nonstdsessions = (); # Hashed names of uncommon sessions. my @nonstdsessions = (); # Names of uncommon sessions. my @sessions = (); # Sessions from command line. ############################################################################ main(); exit(0); #----------------------------------------------------------------------------- # Routine: main() # sub main { $| = 1; # # Munch on the options and arguments. # optsandargs(); # # Ensure the sessions directory exists. # chkdir(); # # Get the session names. # getsessions(); # # Display information on sessions. # showsessions(); listfiles(); # # Clean out the certain sets of files. # clear() if($clear); clone() if($clone); prune() if($prune); save() if($save); zap() if($zap); } #---------------------------------------------------------------------- # Routine: optsandargs() # # Purpose: Parse the command line for options and arguments. # sub optsandargs { my $totargs; # Total number of arguments. # # Parse the options. # GetOptions(\%opts,@opts) || usage(); # # Check for some immediate-action options. # usage() if(defined($opts{'help'})); version() if(defined($opts{'Version'})); $clear = $opts{'clear'}; $clone = $opts{'clone'}; $infoflag = $opts{'info'}; $lister = $opts{'list'} if(defined($opts{'list'})); $prune = $opts{'prune'}; $save = $opts{'save'}; $sessionflag = $opts{'sessions'}; $zap = $opts{'zap'}; $verbose = $opts{'verbose'}; # # Ensure only one command argument was given at a time. # $totargs = $clear + $clone + $infoflag + ($lister ne '') + $prune + $save + $sessionflag + $zap; if($totargs != 1) { print STDERR "$0: a single command argument must be given per execution\n"; exit(3); } $testing = $opts{'test'}; # # Get the sessions directory name. # if(defined($opts{'dir'})) { $sessdir = $opts{'dir'}; } else { $sessdir = glob($DEFDIR); } # # Get the sessions filename. # if(defined($opts{'conf'})) { $sessions = $opts{'conf'}; } else { $sessions = "$sessdir/$DEFCONF"; } # # Set up for testing. # if($testing) { print "in testing mode\n\n"; $sessdir = `session-tests -directory`; chomp($sessdir); if($sessdir eq '') { print STDERR "$0: session-tests did not return the testing directory; exitting...\n"; exit(4); } $sessions = "$sessdir/sessions"; } # # Save the session names given on the command line. # @sessions = @ARGV; if($infoflag) { print "sessdir: $sessdir\n"; print "sessions: $sessions\n"; exit(0); } # # If we're cloning, we've got to have two session names. # if($clone && (@ARGV != 2)) { print STDERR "usage: $0 -clone \n"; exit(5); } } #---------------------------------------------------------------------- # Routine: chkdir() # # Purpose: Ensure that the sessions directory exists. # sub chkdir { # # Create the sessions directory if it doesn't exist already. # If we can't create it, we'll complain. # We'll exit regardless, if the sessions directory doesn't exist. # If we created it, then there's nothing there to check. # If we couldn't create it, again, there's nothing to check. # if(! -d $sessdir) { print "creating sessions directory \"$sessdir\"\n"; mkdir($sessdir); if(! -d $sessdir) { print STDERR "$0: unable to create sessions directory \"$sessdir\"\n"; } exit(0); } } #---------------------------------------------------------------------- # Routine: getsessions() # # Purpose: Get the list of "standard" session names. # sub getsessions { my @files = (); # Files in sessions dir. my $curdir; # Current directory. #################################################################### # # Get the standard sessions in the sessions directory. # # # Ensure the sessions file exists and is readable. # if(! -f $sessions) { print STDERR "$0: sessions file does not exist - \"$sessions\"\n"; exit(10); } if(! -r $sessions) { print STDERR "$0: sessions file is not readable - \"$sessions\"\n"; exit(11); } # # Open the sessions file. # if(open(SFILE, "< $sessions") == 0) { print STDERR "$0: unable to open sessions file - \"$sessions\"\n"; exit(12); } # # Read the sessions file and skip comment lines. We'll add the # session names to the list of standard sessions and the list # of all sessions. # while() { my $ln = $_; chomp($ln); $ln =~ s/^\s*//; next if($ln eq ''); next if($ln =~ /^#/); if(! defined($stdsessions{$ln})) { push @stdsessions, $ln; $stdsessions{$ln} = 1; $allsessions{$ln} = 1; } } # # Sort the sessions list and close the session file. # @stdsessions = sort(@stdsessions); close(SFILE); #################################################################### # # Get the non-standard sessions in the sessions directory. # # # Get the session filenames from the sessions directory. # $curdir = getcwd(); chdir($sessdir); @files = glob("j-* d-*"); # # Get the session names of the sessions stored in the sessions # directory. # foreach my $fn (sort(@files)) { my $sessname = ''; # Session name in file. # # Pull the session name from the filename. # $fn =~ /^[dj]-(.*)-[0-9]+$/; $sessname = $1; # # Save the session name -- as long as it isn't standard. # if((! defined($stdsessions{$sessname})) && (! defined($nonstdsessions{$sessname}))) { push @nonstdsessions, $sessname; $nonstdsessions{$sessname} = 1; $allsessions{$sessname} = 1; } } # # Return home. Or at least back to where we had hung our hat. # chdir($curdir); } #---------------------------------------------------------------------- # Routine: showsessions() # # Purpose: Show the names of the sessions with files in the tty # directory. An indicator will also be given showing # whether or not the session is standard or not. # sub showsessions { # # Only do this stuff if -sessions was given. # return if(! $sessionflag); # # Print the session info and exit. # foreach my $sn (sort(keys(%allsessions))) { printf("%-20s\t%s\n", $sn, defined($stdsessions{$sn}) ? 'standard' : 'non-standard'); } exit(0); } #---------------------------------------------------------------------- # Routine: listfiles() # # Purpose: List the extant files for the named session. # sub listfiles { my @files = (); # Files in sessions directory. my %sessions = (); # Sessions in sessions directory. my @sessfiles = (); # Session's files in sessions directory. # # Only do this stuff if -lister was given. # return if(! $lister); # # Get the session filenames from the sessions directory. # chdir($sessdir); @files = glob("j-* d-*"); # # Get the session names of the sessions stored in the sessions # directory. # foreach my $fn (sort(@files)) { my $sessname = ''; # Session name in file. # # Pull the session name from the filename. # $fn =~ /^[dj]-(.*)-[0-9]+$/; $sessname = $1; # # Save the filename if it belongs to the named session. # push @sessfiles, $fn if($sessname eq $lister); } # # Print the session's info and exit. # foreach my $sn (sort(@sessfiles)) { printf("%-20s\t%s\n", $sn); } exit(0); } #---------------------------------------------------------------------- # Routine: clear() # # Purpose: Delete all but the active job and terminal files. # sub clear { my @fnarr; # List of files to delete. my $cnt; # Count of files to delete. # # Get the files in the sessions directory. # if((@stdsessions == 0) && (@nonstdsessions == 0)) { print "no sessions defined\n"; exit(0); } vprint("clearing the sessions directory\n"); # # Dump the .save files. # chdir($sessdir); @fnarr = glob("j-*.save d-*.save"); $cnt = @fnarr; # # If there are any .save files, we'll delete them all. # if($cnt > 0) { vprint("\tdeleting all .save files\n"); system("rm -f d-*.save"); system("rm -f j-*.save"); } else { vprint("\tno .save files to delete\n"); } # # Delete all the job and directory files for each session, # delold('d', 'directory', 0); delold('j', 'jobs', 0); } #---------------------------------------------------------------------- # Routine: clone() # # Purpose: Clone an existing session to a new session. # In addition to copying the files, the new session name # will be added to the sessions file. # sub clone { my $cursess; # Session to clone. my $newsess; # New session's name. my @files; # Session's files. my $maxlen = 0; # Maximum filename length. # # Get the session names. # $cursess = $ARGV[0]; $newsess = $ARGV[1]; vprint("cloning session \"$cursess\" to \"$newsess\"\n"); # # Move to the sessions directory. # chdir($sessdir); # # Ensure that the new session does not exists. # @files = glob("[dj]-$newsess-*"); if(@files != 0) { print STDERR "$0: unable to clone to existing session \"$newsess\"\n"; exit(7); } # # Ensure that the active session actually exists. # @files = glob("[dj]-$cursess-*"); if(@files == 0) { print STDERR "$0: unable to clone non-existent session \"$cursess\"\n"; exit(6); } # # Find the longest filename in the active session. # foreach my $cfn (@files) { my $len = length($cfn); # Length of filename. $maxlen = $len if($len > $maxlen); } # # Copy the existing session's files. # foreach my $cfn (@files) { my $nfn; # New filename. $nfn = $cfn; $nfn =~ s/^([dj])-$cursess-(.*)/$1-$newsess-$2/; printf("\t%-*s\t$nfn\n",$maxlen,$cfn) if($verbose); system("cp $cfn $nfn"); } # # Add the new session to the sessions file. # if(open(SFILE, ">> $sessions") == 0) { print STDERR "$0: unable to open sessions file - \"$sessions\"\n"; exit(15); } print SFILE "$newsess\n"; close(SFILE); } #---------------------------------------------------------------------- # Routine: prune() # # Purpose: Delete all the saved session files except for the most recent. # sub prune { vprint("pruning old file from the sessions directory\n"); # # Move to the sessions directory. # chdir($sessdir); # # Delete all but the most recent .save files. # delold('d', 'directory', 1); delold('j', 'jobs', 1); } #---------------------------------------------------------------------- # Routine: save() # # Purpose: Copy each session's active files into saved versions. # # There's the possibility of collisions here, if the pid # used for a particular saved session is duplicated in a # active session. We won't worry about this for now. # sub save { my $totcnt = 0; # Total count of saved files. vprint("saving active files to saved files\n"); # # Move to the sessions directory. # chdir($sessdir); # # Go through all the sessions and copy all that session's active # files to being saved files as well. # foreach my $sn (sort(keys(%allsessions))) { my @files; # Session's files. my $fcnt = 0; # Count of saved files. @files = glob("[dj]-$sn-*"); next if(@files == 0); foreach my $fn (@files) { next if($fn =~ /\.save$/); $fcnt++; system("cp $fn $fn.save"); } vprint("\tactive files saved for session $sn: $fcnt\n") if($fcnt); # # Bump up our total count of files saved. # $totcnt += $fcnt; } vprint("\nactive files saved for all sessions: $totcnt\n"); } #---------------------------------------------------------------------- # Routine: zap() # # Purpose: Delete directory files and job files for non-standard sessions. # sub zap { # # Don't do anything if there aren't non-standard files in # the sessions directory. # if(@nonstdsessions == 0) { print "no non-standard sessions\n"; exit(0); } vprint("deleting non-standard session files:\n"); # # Delete the directory and job files for each non-standard session. # foreach my $sessname (@nonstdsessions) { my @fnarr; # List of files to delete. my $cnt; # Count of files to delete. vprint("\t$sessname\n"); # # Dump the directory files. # @fnarr = glob("$sessdir/d-$sessname-*"); $cnt = @fnarr; if($cnt > 0) { system("rm $sessdir/d-$sessname-*"); vprint("\t\tdirectory files: $cnt\n"); } # # Dump the job files. # @fnarr = glob("$sessdir/j-$sessname-*"); $cnt = @fnarr; if($cnt > 0) { system("rm $sessdir/j-$sessname-*"); vprint("\t\tjob files: $cnt\n"); } } } #---------------------------------------------------------------------- # Routine: delold() # # Purpose: Delete all the job *or* directory files for each session, # except for the most recently used file. The routine's # parameter determines if the job or directory files will # be deleted. # # An argument flag will indicate whether or not save files # should be examined exclusively. If the flag is on, then # .save files will be the only ones examined. If it's off, # then we'll assume the .save files have been deleted # already and thus they won't be examined. # sub delold { my $prefix = shift; # File substring to glob. my $pfxstr = shift; # Prefix type. my $saveonly = shift; # Save-files-only flag. vprint("\tdeleting old $pfxstr files\n"); # # Go through all the sessions and delete all that session's files # except for the most recently used file. # foreach my $sn (sort(keys(%allsessions))) { my @fnarr; # List of files to delete. my $tempus = 0; # File's last mod time. my $latest; # Most recently modified file. # # Get a list of the files to examine. # if($saveonly) { @fnarr = glob("$prefix-$sn-*.save"); } else { @fnarr = glob("$prefix-$sn-*"); } # # Don't search and test things if this is the only # active file this session has. # next if(@fnarr == 1); # # Find the most recently modified file from the file list. # foreach my $fn (sort(@fnarr)) { my @fstats; # File's stats. @fstats = stat($fn); # # Save the name of the most recently modified file, # as well as its modification time. # if($fstats[9] > $tempus) { $latest = $fn; $tempus = $fstats[9]; } } # # Go through this session's active files and delete those # that aren't the latest. # foreach my $fn (sort(@fnarr)) { next if($fn eq $latest); unlink($fn); } } } #---------------------------------------------------------------------- # Routine: vprint() # # Purpose: Print a line of text if the verbose option was set. # sub vprint { my $str = shift; chomp($str); print "$str\n" if($verbose); } #---------------------------------------------------------------------- # Routine: version() # # Purpose: Print the version number(s) and exit. # sub version { print STDERR "$VERS\n"; exit(0); } #---------------------------------------------------------------------- # Routine: usage() # # Purpose: Give usage message and exit. # sub usage { print STDERR "usage: sessmaint [options]\n"; print STDERR "\n"; print STDERR "\twhere [options] are:\n"; print STDERR "\t\t-conf cfile give a non-standard configuration file\n"; print STDERR "\t\t-dir dir specify a non-standard sessions directory\n"; print STDERR "\n"; print STDERR "\t\t-info give file information\n"; print STDERR "\t\t-sessions list session names\n"; print STDERR "\t\t-list session list files for a session\n"; print STDERR "\n"; print STDERR "\t\t-clear delete all but active directory and job files\n"; print STDERR "\t\t-clone old new copy a named session's files to another name\n"; print STDERR "\t\t-prune delete all but most recent saved files\n"; print STDERR "\t\t-save copy active files to .save files\n"; print STDERR "\t\t-zap delete files for non-standard sessions\n"; print STDERR "\n"; print STDERR "\t\t-verbose give verbose output\n"; print STDERR "\t\t-help\n"; print STDERR "\t\t-Version\n"; exit(0); } #---------------------------------------------------------------------- # Routine: info_prtsessions() # # Purpose: List the session names, divided by type. # sub info_prtsessions { print "standard sessions:\n"; foreach my $sn (sort(@stdsessions)) { print "\t\"$sn\"\n"; } print "\n"; print "non-standard sessions:\n"; foreach my $sn (sort(@nonstdsessions)) { print "\t\"$sn\"\n"; } print "\n"; print "all sessions:\n"; foreach my $sn (sort(keys(%allsessions))) { print "\t\"$sn\"\n"; } exit(0); } #---------------------------------------------------------------------- # Routine: getatoms() # # Purpose: Return the constituent atoms of a session filename. # # This isn't currently used. It's (probably) intended # for use in testing. # sub getatoms { my $fn = shift; # Filename. my $prefix; # Type-specifying prefix. my $ttynm; # Name of entry's tty. my $ttysfx; # End chunk of entry. # print "getatoms: <$fn>\n"; $fn =~ /\/([dj])-(.+)-(.*)$/; $prefix = $1; $ttynm = $2; $ttysfx = $3; # print "\tprefix - <$prefix>\n"; # print "\tttynm - <$ttynm>\n"; # print "\tttysfx - <$ttysfx>\n"; return($prefix,$ttynm,$ttysfx); } 1; ############################################################################## =pod =head1 NAME B - data management for the B and B session management commands =head1 SYNOPSIS sessmaint [options] ... =head1 DESCRIPTION B is a general management interface for the session management commands provided by the B and B scripts. Those commands store session information in a sessions directory. B provides a set of maintenance operations to handle those files. The sessions directory is in B<~/.sessions>. It contains a list of "standard" sessions that the user habitually uses. This list will vary from user to user. This file is in B<~/.sessions/sessions>. The other files in the sessions directory may be looked at in two ways -- jobs files and directory files, or active sessions and saved sessions. Jobs files store a list of jobs the user is expecting to have available at the re-start of a particular named session. (For example, after logging out and then logging in again.) The jobs files have the I prefix. This file can be created by redirecting the output of the B command into the jobs file. Jobs files are used by B. Directory files are a list of directories to be added to a directory stack. This is the directory stack from the named session when it was last exitted, with the assumption that the B and B commands will be followed by a fresh stack save. Directory files are used by B. Saved sessions are the jobs and directories files for a particular named session with a particular process id. These files have the I<.save> suffix. They are not used by anything but B. They may be used to restore old sessions or for historical reference. B provides the following operations: =over 4 =item * delete all but the active job and terminal files =item * delete all but most recent saved files =item * copies the active files to I<.save> versions =item * copy a named session's files to a new session name =item * delete files for non-standard sessions =item * give information on the sessions directory and sessions file =item * list all session names =item * list files for a particular session =back =head1 SESSION NAMES B Be careful with session names. It'd be best to stick with distinct session names, and avoid being clever. Simple names have been used very successfully (e.g., "dev", "docs", "24", "middle48"). Session names containing spaces and non-alphanumerics I work, but they haven't been tested much at all. It is hoped that such names will be tested and supported at some point, but those definitely fall into the RSN category. Use those names at your own risk. =head1 OPTIONS B takes the following options: =over 4 =item I<-clear> This option deletes all but the active job and directory files. All B<*.save> files will be deleted as well. =item I<-clone oldsession newsession> This option clones an existing session to a new session. All the existing session's files will be copied and given the new session's name. The process-id portion of the filenames will be copied because there's not much else that can be done. In addition to copying the files, the new session name will be added to the sessions file. =item I<-conf conffile> This option selects an alternate sessions file. =item I<-dir> This option specifies the sessions directory that should be used. =item I<-info> This option shows the paths used for the sessions directory and for the B file. =item I<-list session> This option lists the files associated with the named session. =item I<-prune> This option deletes all the B<*.save> files except for the most recent. =item I<-save> This option copies the active files into B<*.save> versions. =item I<-sessions> This option lists the session names. The names are taken from the sessions file (standard sessions) and the non-standard session from the sessions directory. =item I<-testing> This option forces B to work in testing mode. The session directory will be the standard test directory, as reported by B. The sessions file will be located in that directory. Most users won't need this option. =item I<-zap> This option deletes job and directory files for non-standard sessions. =item I<-verbose> This option gives more extensive descriptions about what is being done. =item I<-Version> Display the version information for B. =item I<-help> Display a help message. =back =head1 AUTHOR Wayne Morrison, wayne@waynemorrison.com =head1 LICENSE Copyright 2015 Wayne Morrison Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =head1 SEE ALSO B, B, B =cut __DATA__ todo list: -jobrestore make a saved session the active session - move active session to be saved - move most recent saved session (that isn't session just saved) to be active session -archive save named session's files to non-clear'd, non-prune'd, non-zap'd names -standard add a session name to the standard sessions file. there should be a corresponding option to remove a session name from the file - more testing for names with spaces, parens, etc.