#!/usr/bin/perl use IO::File; use warnings; use strict; use Data::Dumper; use Date::Parse; use POSIX qw(strftime); my $false=0; my $true=1; my $progname=$0; $progname =~ s%.*/%%g; $~ = 1; my $force="-f"; sub root_exec_cap($) { my ($cmd)=@_; my $uid=$<; if($uid == 0) { return `$cmd`; } else { return `sudo $cmd`; } } my %childprocs=(); sub eprint(@) { print STDERR join(" ",@_)."\n"; } sub read_fsck_progress($) { my ($logfile)=@_; use bytes; if(sysopen(INLOGF, $logfile,O_RDONLY)) { use Fcntl qw(SEEK_END); use IO::Handle; my $position = sysseek(INLOGF,-300,SEEK_END); if(defined($position)) { print STDERR "file position is now $position\n"; } my $data; my $nread = sysread(INLOGF,$data,9000); if(!defined($nread)) { print STDERR "error:",$!,"\n"; } # print STDERR "read $nread ".length($data)." bytes of length\n"; close INLOGF; my @lines=split(/\0/,$data); if(@lines) { # print STDERR "got a line:$lines[0]\n"; my $line=pop @lines; if($line =~ /\s([\d.]+)%/) { return $1; } } } else { print STDERR "Failed opening $logfile\n"; } return "?"; } sub execinbg($$@) { my ($tag,$log,@args)=@_; # return; my $cpid = fork(); if($cpid == 0) { #it is nice practice to close these file descriptors, and if we don't close them, then the parent #process's X connection gets all fucked up anyway. for my $fd (1..255) { POSIX::close($fd); } use POSIX qw(setsid); open(STDIN,"$log"); open(STDERR,">$log.err"); chdir "/"; setsid(); print STDERR "Calling ",join(",",@args),"\n"; if($< != 0) { unshift(@args,"sudo"); } system(@args); if(open(DONEF,">$log.done")) { close(DONEF); } exit($?); } else { $SIG{CHLD}="IGNORE"; $childprocs{$tag}=$cpid; return $cpid; } } my %alldisks=(); my %label2dev=(); my %dev2label=(); my $discovered_labels=$false; my $safeToFsck=qr/ext\d/; my @supported=(); my %checkneeded=(); my %disks=(); my %infos=(); my $anhour = 60 * 60; my $aday = 24 * $anhour; my $toSoon = 4 * $aday; my $mountsToSoon = 4; sub daysandhours($) { my ($secs)=@_; my $days = int($secs / $aday); my $hrs = int(($secs % $aday)/ $anhour); return $days ." days " . $hrs ." hrs"; } sub device($) { my ($name)=@_; if($name =~ /^LABEL=(.*)$/) { return $label2dev{$1}; } elsif(exists $label2dev{$name}) { return $label2dev{$name}; } else { return $name; } } sub discover_labels() { my @disks=scsidevs(); for my $d (@disks) { my $l=root_exec_cap("/sbin/vol_id -l '$d'"); chomp $l; if(length($l)) { $dev2label{$d}=$l; $label2dev{$l}=$d; } } } sub ddev($) { return $_[0]->[0]; } sub dlabel($) { return $_[0]->[1]; } sub ddevspec($) { return $_[0]->[2]; } sub dmnt($) { return $_[0]->[3]; } sub dfs($) { return $_[0]->[4]; } sub dopt($) { return $_[0]->[5]; } sub disk($) { return $alldisks{$_[0]}; } sub read_fstab() { if(open(INF,") { chomp; s/^\s+//; s/\s+$//; next unless length($_)>0 && ! /^[#]/; my @parts=split(/\s+/); if(@parts >= 4) #5th and 6th fields are optional { my $lbl=$parts[0]; my $dev=device($lbl); if(defined($dev)) { if($lbl =~ /^LABEL=(.*)$/) { $lbl=$1; } else { $lbl=$parts[1];#use the mount-point. } #$lbl =~ s%^(LABEL=|/dev/)%%; $alldisks{$lbl}=[$dev,$lbl,@parts]; } } else { eprint "unrecognised line format:",$_; } } close INF; } } sub is_supported($) { my ($name)=@_; my $info=$alldisks{$name}; return dfs($info) =~ /$safeToFsck/; } sub scsidevs() { my @disks=`ls /dev/sd*[0-9]`; chomp @disks; return @disks; } sub e2fsinfo($) { my ($d)=@_; eprint "e2fsinfo $d"; my $dev=ddev($d); my @lines=root_exec_cap("/sbin/dumpe2fs -h '$dev'"); chomp @lines; # for(@lines) { eprint $_; } my %info=(); for (grep(/mount count|last checked|check interval|next check/i,@lines)) { if(my ($key,$val)=/^(maximum mount count|mount count|last checked|next check|check interval)[^:]*:\s*(.*)\s*$/i) { $key =lc($key); $key =~ s/maximum/max/; $key =~ s/mount count/mounts/; $key =~ tr/ /-/; if($key =~ /check/) { if($key =~ /interval/) { $val=(split(/\s/,$val))[0]; } else { $val=str2time($val); } } $info{$key}=$val; } else { eprint "unrecognised line:",$_; } } return %info; } sub fsinfo($) { my ($d)=@_; eprint "fsinfo $d"; my $fs=dfs($d); eprint "fs=$fs"; return $fs =~ /^ext\d$/ ? (e2fsinfo($d)) : (); } sub read_fsinfo($) { my ($n)=@_; eprint "name:$n"; eprint "dev:".device($n); my $d=disk($n); $infos{$n}={disk=>$d,fsinfo($d)}; } sub determine_supported() { @supported = grep { is_supported($_); } keys %alldisks; %disks = map { ($_=>$alldisks{$_}) } @supported; } sub check_fsck_needed(@) { my (@which)=@_; if(!@which) { @which=(keys %disks); %checkneeded=(); } my $now=time(); for my $n (@which) { read_fsinfo($n); my $d=$infos{$n}; eprint ::Dumper($d); $d->{'till-check'}=$d->{'next-check'}-$now; $d->{'mounts-left'}=$d->{'max-mounts'} -$d->{mounts}; $d->{reasons}=[]; my $when=""; my $what; $what=$d->{'till-check'}; if($what < $toSoon) { $when = ($what < 0) ? "overdue" : "soon"; push(@{$d->{'reasons'}},"date"); } $what=$d->{'mounts-left'}; if($what <= $mountsToSoon) { $when = ($what < 0) ? "overdue" : "soon"; push(@{$d->{'reasons'}},"mount count"); } if($when) { $d->{'check-needed'}=$when; $checkneeded{$n}++; } } } my %fonts=(); sub getfont($) { my ($name)=@_; my $font=Gtk2::Pango::FontDescription->from_string($name); $fonts{$name}=$font; } sub makecolor($$) { my ($widget,$colourname)=@_; $widget->modify_fg('normal',Gtk2::Gdk::Color->parse($colourname)); } sub makered($) { my ($widget)=@_; makecolor($widget,'red'); } sub changefont($$) { my ($widget,$fontname)=@_; $widget->modify_font(getfont($fontname)); } sub setred($$) { my ($widget,$isred)=@_; if($isred) { makered($widget); } } sub fontify($$) { my ($widget,$when)=@_; $when=defined($when)?$when:""; my $font="Terafic 9"; my $color=""; if($when eq "overdue") { $color="red"; $font="Terafic Bold 12";} elsif($when eq "soon") { $color="dark orange"; $font = "Terafic Bold Italic 10"; } else { $color="darkgreen"; $font="Terafic 9"; } if($font) { changefont($widget,$font); } if($color) { makecolor($widget, $color); } } my %mounted=(); sub parse_mtab() { my @lines=`cat /etc/mtab`; chomp @lines; %mounted=(); for my $l (@lines) { if($l =~ m%^(/dev/\S+)\s%) { #print STDERR "set mounted $1 to $true\n"; my $dev=$1; my $lbl=$dev2label{$dev}; if(defined($lbl)) { $mounted{$lbl}=$true; } } } } my %buttons=(); my %checking=(); sub save_buttons($$$$) { my ($dev,$umb,$cb,$mb)=@_; $buttons{$dev}=[$umb,$cb,$mb]; } sub activate_buttons() { parse_mtab(); for my $k (keys %buttons) { my $mounted=$mounted{$k} ? 1 : 0; #print STDERR "Mounted $k ? $mounted\n"; my ($umb,$cb,$mb)=(@{$buttons{$k}})[4..6]; $umb->set('sensitive',$mounted && !$checking{$k}); $cb->set('sensitive',!$mounted && !$checking{$k}); $mb->set('sensitive',(!$mounted) && !$checking{$k}); } } sub maketmpfile($) { my ($dev)=@_; my $time=time(); $dev =~ tr%/%_%; my $dir="/tmp/$progname"; my $fn=$progname."-".$dev."-".$time; if(! -d $dir) { mkdir $dir; } if(! -d $dir) { $dir="/tmp"; } if(! -d $dir) { die "could not make a temp dir"; } return $dir."/".$fn; } sub domount($$$) { my ($action,$d,$what)=@_; my $opts=dopt($d); my $user=$false; for my $o (split(/,/,$opts)) { if($o eq "user") { $user=$true; } elsif($o eq "nouser") { $user=$false; } } my @action= ($user ||($< == 0)) ? ($action,$what) : ("sudo $action $what"); system(@action); } sub setup_buttons($) { my ($n)=@_; my $d=$infos{$n}; my $guistuff=$buttons{$n}; my ($l,$textw,$nexttw,$nextmw,$umb,$cb,$mb)=@{$guistuff}; my $dd=$d->{disk}; my $dev=ddev($dd); my $mntpnt=dmnt($dd); $umb->signal_connect("clicked", sub { parse_mtab(); if($mounted{$n} && !$checking{$n}) { domount("/bin/umount",$dd,$mntpnt); update_gui($n); } }); $mb->signal_connect("clicked", sub { parse_mtab(); if((!$mounted{$n}) && !$checking{$n}) { domount("/bin/mount",$dd,$mntpnt); update_gui($n); } }); $cb->signal_connect("clicked", sub { parse_mtab(); if((!$mounted{$n}) && !$checking{$n}) { my $log=maketmpfile($n); if(dfs($dd) =~ /^ext\d/) { print STDERR "dev $n is $mntpnt\n"; my $cpid = execinbg("check $dev",$log,"/sbin/e2fsck",($force?($force):()),"-p","-C","0",$dev); $checking{$n}=[$log,$cpid]; } } activate_buttons(); }); } sub update_gui(@) { my ($n,$what,$progress)=@_; check_fsck_needed($n); my $d=$infos{$n}; my $guistuff=$buttons{$n}; my ($l,$textw,$nexttw,$nextmw,$umntbtn,$checkbtn,$mntbtn)=@{$guistuff}; if(defined($what) && $what eq "progress") { $textw->set_text("checking [$progress%]"); return; } my $needed=$d->{'check-needed'}; my $font="Terafic"; my $fontsz=10; my $text = $needed; my $tillcheck = $d->{'till-check'}; my $mounts = $d->{'mounts-left'}; my $nextt = daysandhours($tillcheck<0?-$tillcheck:$tillcheck); $nextt = $tillcheck < 0 ? $nextt . " ago" : "in ".$nextt; my $nextm = $mounts < 0 ? -$mounts . " mounts ago" : " in " . $mounts ." mounts"; my $reasons = join(" and ", @{$d->{reasons}}); $nexttw->set_text($nextt); $nextmw->set_text($nextm); fontify($l,$needed); fontify($nextmw,$needed && $reasons =~ /mount/ ? $needed:""); fontify($nexttw,$needed && $reasons =~ /date/ ? $needed:""); #$sg[4]->add_widget($mntpntw); if($needed) { if($needed eq "soon") { $text = "due soon"; } } else { #$text="due in ".daysandhours($d->{'till-check'})." or in ".(($d->{'mounts-left'})) . " mounts"; $text="not for a while"; } fontify($textw,$needed); $textw->set_text($text); activate_buttons(); } sub show_gui() { use Gtk2 -init; my $win=new Gtk2::Window -toplevel; my $title=$0; $title=~ s%.*/%%g; $win->set_title($title); # Glib::Timeout->add(700, sub { # activate_buttons(); # 1; # }); $win->signal_connect('delete_event', sub { Gtk2->main_quit(); }); my $vb=new Gtk2::VBox(); my @sg= (new Gtk2::SizeGroup('both'), new Gtk2::SizeGroup('both'), new Gtk2::SizeGroup('both'), new Gtk2::SizeGroup('both'), new Gtk2::SizeGroup('both') ); for my $n (sort keys %infos) { my $info= my $d=$infos{$n}; my $hb=new Gtk2::HBox(0,0); my $l=new Gtk2::Label($n); my $hb2=new Gtk2::HBox(0,0); my $umntbtn=new Gtk2::Button("umount"); my $mntbtn=new Gtk2::Button("mount"); my $checkbtn=new Gtk2::Button("check"); my $textw = new Gtk2::Label(); #my $mntpntw = new Gtk2::Label(dmnt($d->{disk})); my $nexttw = new Gtk2::Label(); my $nextmw = new Gtk2::Label(); $sg[0]->add_widget($l); $sg[1]->add_widget($nexttw); $sg[2]->add_widget($nextmw); $sg[3]->add_widget($textw); #$hb->pack_start($mntpntw,0,0,10); $hb->pack_start($l,0,0,0); for my $w ($textw,$nexttw,$nextmw) { $hb->pack_start($w,1,1,10); } $hb->pack_start($hb2,0,0,0); $hb2->pack_start($umntbtn,0,0,0); $hb2->pack_start($checkbtn,0,0,0); $hb2->pack_start($mntbtn,0,0,0); $vb->pack_start($hb,0,0,0); $buttons{$n}=[$l,$textw,$nexttw,$nextmw,$umntbtn,$checkbtn,$mntbtn]; setup_buttons($n); update_gui($n); } activate_buttons(); my $quitbtn=new Gtk2::Button("Dismiss"); $quitbtn->signal_connect("clicked",sub { Gtk2->main_quit; }); $vb->pack_start($quitbtn,0,0,0); $win->add($vb); $win->show_all; Glib::Timeout->add(1000, sub { my @devs=(keys %checking); for my $k (@devs) { my $arr=$checking{$k}; my ($log,$cpid,$progress)=@{$arr}; if(-f $log.".done" && ! -d "/proc/$cpid") { print STDERR "fsck has completed\n"; delete $checking{$k}; update_gui($k); } else { my $progress=$arr->[2]=read_fsck_progress($log); update_gui($k,"progress",$progress); } } 1; }); Gtk2->main; } eprint "discover labels"; discover_labels(); eprint ::Dumper(\%label2dev); eprint "read fstab"; read_fstab(); eprint ::Dumper(\%alldisks); eprint "determine supported"; determine_supported(); eprint ::Dumper(\%disks); eprint "Supported disks ",@supported; eprint "checking if any are close to needing a check"; check_fsck_needed(); eprint ::Dumper(\%infos); my $show = scalar keys %checkneeded > 0; for my $arg (@ARGV) { if($arg eq "-show") { $show=$true; } } if($show) { eprint "show gui"; show_gui(); }