Modifying perl file check_file_age.pl

Hi,

I am new to perl. I need your help in doing one task.

I have a script which checks age of a file, means for how long this file is modified from current time in the system

This file runs perfect when u run on command using parameter

I tried

perl check_file_age.pl -f filename.ext -h 171.11.11.11

This gives result to me

Now I wanted this script to check folder and if there is file more than given time (for example 5 min) then it should produce error.txt file

The folder path,file type, host should be picked up from csv file for example

/home/username/folderToCheck,5,172.11.12.13
/home/username/folderToCheck/anotherfolder,2,172.11.12.15

first parameter is folderpath, second parameter is time in minutes, and the third parameter is IP

Can anyone please help me in updating this script


This script I have to update in Nagios

Here are some links for  help


http://search.cpan.org/dist/Nagios-Plugin/

http://redhatlinuxworld.blogspot.com/2009/01/nagios-script-to-check-file-age-on.html

Many Thanks again
#! /usr/bin/perl -w
# $Id: check_file_age.pl 1750 2007-07-07 11:54:29Z psychotrahe $
 
# check_file_age.pl Copyright (C) 2003 Steven Grimm <koreth-nagios@midwinter.com>
#
# Checks a file's size and modification time to make sure it's not empty
# and that it's sufficiently recent.
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# you should have received a copy of the GNU General Public License
# along with this program (or with Nagios);  if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA
 
use strict;
use English;
use Getopt::Long;
use File::stat;
use vars qw($PROGNAME);
use lib "/usr/local/nagios/libexec" ;
use utils qw (%ERRORS &print_revision &support);
 
sub print_help ();
sub print_usage ();
 
my ($opt_c, $opt_f, $opt_w, $opt_C, $opt_W, $opt_h, $opt_V);
my ($result, $message, $age, $size, $st);
 
$PROGNAME="check_file_age";
 
$opt_w = 240;
$opt_c = 600;
$opt_W = 0;
$opt_C = 0;
$opt_f = "";
 
Getopt::Long::Configure('bundling');
GetOptions(
        "V"   => \$opt_V, "version"     => \$opt_V,
        "h"   => \$opt_h, "help"        => \$opt_h,
        "f=s" => \$opt_f, "file"        => \$opt_f,
        "w=f" => \$opt_w, "warning-age=f" => \$opt_w,
        "W=f" => \$opt_W, "warning-size=f" => \$opt_W,
        "c=f" => \$opt_c, "critical-age=f" => \$opt_c,
        "C=f" => \$opt_C, "critical-size=f" => \$opt_C);
 
if ($opt_V) {
        print_revision($PROGNAME, '$Revision: 1750 $');
        exit $ERRORS{'OK'};
}
 
if ($opt_h) {
        print_help();
        exit $ERRORS{'OK'};
}
 
$opt_f = shift unless ($opt_f);
 
if (! $opt_f) {
        print "FILE_AGE UNKNOWN: No file specified\n";
        exit $ERRORS{'UNKNOWN'};
}
 
# Check that file exists (can be directory or link)
unless (-e $opt_f) {
        print "FILE_AGE CRITICAL: File not found - $opt_f\n";
        exit $ERRORS{'CRITICAL'};
}
 
$st = File::stat::stat($opt_f);
$age = time - $st->mtime;
$size = $st->size;
 
 
$result = 'OK';
 
if (($opt_c and $age > $opt_c) or ($opt_C and $size < $opt_C)) {
        $result = 'CRITICAL';
}
elsif (($opt_w and $age > $opt_w) or ($opt_W and $size < $opt_W)) {
        $result = 'WARNING';
}
 
print "FILE_AGE $result: $opt_f is $age seconds old and $size bytes\n";
exit $ERRORS{$result};
 
sub print_usage () {
        print "Usage:\n";
        print "  $PROGNAME [-w <secs>] [-c <secs>] [-W <size>] [-C <size>] -f <file>\n";
        print "  $PROGNAME [-h | --help]\n";
        print "  $PROGNAME [-V | --version]\n";
}
 
sub print_help () {
        print_revision($PROGNAME, '$Revision: 1750 $');
        print "Copyright (c) 2003 Steven Grimm\n\n";
        print_usage();
        print "\n";
        print "  <secs>  File must be no more than this many seconds old (default: warn 240 secs, crit 600)\n";
        print "  <size>  File must be at least this many bytes long (default: crit 0 bytes)\n";
        print "\n";
        support();
}

Open in new window

tia_kamakshiAsked:
Who is Participating?
 
Adam314Commented:
I'm not sure why you are getting this... It works as expected when I run it.  Try this version, which adds some debugging, post the output here.

...
while(<$csv_fh>) {
    print "*DEBUG: Got line '$_'\n";
    s/[\r\n]+//;
    print "  *DEBUG: Removed line endings '$_'\n";
    my ($dir, $mins) = split /,/;
    print "  *DEBUG: dir='$dir', mins='$mins'\n";
    
    next unless defined($mins);
    print "  *DEBUG: Processing\n";
    my $dirh;
...

Open in new window

0
 
ozoCommented:
perl -F, -ane 'system("perl check_file_age.pl -f $F[0] -w ".($F[1]*60)." -h $F[2]")' csvfile
0
 
tia_kamakshiAuthor Commented:
Many Many Thanks

Where should I write this code in the perl file. I want system to check this activity regulary

Write now it is printing on the screen the file age

I want to put it into the error file at some location

Can you please help me in
0
Free Tool: Port Scanner

Check which ports are open to the outside world. Helps make sure that your firewall rules are working as intended.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

 
tia_kamakshiAuthor Commented:
Can you please help me in understanding what code you have wriiten

or what does this line means

many Thanks again
0
 
Adam314Commented:
The code you've posted doesn't use the host at all.  And with the -h option, it will just display a help message and exit.

Can you clarify what you want the program to do?
0
 
tia_kamakshiAuthor Commented:
Many Thanks for your reply

Ok means -h is help not host. Thanks for making me understand

What does line means which ozo has posted

perl -F, -ane 'system("perl check_file_age.pl -f $F[0] -w ".($F[1]*60)." -h $F[2]")' csvfile

Where should I write this

Do I need to write in a cron tab???

Nagios is a system which monitors the servers and raise alarm if something goes wrong. And also gives stats as well

It will check if some error file is generated

Here I wanted to check some folder where file should not exists more than 5 min or 2 min

Please ask for any other clarification if required
0
 
Adam314Commented:
The command ozo gave is something you'd type at the command line.  I'm not familiar with Nagios, so I don't know how you would tie it in to this.

Do you want this script to run from within Nagios, or can it run from anywhere?
0
 
tia_kamakshiAuthor Commented:

Hi,

Many Thanks for your response

I also dont know much, But I assume that this works only in nagios

Anyway

I have created csv file where text is writtem

/home/defgmcc/archive/,5,11111
/home/defgmcc/in/,5,11111


When I executed given command, I get the message below


abcd-pbhi5:/usr/local/nagios/libexec# perl -F, -ane 'system("perl check_file_age -f $F[0] -w ".($F[1]*60)." -h $F[2]")' checkFilesInFolders.csv
check_file_age v1750 (nagios-plugins 1.4.12)
The nagios plugins come with ABSOLUTELY NO WARRANTY. You may redistribute
copies of the plugins under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.
Copyright (c) 2003 Steven Grimm

Usage:
  check_file_age [-w <secs>] [-c <secs>] [-W <size>] [-C <size>] -f <file>
  check_file_age [-h | --help]
  check_file_age [-V | --version]

  <secs>  File must be no more than this many seconds old (default: warn 240 secs, crit 600)
  <size>  File must be at least this many bytes long (default: crit 0 bytes)

Send email to nagios-users@lists.sourceforge.net if you have questions
regarding use of this software. To submit patches or suggest improvements,
send email to nagiosplug-devel@lists.sourceforge.net.
Please include version information with all correspondence (when possible,
use output from the --version option of the plugin itself).
abcd-pbhi5:/usr/local/nagios/libexec#


Can you please help me in fixing this. That what csv file I should create and what command I should pass

Also, If -h is help not host. Then can you please help me in removing this parameter

I am highly thankful to you and EE

Kind Regards
0
 
tia_kamakshiAuthor Commented:
Is it hard to modify this script.

Nagio automatiacally executes these files

So, I just wanted that this file to read the directory and check if any file should not reside there for more than passed minutes in csv file

and generate error file that file age is greater than specified time in csv file

Please help me in fixing this

Many Thanks
0
 
Adam314Commented:
So if you have this in your csv file:
    /home/defgmcc/archive/,5,11111
What are those 3 parameters?  The first appears to be the directory to check.  The second I'm guessing is the amount of time (in minutes).  What is the purpose of the third parameter?  What should be done with it?
0
 
tia_kamakshiAuthor Commented:
Thanks for your reply.

I am Sorry 3rd one is invalid. We can remove 3rd parameter in csv file

Thanks again
0
 
Adam314Commented:

#!/usr/bin/perl -w
use strict;
use lib "/usr/local/nagios/libexec" ;
 
#NOTE: update these as necessary
my $csv_filename='/path/to/file.csv';
my $error_filename='/path/to/error_log.txt';
 
open(my $error_fh, ">>", $error_filename) or die "Could not open error log: $!\n";
open(my $csv_fh, "<", $csv_filename) or die "Could not open csv: $!\n";
while(<$csv_fh>) {
  chomp;
  my ($dir, $mins) = split /,/;
  opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;
  while(my $fn=readdir($dirh)) {
    next unless ((-M "$dir/$fn")/24/60)>$mins;
    print $error_fh "$dir/$fn\n";
  }
}
close($csv_fh);
close($error_fh);
 
 
print "OK\n";  #for Nagios... It seems to need this

Open in new window

0
 
tia_kamakshiAuthor Commented:
Many Thank for your script

What you are doing with your script if there is some error, warning etc

Are you creating file or giving result as Error, Ok, Warning

As If I see the first nagios perl script which I have given to you

There it is sending responses as

exit $ERRORS{'OK'};
exit $ERRORS{'UNKNOWN'};
exit $ERRORS{'CRITICAL'};



Doing something like

print "FILE_AGE $result: $opt_f is $age seconds old and $size bytes\n";
exit $ERRORS{$result};


Yes, Nagios executes the perl file from some central location after regular interval of time

So, we cannot add this perl script in cron tabs

Many Thanks for your co-opearation

Kind Regards
0
 
Adam314Commented:
If there are any errors, it will display an error message to STDERR, and exit with the error value from the function that caused the error.
If there are no errors, it'll display the message "OK", and exit with a value of 0.
0
 
tia_kamakshiAuthor Commented:
Hi,

Many thanks for your help

To me it doesn't look working fine

I executed the perl file and In response it says OK

In my csv file, I have given a path to archive folder where we have many old files

I was expecting some errors saying that files are older than 5 min in this folder

SEE RESPONSE


abcdmcc@pbms-pbhi5:~$ perl defg-check-file-age.pl
OK
abcdmcc@defg-pbhi5:~$


SEE MY CSV FILE CONTENTS


/home/shouse/in,5
/home/shouse/out,5
/home/abcdmcc/archive,5

Please help me out  

Also can you please tell me more about STDERROR

Many Thanks again
0
 
Adam314Commented:
Did you update lines 6 and 7 to point to your csv file and error file?
What is the output from:
   ls -l /home/shouse/in
   ls -l /home/shouse/out
0
 
tia_kamakshiAuthor Commented:
my $csv_filename='/home/abcdmcc/checkage.csv';
my $error_filename='/home/abcdmcc/error_log.txt';

Line 6 and 7,  These are the path where I have csv file located and error_log.txt located

Folders /home/shouse/in and /home/shouse/out

are blank But folder /home/abcdmcc/archive has more than 1000 files in it


If I do ls -l /home/abcdmcc/archive

I get long list sample is below:


-rw-r--r-- 1 abcdmcc    abcdmcc      947 2009-02-18 16:34 REGISTRATION-0000005352.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      926 2009-02-18 16:34 REGISTRATION-0000005353.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      933 2009-02-18 16:34 REGISTRATION-0000005354.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      938 2009-02-18 16:34 REGISTRATION-0000005355.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      938 2009-02-18 16:34 REGISTRATION-0000005356.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      906 2009-02-18 16:34 REGISTRATION-0000005357.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      929 2009-02-18 16:34 REGISTRATION-0000005358.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      935 2009-02-18 16:34 REGISTRATION-0000005359.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      938 2009-02-18 16:35 REGISTRATION-0000005360.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      942 2009-02-18 16:35 REGISTRATION-0000005361.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      916 2009-02-18 16:35 REGISTRATION-0000005362.xml
-rw-r--r-- 1 abcdmcc    abcdmcc      919 2009-02-18 16:35 REGISTRATION-0000005363.xml


As my csv file says, there should not be any file for more than 5 minutes

home/abcdmcc/archive,5


I have just created blank error_log.txt in the folder

If I do ls on abcdmcc home directory here is my files details

abcdmcc@defg-pbhi5:~$ ls -l
total 484
drwxrwx--- 5 abcdmcc abcdmcc 372736 2009-03-11 15:01 archive
-rwxrwxrwx 1 root    root        70 2009-03-11 09:41 checkage.csv
-rwxrwxrwx 1 root    root         1 2009-03-10 16:38 error_log.txt
drwxr-xr-x 2 abcdmcc abcdmcc   4096 2009-03-10 18:43 RegistrationStatusUpdate
drwxr-xr-x 2 abcdmcc abcdmcc   4096 2009-03-03 15:54 SalesContactRegistration
drwxr-xr-x 5 abcdmcc abcdmcc   4096 2008-07-23 10:41 test
-rw-rw-rw- 1 abcdmcc abcdmcc      9 2008-07-25 13:39 test.eot
drwxrwx--- 3 abcdmcc abcdmcc   4096 2008-07-21 08:56 XML_Import
abcdmcc@defg-pbhi5:~$


Also, can you please help me in telling that when we will be getting response in error_log.txt and what?

Many Thanks again
0
 
Adam314Commented:
Had a bug in the script... divided when I should have multiplied.  Here is an update.

>>when we will be getting response in error_log.txt and what
The file will be appended whenever the script is ran, which would depend on your Nagios config.
It will contain a list of old files, one per line.

...
while(<$csv_fh>) {
	next unless /\S/;
	chomp;
	my ($dir, $mins) = split /,/;
	opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;
	while(my $fn=readdir($dirh)) {
		my $age_minutes = (-M "$dir/$fn")*24*60;
		next if $age_minutes<$mins;
		print $error_fh "$dir/$fn\n";
	}
}
...

Open in new window

0
 
tia_kamakshiAuthor Commented:
Hi,

Many Thanks for your script

Looks it is looking sub -directories as well.

Where I do not wanted to look there sub-directories

Also, It is printing OK even if there are errors in any folder.

Is it fine??

Can we print ok and error for each directory specified in the csv file

Really thanks allot

Kind Regards
0
 
Adam314Commented:
If there are subdirectories of the specified directory, it will look at the subdirectory, but it will not look into the subdirectory.

What do you want the output to look like?
0
 
tia_kamakshiAuthor Commented:
Many Many Thanks Adam314 for all your help doing...

Yes, my folder has subdirectories like errored folder, backup folder.

I cannot move these folders, so i donot wanted that my error list should produce folders as an error.

I should look only files in the directory and It would be great if in some way i can specify in the csv or xml file to look the specific files.

I mean look for .xml, .eot, .dij files only for each specified folder...

and I do not wanted to look any files in the sub-directories

Can we pass error file path for each directory in the csv file itself.

I think I am going longer with my requirements...

Please help me with whatever you can give, atleast code should only see files not any sub directories...

Many Thanks again
0
 
tia_kamakshiAuthor Commented:
Forgot to mention, also I want error or ok result to produce by each directory...

Many Thanks again
0
 
Adam314Commented:

...
while(<$csv_fh>) {
    next unless /\S/;
    chomp;
    my ($dir, $mins) = split /,/;
    opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;
    my $ok=1;
    while(my $fn=readdir($dirh)) {
        my $pn="$dir/$fn";
        next if -d $pn;
        my $age_minutes = (-M $pn)*24*60;
        next if $age_minutes<$mins;
        print $error_fh "$pn\n";
        $ok=0;
    }
    closedir($dirh);
    print("$dir: " . ($ok ? "OK\n" : "ERROR") . "\n");
}
...

Open in new window

0
 
tia_kamakshiAuthor Commented:
Many Thanks

Now works great to me
Looks it is not working for first directory details in the csv file

I mean if in csv file
it is written
/home/shouse/in,5
/home/shouse/out,5
/home/abcdmcc/archive,5

Then we get result for
/home/shouse/out,5
/home/abcdmcc/archive,5

Then in csv file i changed the contents to

it is written
/home/shouse/out,5
/home/shouse/in,5
/home/abcdmcc/archive,5

Then it has not displayed the result for folder
/home/shouse/out,5

Then I changed the contents again of the csv file to

Folderpath,minutes
/home/shouse/out,5
/home/shouse/in,5
/home/abcdmcc/archive,5

Still I get the result for folders
/home/shouse/in,5
/home/abcdmcc/archive,5

Also, In error file can we print the date and time for the file and its age in minutes

Really Thanks
0
 
Adam314Commented:

...
while(<$csv_fh>) {
    next unless /\S/;
    chomp;
    my ($dir, $mins) = split /,/;
    my $dirh;
    unless(opendir($dirh, $dir)) {
        warn "Could not opendir '$dir': $!\n";
        next;
    }
    my $ok=1;
    while(my $fn=readdir($dirh)) {
        my $pn="$dir/$fn";
        next if -d $pn;
        my $age_minutes = (-M $pn)*24*60;
        next if $age_minutes<$mins;
        printf $error_fh "%5.2f: %s\n", $age_minutes, $pn;
        $ok=0;
    }
    closedir($dirh);
    print("$dir: " . ($ok ? "OK\n" : "ERROR") . "\n");
}
...

Open in new window

0
 
tia_kamakshiAuthor Commented:

Many Thanks again

Still it is not looking to the first folder defined in the csv

As in my csv it is defined
/home/shouse/in,5
/home/shouse/out,5

And I get the result for only

/home/shouse/out

See results below


abcdmcc@defg-pbhi5:~$ cat checkage.csv
/home/shouse/in,5
/home/shouse/out,5abcdmcc@defg-pbhi5:~$ perl defg-check-file-age.pl
/home/shouse/out: OK

OK
abcdmcc@defg-pbhi5:~$ cat checkage.csv
/home/shouse/in,5
/home/shouse/out,5abcdmcc@defg-pbhi5:~$


Please help me in fixing this issue
0
 
Adam314Commented:
Attach yoru checkage.csv file here, and I'll take a look.
0
 
tia_kamakshiAuthor Commented:
Hi,

I have renamed file to .txt as I cannot upload csv file

Please find it attached

Kind regards
checkage.txt
0
 
Adam314Commented:
It looks like your file has windows line endings, and the last line has no ending.  Add a line ending to the last line, and make this change to the script:

...
while(<$csv_fh>) {
    #REMOVE next AND chomp LINES
    s/[\r\n]+//;   #THIS LINE IS NEW
    my ($dir, $mins) = split /,/;
    next unless defined($mins);    #THIS LINE IS NEW
    my $dirh;
...

Open in new window

0
 
tia_kamakshiAuthor Commented:
Many Thanks for your help

Still I am not able to read first directory from my csv file

I am happy to add first line as header. I added header still it is not reading first directory mentioned in the csv file

Please fix this...

Please see my output of my perl file, contents of csv file and perl file

abcdmcc@defg-pbhi5:~$ perl defg-check-file-age.pl
/home/shouse/in: OK

/home/shouse/out: OK

OK
abcdmcc@defg-pbhi5:~$ cat checkage.csv
/home/abcdmcc/archive,5
/home/shouse/in,5
/home/shouse/out,5
abcdmcc@defg-pbhi5:~$ cat defg-check-file-age.pl
#!/usr/bin/perl -w
use strict;
use lib "/usr/local/nagios/libexec";

my $csv_filename='/home/abcdmcc/checkage.csv';
my $error_filename='/home/abcdmcc/error_log.txt';

open(my $error_fh, ">>", $error_filename) or die "Could not open error log: $!\n";
open(my $csv_fh, "<", $csv_filename) or die "Could not open csv: $!\n";
while(<$csv_fh>) {
  chomp;
  my ($dir, $mins) = split /,/;
  opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;
          while(<$csv_fh>) {
                s/[\r\n]+//;
                my ($dir, $mins) = split /,/;

                next unless defined($mins);
                my $dirh;

                unless(opendir($dirh, $dir)) {
                warn "Could not opendir '$dir': $!\n";
                next;
              }
              my $ok=1;
              while(my $fn=readdir($dirh)) {
                  my $pn="$dir/$fn";
                  next if -d $pn;
                  my $age_minutes = (-M $pn)*24*60;
                  next if $age_minutes<$mins;
                  printf $error_fh "%5.2f: %s\n", $age_minutes, $pn;
                  $ok=0;
              }
              closedir($dirh);
              print("$dir: " . ($ok ? "OK\n" : "ERROR") . "\n");
}
}
close($csv_fh);
close($error_fh);


print "OK\n";abcdmcc@defg-pbhi5:~$
0
 
tia_kamakshiAuthor Commented:
Hi,

Thanks for your updated script

Looks this is not reading first directory path

See all results below

abcdmcc@defg-pbhi5:~$ perl defg-check-file-age.pl
*DEBUG: Got line '/home/shouse/in,5
'
  *DEBUG: Removed line endings '/home/shouse/in,5'
  *DEBUG: dir='/home/shouse/in', mins='5'
  *DEBUG: Processing
/home/shouse/in: OK

*DEBUG: Got line '/home/shouse/out,5
'
  *DEBUG: Removed line endings '/home/shouse/out,5'
  *DEBUG: dir='/home/shouse/out', mins='5'
  *DEBUG: Processing
/home/shouse/out: OK

OK
abcdmcc@defg-pbhi5:~$ cat checkage.csv
Directory path, minutes
/home/abcdmcc/archive,5
/home/shouse/in,5
/home/shouse/out,5
abcdmcc@defg-pbhi5:~$
0
 
Adam314Commented:
Are you sure the checkage.csv that you cat is the /home/abcdmcc/checkage.csv?

Try this
cat /home/abcdmcc/checkage.csv

Open in new window

0
 
tia_kamakshiAuthor Commented:
yes, It is the same

when, I have commented the line
# opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;

What this line is doing.

Looks that if there is problem in opening directory then it is returning the while loop after giving warning.

But is this is the case then this should be written in error file

So looks we need to fix the above line

please suggest
0
 
tia_kamakshiAuthor Commented:
In above comment

...
when, I have commented the line
# opendir(my $dirh, $dir) or warn "Could not opendir '$dir': $!\n",next;

Then code works fine

...

What this line is doing
...
0
 
tia_kamakshiAuthor Commented:
Many Many Thanks Adam314 for your great great help.
0
 
Adam314Commented:
This line opens the directory (so you can get the list of files in it).  If it can't open the directory, it will display a warning message, then go to the next directory.

Did you get it working?  It looks like you accepted an answer.
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.