Print full exception in PowerShell try/catch block using “format-list”

I recently started PowerShell programming and I like it! It feels much like bash or another scripting language system engineers tend to love (like Ruby/Python/Perl). While writing some backup automation (Windows 2012) I ran into the following:

          Add-Type -Path 'C:\Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.Smo.dll'
          Add-Type -Path 'C:\Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.SmoExtended.dll'

          $server = "POWERSHELL_TEST"
          $path = "C:\Backup\DB\"

          # Connect to the specified instance
          $srv = new-object ('Microsoft.SqlServer.Management.Smo.Server') ($server)

          foreach ($Database in $srv.Databases | where {$_.IsSystemObject -eq $False})
          {
              $dbname = $Database.Name
              $tStamp = Get-Date -format yyyy_MM_dd_HHmmss
              $bckfile = $path.TrimEnd('\') + "\" + $dbname + "_backup_" + $tStamp + ".bak"
              echo "backing up $dbname to $bckfile"
              $bk = New-Object ("Microsoft.SqlServer.Management.Smo.Backup")
              $bk.Action = [Microsoft.SqlServer.Management.Smo.BackupActionType]::Database
              $bk.BackupSetName = $dbname + "_backup_" + $tStamp
              $bk.Database = $dbname
              $bk.CompressionOption = 1
              $bk.MediaDescription = "Disk"
              $bk.Devices.AddDevice($bckfile, "File")
              try
              {
                $bk.SqlBackup($srv)
              }
              catch [Exception]
              {
                echo $_.Exception.GetType().FullName, $_.Exception.Message
              }
          }

The error it gave me was:

Microsoft.SqlServer.Management.Smo.FailedOperationException
Backup failed for Server 'POWERSHELL_TEST'.

I changed the line:

echo $_.Exception.GetType().FullName, $_.Exception.Message

to:

echo $_.Exception|format-list -force

And this dramatically improved the output:

SmoExceptionType : FailedOperationException
Operation        : Backup
FailedObject     : [POWERSHELL_TEST]
Message          : Backup failed for Server 'POWERSHELL_TEST'.
HelpLink         : http://go.microsoft.com/fwlink?ProdName=Microsoft+SQL+Server&ProdVer=11.0.3000.0+((SQL11_PCU_Main).1210
                   19-1325+)&EvtSrc=Microsoft.SqlServer.Management.Smo.ExceptionTemplates.FailedOperationExceptionText&Evt
                   ID=Backup+Server&LinkId=20476
Data             : {HelpLink.ProdName, HelpLink.BaseHelpUrl, HelpLink.LinkId, HelpLink.ProdVer...}
InnerException   : Microsoft.SqlServer.Management.Common.ExecutionFailureException: An exception occurred while executing
                   a Transact-SQL statement or batch. ---> System.Data.SqlClient.SqlException: BACKUP DATABASE WITH COMPRE
                   SSION is not supported on Web Edition (64-bit).
                   BACKUP DATABASE is terminating abnormally.
                      at Microsoft.SqlServer.Management.Common.ConnectionManager.ExecuteTSql(ExecuteTSqlAction action, Obj
                   ect execObject, DataSet fillDataSet, Boolean catchException)
                      at Microsoft.SqlServer.Management.Common.ServerConnection.ExecuteNonQuery(String sqlCommand, Executi
                   onTypes executionType)
                      --- End of inner exception stack trace ---
                      at Microsoft.SqlServer.Management.Common.ServerConnection.ExecuteNonQuery(String sqlCommand, Executi
                   onTypes executionType)
                      at Microsoft.SqlServer.Management.Common.ServerConnection.ExecuteNonQuery(StringCollection sqlComman
                   ds, ExecutionTypes executionType)
                      at Microsoft.SqlServer.Management.Smo.ExecutionManager.ExecuteNonQuery(StringCollection queries)
                      at Microsoft.SqlServer.Management.Smo.BackupRestoreBase.ExecuteSql(Server server, StringCollection q
                   ueries)
                      at Microsoft.SqlServer.Management.Smo.Backup.SqlBackup(Server srv)
TargetSite       : Void SqlBackup(Microsoft.SqlServer.Management.Smo.Server)
StackTrace       :    at Microsoft.SqlServer.Management.Smo.Backup.SqlBackup(Server srv)
                      at CallSite.Target(Closure , CallSite , Object , Object )
Source           : Microsoft.SqlServer.SmoExtended
HResult          : -2146233088

Actually the reason was right there:

BACKUP DATABASE WITH COMPRESSION is not supported on Web Edition (64-bit).

So next time you run into an exception and need to print it, use “format-list”!

Share

A smart alternative to PHP’s “var_dump” function

The “var_debug” function is a smart alternative to PHP’s “var_dump” function, which limits the output and prints the file path and line of the invocation.

The problem with “var_dump”

It may output so much information that it locks up your browser (and your web server thread). This is very annoying and not very useful either, since there is no way you are ever going to read all the information. And when you sprinkle your code with “var_debug” function calls during a debug session, you sometimes lose track of which call produced the output you are currently looking at.

File path and line of invocation

The “var_debug” function outputs the file path and line number of the invocation. So even when your “var_dump” calls all say the same you can still see what happened. To accomplish this, it uses the PHP “debug_backtrace” function. It sets the “DEBUG_BACKTRACE_IGNORE_ARGS” option to reduce memory usage. From the produced backtrace, it gets the first element that holds a file path and line number and it adds that to the start of the output.

Usage example

The following:

<?php
var_debug(array('a'=>new stdClass(),'b'=>array(1,2,3,4,5),new mysqli()));

Outputs:

/home/maurits/public_html/debug.php:2
array(3)
{
  [a] => stdClass#1
  {
  }
  [b] => array(5)
  {
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 5
  }
  [0] => mysqli#2
  {
    [affected_rows] => null
    [client_info] => null
    [client_version] => null
    [connect_errno] => null
    [connect_error] => null
    [errno] => null
    [error] => null
    [field_count] => null
    [host_info] => null
    [info] => null
    [insert_id] => null
    [server_info] => null
    [server_version] => null
    [stat] => null
    [sqlstate] => null
    [protocol_version] => null
    [thread_id] => null
    [warning_count] => null
  }
}

Limit the output

The “var_debug” function limits the output with the following default rules:

  1. Only the first 100 characters of each string are logged (full length is shown)
  2. Only the first 25 elements of an array are logged (full length is shown)
  3. Only the first 10 levels of nested objects/arrays are logged

These three limits can be set using three optional parameters in the above order. Hence, calling “var_debug($variable)” is equal to calling “var_debug($variable,100,25,10)”.

Use the source

            <?php
            function var_debug($variable,$strlen=100,$width=25,$depth=10,$i=0,&$objects = array())
            {
              $search = array("\0", "\a", "\b", "\f", "\n", "\r", "\t", "\v");
              $replace = array('\0', '\a', '\b', '\f', '\n', '\r', '\t', '\v');

              $string = '';

              switch(gettype($variable)) {
                case 'boolean':      $string.= $variable?'true':'false'; break;
                case 'integer':      $string.= $variable;                break;
                case 'double':       $string.= $variable;                break;
                case 'resource':     $string.= '[resource]';             break;
                case 'NULL':         $string.= "null";                   break;
                case 'unknown type': $string.= '???';                    break;
                case 'string':
                  $len = strlen($variable);
                  $variable = str_replace($search,$replace,substr($variable,0,$strlen),$count);
                  $variable = substr($variable,0,$strlen);
                  if ($len<$strlen) $string.= '"'.$variable.'"';
                  else $string.= 'string('.$len.'): "'.$variable.'"...';
                  break;
                case 'array':
                  $len = count($variable);
                  if ($i==$depth) $string.= 'array('.$len.') {...}';
                  elseif(!$len) $string.= 'array(0) {}';
                  else {
                    $keys = array_keys($variable);
                    $spaces = str_repeat(' ',$i*2);
                    $string.= "array($len)\n".$spaces.'{';
                    $count=0;
                    foreach($keys as $key) {
                      if ($count==$width) {
                        $string.= "\n".$spaces."  ...";
                        break;
                      }
                      $string.= "\n".$spaces."  [$key] => ";
                      $string.= var_debug($variable[$key],$strlen,$width,$depth,$i+1,$objects);
                      $count++;
                    }
                    $string.="\n".$spaces.'}';
                  }
                  break;
                case 'object':
                  $id = array_search($variable,$objects,true);
                  if ($id!==false)
                    $string.=get_class($variable).'#'.($id+1).' {...}';
                  else if($i==$depth)
                    $string.=get_class($variable).' {...}';
                  else {
                    $id = array_push($objects,$variable);
                    $array = (array)$variable;
                    $spaces = str_repeat(' ',$i*2);
                    $string.= get_class($variable)."#$id\n".$spaces.'{';
                    $properties = array_keys($array);
                    foreach($properties as $property) {
                      $name = str_replace("\0",':',trim($property));
                      $string.= "\n".$spaces."  [$name] => ";
                      $string.= var_debug($array[$property],$strlen,$width,$depth,$i+1,$objects);
                    }
                    $string.= "\n".$spaces.'}';
                  }
                  break;
              }

              if ($i>0) return $string;

              $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
              do $caller = array_shift($backtrace); while ($caller && !isset($caller['file']));
              if ($caller) $string = $caller['file'].':'.$caller['line']."\n".$string;

              echo $string;
            }

Modifications

If you would like the function to echo HTML-printable output, you can replace the last line that says “echo $string;” with the following:

echo nl2br(str_replace(' ','&nbsp;',htmlentities($string)));

Replace “echo $string;” with “return $string;” if you want to use the output for logging purposes in some other function.

Alternatives

This post has turned out to be rather popular on Reddit. Here are some alternatives that might also deserve your attention:

All of them seem like good alternatives. The thing that possibly makes “var_debug” stand out is its simplicity: It is one function that you can easily copy-paste. Use the comments and let me know what works (best) for you.

Share

Subversion revision information in the Symfony debug toolbar

At LeaseWeb we are very serious about software development and that is why we hire software testers. These people do risk analysis, think of new test cases, execute them and organize them in test suites using TestLink. We also use PHP and are great fans of the Symfony MVC framework. Being serious about Agile software development means that we do TDD using unit tests and functional test. For this we use the Symfony build-in unit and functional tests. We also use Selenium for creating browser-driven test suites. The advantages of Selenium over the functional tests in Symfony are cross browser testing and JavaScript testing.

We have this product called “SSC” (this is the hosting control panel for the customers) and we test it thoroughly and often. For this product we have a DTAP environment to do testing and acceptance testing. Within the tests of this product the testers encountered a challenge: How do the testers know what revision of our application they are testing and what branch they are testing? This is especially a problem with acceptance testing where interactions between various systems are tested. Because the testers do not have command line access on the Linux machines that run the acceptance environment they cannot simply issue the “svn info” command like developers can.

To solve this problem we wrote a Symfony plugin called “lwTestingInformationPlugin”. It shows the “svn info” command output in a popup that is accessible from the Symfony debug toolbar. Even though it was extremely easy to write, it could be very useful. We made it available for download, so if you think it might be useful for your project as well, please try it and send us your feedback!

Click here to download the plugin (lwTestingInformationPlugin.tar)

TestLink = http://www.teamst.org/
Symfony = http://www.symfony-project.org
Selenium = http://seleniumhq.org/
Subversion = http://subversion.apache.org

Share