# Friday, May 21, 2010

If you want to do this, you won’t really need much of an explanation as to why I’m posting this. As to the rest of you, never mind; stick with your BigEndian Unicode (hint: powershell console and other console applications prefer ASCII) :)

First up, create yourself a Windows ISE Profile script that will be loaded by default when ISE starts (and/or when you open a new “Tab”)

# run this one-liner from within ISE through the interactive window (command pane):
if (-not (test-path $profile)) { md -force (split-path $profile); "" > $profile; psedit $profile }

Now, put this one-liner (well, it could fit on one line) in your $profile:

# watch for changes to the Files collection of the current Tab
register-objectevent $psise.CurrentPowerShellTab.Files collectionchanged -action {
    # iterate ISEFile objects
    $event.sender | % {
         # set private field which holds default encoding to ASCII
         $_.gettype().getfield("encoding","nonpublic,instance").setvalue($_, [text.encoding]::ascii)
    }
}
Every time the tabs "files" collection changes, it will set the default save encoding to ASCII for all files in that tab. As the profile is loaded in each tab, all files in all tabs will default to ASCII when saving. No more "save as" annoyances; just hit save and ASCII will be used for encoding. "Save as" will still let you save as unicode if you wish.

Have fun!

posted on Friday, May 21, 2010 5:58:46 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [2] Trackback
# Sunday, November 01, 2009

One of the things that never quite fit well with me with the remoting feature in PowerShell 2.0 is that while you can “telnet” to remote systems with Enter-PSSession and import commands and do all sorts of cool tricks, there is no way to send or retrieve files from the console. It seems like such a waste that you configure WinRM up with SSL and Kerberos and get this nice encrypted channel up, but if you want to transfer files you have to revert to file shares, remote desktop or classic ftp.

Back in the “good ole’ days” of BBSs and FidoNet, people used to use simple protocols like XModem (advancing to YModem and then ZModem) or Kermit that worked by streaming character data directly to your terminal. It wasn’t fast or particularly efficient, but it got the job done. I thought I’d take a crack at building something similar for PowerShell, and this first 0.5 release is the fruits of this weekend’s tinkering. At the moment it only can “pull” a file to the local system from a remote session, but the next release will allow “pushing” a file from a local system to a remote session.

image

The reason I named it after XModem is because it works in a similar way: files are not “pulled” from the remote server, but instead are “pushed.” X[YZ]Modem file transfer was initiated by the remote end. I’ll not spoil the fun by explaining how it works, but I think you’ll enjoy pulling it apart. It’s in a module format and is implemented in pure script.

Requirements

  • PowerShell 2.0 installed on both client and server with remoting enabled to the location of the file being transferred.

E.g. if you want to grab a file using Get-RemoteFile from a remote server, you must be able to create a valid PSSession to it with the New-PSSession cmdlet. When Send-LocalFile is implemented, you’ll need remoting enabled in the other direction too.

  • The PMODEM module must be findable on both the client and server via import-module and must be the same version.

Here’s the Get-RemoteFile function help (via –?):

NAME
    Get-RemoteFile

SYNOPSIS
    Retrieves a file from a remote computer via a supplied PSession.


SYNTAX
    Get-RemoteFile [-Session] <pssession> [-RemoteFile] <string> [[-LocalPath] <string>] [[-PacketSize] <int32>]
	[-PassThru] [-AsJob] [<commonparameters>]


DESCRIPTION
    Retrieves a remote file from a server via a supplied PSSession. All communication
    is performed out-of-band, yet inside the secure WinRM channel.

    No other ports, file shares or any other special configuration is needed. However,
    the PMODEM module must be on the remote computer and findable in its $ENV:PSModulePath;
    the protocol versions must also match on both ends. You will be warned of any
    misconfiguration(s).

    When not running asynchronously, progress records are generated.


RELATED LINKS

REMARKS
    To see the examples, type: "get-help Get-RemoteFile -examples".
    For more information, type: "get-help Get-RemoteFile -detailed".
    For technical information, type: "get-help Get-RemoteFile -full".

Things coming in later releases: wildcards/multiple file support, compression and integration via proxy functions (copy-item/move-item/remove-item/rename-item etc).

Download PModem

Grab pmodem-0.5.zip and unzip it into a folder in your $ENV:PSModulePath on the client and server computers you want to use PMODEM on.

Have fun!

posted on Sunday, November 01, 2009 8:57:10 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1] Trackback
# Friday, October 09, 2009

Asynchronous callback delegates are not a friend to PowerShell. They are serviced by the .NET threadpool which means that if they point to script blocks, there will be no Runspace available to execute them. Runspaces are thread-local resources in the PowerShell threadpool. The .NET threadpool, operating independently, is not too interested in coordinating callbacks with PowerShell. So what do we do?

There is one feature of PowerShell 2.0 that is capable of running scriptblocks in a pseudo-asynchronous manner: Eventing. Any events bound to with Register-ObjectEvent, EngineEvent or WmIEvent can have associated scriptblocks that will get executed when the associated event is raised. So, if we can somehow convert an asynchronous callback to a .NET event then we can run scriptblocks in response to Async .NET Callbacks. I’ve written a simple function called New-ScriptBlockCallback that helps us do exactly that:

#requires -version 2.0

function New-ScriptBlockCallback {
    param(
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]$Callback
    )
<#
    .SYNOPSIS
        Allows running ScriptBlocks via .NET async callbacks.

    .DESCRIPTION
        Allows running ScriptBlocks via .NET async callbacks. Internally this is
        managed by converting .NET async callbacks into .NET events. This enables
        PowerShell 2.0 to run ScriptBlocks indirectly through Register-ObjectEvent.         

    .PARAMETER Callback
        Specify a ScriptBlock to be executed in response to the callback.
        Because the ScriptBlock is executed by the eventing subsystem, it only has
        access to global scope. Any additional arguments to this function will be
        passed as event MessageData.
        
    .EXAMPLE
        You wish to run a scriptblock in reponse to a callback. Here is the .NET
        method signature:
        
        void Bar(AsyncCallback handler, int blah)
        
        ps> [foo]::bar((New-ScriptBlockCallback { ... }), 42)                        

    .OUTPUTS
        A System.AsyncCallback delegate.
#>
    # is this type already defined?    
    if (-not ("CallbackEventBridge" -as [type])) {
        Add-Type @"
            using System;
            
            public sealed class CallbackEventBridge
            {
                public event AsyncCallback CallbackComplete = delegate { };

                private CallbackEventBridge() {}

                private void CallbackInternal(IAsyncResult result)
                {
                    CallbackComplete(result);
                }

                public AsyncCallback Callback
                {
                    get { return new AsyncCallback(CallbackInternal); }
                }

                public static CallbackEventBridge Create()
                {
                    return new CallbackEventBridge();
                }
            }
"@
    }
    $bridge = [callbackeventbridge]::create()
    Register-ObjectEvent -input $bridge -EventName callbackcomplete -action $callback -messagedata $args > $null
    $bridge.callback
}
posted on Friday, October 09, 2009 2:09:30 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1] Trackback
# Tuesday, August 18, 2009

One thing that has been lamented frequently about PowerShell is that it is very difficult to log, if not impossible, to log all of the various types of streams it has to a single source. The legacy Windows shell CMD, and Unix shells like Bash, Ksh etc only deal with three streams: stdin, stdout and stderr for Input, Ouput and Error respectively. PowerShell has many more: Input, Output, Verbose, Warning, Debug, Progress and Error. Finally, the APIs in v2.0 offer enough hooks to unify the logging but you got to work a bit to make it come together. Well, to be honest, you got me doing the work. The rest is easy ;)

import-module .\scriptlogger.psm1 -force

$logger = New-ScriptLogger

# override error handler
$logger.ErrorHandler = {
    param($record)
    
    $record.tostring() >> scriptlog.txt
}

# override verbose handler
$logger.VerboseHandler = {
    param($record)
    
    $record.message >> scriptlog.txt
}

# run scriptblock with logging
$logger.Invoke(
    {
        $verbosepreference='continue';
        $erroractionpreference = 'continue';
        $debugpreference = 'continue';
        write-verbose "verbose";
        write-error "an error";
        write-warning "a warning"
        Write-debug "debug string"
        "this is output"
        1,2,3
    })
And here is the module; save it as ScriptLogger.psm1. By default, all logging goes to an attached debugger, like sysinternals DbgView. You can override any of the handlers like above and do what you want. Each handler receives one argument: a ErrorRecord, WarningRecord, VerboseRecord, DebugRecord or ProgressRecord. All of these Types are native powershell types and are documented on MSDN.
<#
    Name     : Universal Script Logging Module (ScriptLogger.psm1)
    Version  : 0.1
    Author   : Oisin Grehan (MVP)
    Site     : http://www.nivot.org/
#>
function New-ScriptLogger {
    New-Module -AsCustomObject -ScriptBlock {
        
        $script:ps              = [powershell]::Create()
        $script:ar              = $null
        $script:module          = $ExecutionContext.SessionState.Module
        
        [scriptblock]
        $script:ErrorHandler    = {
            param(
                [Management.Automation.ErrorRecord]
                $record
            )
            [diagnostics.debug]::writeline(
                "Error: " + $record.tostring());
        }
        [scriptblock]
        $script:WarningHandler  = {
            param(
                [Management.Automation.WarningRecord]
                $record
            )        
            [diagnostics.debug]::writeline(
                "Warning: " + $record.message);
        }
        [scriptblock]
        $script:VerboseHandler  = {
            param(
                [Management.Automation.VerboseRecord]
                $record
            )
            [diagnostics.debug]::writeline(
                "Verbose: " + $record.message);
        }
        [scriptblock]
        $script:DebugHandler    = {
            param(
                [Management.Automation.DebugRecord]
                $record
            )
            [diagnostics.debug]::writeline(
                "Debug: " + $record.message);
        }
        [scriptblock]
        $script:ProgressHandler = {
            param(
                [Management.Automation.ProgressRecord]
                $record
            )        
            [diagnostics.debug]::writeline(
                "Progress: " + $record);
        }
        
        $script:Handlers   = @{
            Error = Register-ObjectEvent $ps.Streams.Error DataAdded -Action {
                & $event.MessageData {& $ErrorHandler @args} $event.sender[$eventargs.index]
            } -MessageData $module #-SupportEvent
            
            Warning = Register-ObjectEvent $ps.Streams.Warning DataAdded -Action {
                & $event.MessageData {& $WarningHandler @args} $event.sender[$eventargs.index]
            } -MessageData $module #-SupportEvent
            
            Verbose = Register-ObjectEvent $ps.Streams.Verbose DataAdded -Action {
                & $event.MessageData {& $VerboseHandler @args} $event.sender[$eventargs.index]
            } -MessageData $module #-SupportEvent
            
            Debug = Register-ObjectEvent $ps.Streams.Debug DataAdded -Action {
                & $event.MessageData {& $DebugHandler @args} $event.sender[$eventargs.index]
            } -MessageData $module #-SupportEvent
            
            Progress = Register-ObjectEvent $ps.Streams.Progress DataAdded -Action {
                & $event.MessageData {& $ProgressHandler @args} $event.sender[$eventargs.index]
            } -MessageData $module #-SupportEvent
        }
        
        function Invoke {
            param(
                [validatenotnullorempty()]
                [scriptblock]$script
            )
            
            try {
            
                write-host -foreground green "Running"
                
                $ps.commands.clear()
                $command = new-object management.automation.runspaces.command $script, $true            
                $ps.commands.addcommand($command) > $null                
                $ps.invoke() # returns output                
            
            } catch {
            
                # oops-ee!
                write-host -foreground red "Unhandled terminating error: $_"
                $record = new-object management.automation.errorrecord $(
                    new-object exception $_.tostring()), "TerminatingError", "NotSpecified", $null
                & $ErrorHandler $record
            
            } finally {
            
                write-host -foreground green "Complete"
            
            }
        }
                
        Export-ModuleMember -Function Invoke -Variable ErrorHandler, WarningHandler, VerboseHandler, DebugHandler, ProgressHandler
    }
}

Have fun!

posted on Tuesday, August 18, 2009 10:58:24 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [1] Trackback
# Friday, August 14, 2009

update: 2009-09-27 - removed usage of closures - turns out i hadn't tested that modification properly. oops.

Here’s a quick one – in order to sway excessive BASH jealously, I knocked this up to persist the last 100 history items between PowerShell sessions. It hooks the special engine (as opposed to object) event “poweshell.exiting” and runs a script to save history to an XML file using the universally useful Export-CliXML cmdlet. Another trick in there is to use a closure to capture the value of the $historyPath variable. I need to do this because powershell event handlers use their own runspace (and will lose the values of the variables in the current runspace). I also could have passed the value via the –MessageData parameter and done it that way, but I figured I’m already in v2 territory so lets use that feature ;-)

# save last 100 history items on exit
$historyPath = Join-Path (split-path $profile) history.clixml

# hook powershell's exiting event & hide the registration with -supportevent.
Register-EngineEvent -SourceIdentifier powershell.exiting -SupportEvent -Action {
    Get-History -Count 100 | Export-Clixml (Join-Path (split-path $profile) history.clixml) }

# load previous history, if it exists
if ((Test-Path $historyPath)) {
    Import-Clixml $historyPath | ? {$count++;$true} | Add-History
    Write-Host -Fore Green "`nLoaded $count history item(s).`n"
}

Dump this into your profile and have fun!

posted on Friday, August 14, 2009 9:28:58 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback
# Saturday, July 18, 2009

updated 2009/7/20: added link to PSEventing for v1.0 event handling
updated 2009/7/24: added link to Get-Delegate script for v1.0 callbacks

There have been some nice improvements made in the latest build of PowerShell with respect to interop with the "callback” pattern in .NET. What exactly are callbacks anyway? It’s exactly what it sounds like, pretty much. In .NET there are sometimes APIs you need to call that expect you to hand them delegates (pointers to methods) which that API will call some time in the future, usually based on certain conditions being fulfilled. If that sounds a bit like .NET events, you’d be right. An event is a much gussied-up callback - it’s just a way of permitting multiple methods to be invoked in response to some condition.

Synchronous Callbacks in PowerShell v1.0

In .NET there are two types of callbacks: Asynchronous (non-blocking) and synchronous (blocking). In PowerShell v1.0, the only callbacks that were catered for were for EventHandler Delegates. This is the method signature that most of the Windows Forms controls expect to call back to in response to button clicks etc. You may have seen code similar to:

$button = new-object system.windows.forms.button
$button.add_Click( { $form.Close() } )

This works because in PowerShell v1.0, there is specialized support for this kind of callback to methods with the EventHandler signature, that is to say, methods with parameters of (object sender, EventArgs e). PowerShell is able to run the ScriptBlock in response to the button being clicked and will even pass the two arguments to the scriptblock for you. When the form is shown from a PowerShell script, there is a single thread that is running the message loop for the application. It is this same thread that handles running the script. In the PowerShell engine, there is a pool of threads created at startup, and each of them has its own “Runspace” for running scripts. Because it is one of the PowerShell threads that is running the application, that same thread is able to run the ScriptBlocks in its Runspace when called upon to do so. Although it is in a slightly roundabout way, this is an example of a synchronous callback. This single application thread is effectively waiting (blocked) for the callback to occur in response to a button click (in reality, it’s doing other things while waiting, but it’s still waiting.)

Some rather creative folks, namely one of the primary developers of PowerShell, wrote a delegate/scriptblock binder in pure script some years ago which you can use to pass script to a .NET api to be called back to in a synchronous manner.  See: Creating arbitrary delegates to ScriptBlocks.

If you want to work with .NET events in version 1.0 of PowerShell, you’ll need an add-on, like my PSEventing Snap-In.

Synchronous Callbacks in PowerShell v2.0

In the latest and greatest version of PowerShell, v2.0 RC (which comes with the public Windows 7 RC), synchronous callbacks got a whole lot easier. PowerShell is now able to deal with pretty much ANY delegate signature, automatically. Lets test this by using the .NET 3.5 System.Func<T, TResult> delegate. This is a generic delegate which lets us pass a method to an API expecting a callback to a method which has one parameters of type T1, and will return type T2. Because it’s generic, we get to pick which parameters. Lets demo creating a ScriptBlock that will be passed a DateTime and returns a String:

add-type –assembly system.core # load .net 3.5
$callback = [system.func[datetime, string]] { param($date); "the date is $date" }
$callback.Invoke( [datetime]::now )
# returns
”the date is 2009/07/14 21:50:35”

We can even take advantage of PowerShell’s super-versatile parameter binder by passing it in a string and having it get coerced to DateTime with ne’er a Parse in sight!

$callback.Invoke(“1/1/2009”)
”the date is 2009/01/01 00:00:00”

So, what’s the point of all this? Why go to all that trouble? Why not just write a function? The point is that functions cannot be called by .NET directly. So when would you need a callback like this in a PowerShell script?

Calling Web Services with Invalid, Untrusted or Expired SSL Certificates

The title says it all – sometimes you need to do this. Usually it’s because you’re working with self-signed, expired or otherwise invalid certificates on a QA or Development system. The New-WebServiceProxy Cmdlet in v2.0 is great for calling Web Services, but it doesn’t have a switch to ignore invalid certificates. If you were writing .NET code in C# or VB.NET, the way go about this is to pass a callback method to an API that expects a RemoteCertificateValidationCallback Delegate. This delegate is designed to point to a method that is passed a handful of arguments describing the attempted connection, and is expected to return a boolean; that is to say, true or false. True means “sure, the connection looks fine – go for it.” A value of False being returned tells .NET to stop the connection before it happens.

The amount of .NET code needed to do this is not a ton, but it’s still a fair handful of lines. Check out this example here: http://blog.jameshiggs.com/2008/05/01/c-how-to-accept-an-invalid-ssl-certificate-programmatically/

Now let’s see how much PowerShell script is needed for this same task:

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }

Bwahahahah! Eat that, Mr. C# coder! Again, PowerShell’s binder comes into play here and automatically casts our ScriptBlock to a RemoteCertificateValidateCallback delegate (it’s not really a cast – there is no conversion – it’s a sizeable chunk of code.) From this point on, any attempts to use the New-WebServiceProxy Cmdlet with dodgy SSL certificates will succeed without so much as a warning. In fact, pretty much any other classes, like WebRequest will behave the same way. It’s important to note that this only affects the current AppDomain, that is to say, the current PowerShell process. Other processes on the system will continue to stick their nose up at dodgy SSL certs. Quit PowerShell and restart it -- or set that ServerCertificateValidationCallback property to $null -- and all is right in the world again. This works as long as you use one of PowerShell’s threads to do the work of connecting; i.e. don’t use an asynchronous request. This ensures there is a Runspace available to execute this ScriptBlock.

Converting Synchronous Callbacks into Events

This is really quite straightforward. Taking the previous example, we would generate an event inside the scriptblock using New-Event, and then bind one or more event handlers using the Register-EngineEvent Cmdlet:

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
		new-event -SourceIdentifier SslCheck -MessageData $args > $null
		$true
	}
# dump out arguments to cert validate callback
Register-EngineEvent -SourceIdentifier SslCheck -Action { write-output $args }
Next time, we'll get into some meatier stuff. Have fun!

In Part Two: Asynchronous Callbacks in PowerShell v1.0 and v2.0

posted on Saturday, July 18, 2009 4:22:00 AM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback
# Friday, August 15, 2008

The new eventing infrastructure in PowerShell 2.0 is pretty delicious. You couldn’t do the following in 1.0 without a 3rd party snap-in (like my PSEventing snapin), but now it’s all there at the touch of your fingers. Well, it demands a bit of a sniff around WMI too, but hey, it works well.  With this module, anytime you add or remove a removable device like an external harddrive or USB key, or map a new network drive in explorer, PowerShell will now automatically add or remove a corresponding PSDrive for you.

  1. # AutoMount.psm1 v1.0  
  2. # Oisin "x0n" Grehan (MVP)  
  3.  
  4. $query = new-object System.Management.WqlEventQuery  
  5. $query.EventClassName = "__InstanceOperationEvent" 
  6.  
  7. # default to every 2 seconds  
  8. $query.WithinInterval = new-object System.TimeSpan 0,0,2  
  9.  
  10. # this WMI is only available with Windows 2003 and Vista (not XP it appears).  
  11. # this could be rewritten to use different WMI queries to support 2000/NT/XP also.  
  12. $query.QueryString = "Select * from Win32_VolumeChangeEvent" 
  13.  
  14. # attach a watcher  
  15. $watcher = new-object System.Management.ManagementEventWatcher $query 
  16.  
  17. # here we use -SupportEvent instead of -SourceIdentifier  
  18. # this prevents this event from being generally visible  
  19. # also note the use of the call operator to invoke a   
  20. # function in the scope of the module since this action  
  21. # occurs outside of module scope.  
  22. Register-ObjectEvent $watcher -EventName "EventArrived" `  
  23.     -SupportEvent "WMI.VolumeChange" -Action {  
  24.         & (get-module automount) VolumeChangeCallback @args 
  25.     }  
  26.  
  27. # New PSEvents:  
  28. #  
  29. #     PowerShell.DeviceConfigurationChanged  
  30. #     PowerShell.DeviceArrived  
  31. #     PowerShell.DeviceRemoved  
  32. #     PowerShell.DeviceDocking  
  33.  
  34. # win32_volumechangeevent event types  
  35. $eventTypes = @{  
  36.     1 = "ConfigurationChanged";  
  37.     2 = "Arrived";  
  38.     3 = "Removed";  
  39.     4 = "Docking";  
  40. }  
  41.  
  42. # private module level callback function  
  43. function VolumeChangeCallback ($sender, $eventargs) {  
  44.     trap { write-warning $_ }  
  45.  
  46.     $driveName = $eventArgs.NewEvent.DriveName.TrimEnd(":")  
  47.     $eventType = [int]$eventArgs.NewEvent.EventType # was uint16  
  48.  
  49.     $forwardedEvent = "Device$($eventTypes[$eventType])" 
  50.       
  51.     # forward a new simpler event specific to device event type  
  52.     [void]( New-PSEvent "PowerShell.$forwardedEvent" -Sender $driveName `  
  53.         -EventArguments $eventargs )  
  54. }  
  55.  
  56. # hook up our psdrive mount / unmount events  
  57. # and start the WMI watcher  
  58. function Enable-AutoMount {  
  59.  
  60.     Register-PSEvent -SourceIdentifier "PowerShell.DeviceArrived" `  
  61.         -Action {              
  62.             new-psdrive -name $args[0] -psprovider `  
  63.                 filesystem -root "$args[0]:";  
  64.          }  
  65.  
  66.     Register-PSEvent -SourceIdentifier "PowerShell.DeviceRemoved" `  
  67.         -Action {  
  68.             remove-psdrive -name $args[0] -ea 0; # may not exist  
  69.         }  
  70.       
  71.     $watcher.Start()  
  72. }  
  73.  
  74. # tear down our psdrive mount / unmount events  
  75. # and stop the WMI watcher  
  76. function Disable-AutoMount {  
  77.  
  78.     Unregister-PSEvent -SourceIdentifier "PowerShell.DeviceArrived" 
  79.     Unregister-PSEvent -SourceIdentifier "PowerShell.DeviceRemoved" 
  80.       
  81.     $watcher.Stop()  
  82. }  
  83.  
  84. # export functions to control automount  
  85. Export-ModuleMember Enable-AutoMount, Disable-AutoMount  
  86.  
  87. # start watching and (un)mounting  
  88. Enable-AutoMount 

This only works PowerShell v2.0 CTP2, and you’ll need to save it as AutoMount.psm1 in a directory under your documents folder like so (vista example):

%userprofile%\documents\windowspowershell\packages\automount\automount.psm1

You can then load it with the command:

ps> add-module automount

I have this in my profile.  You can temporarily disable automount with the function Disable-AutoMount and reenable it at anytime with Enable-AutoMount. The module also exposes four new events for you to consume yourself. You could, for example, hook your own script to run anytime a device is added and/or removed. This is what I do myself in the module. I hook a WMI event once then forward 1 of 4 possible new events depending on the type of WMI event that was raised.

NOTE: this particular flavour of WMI query only works in Vista and Windows 2003 it appears. I’m looking into getting it working with 2000/XP also.

Have fun!

posted on Friday, August 15, 2008 10:14:47 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback