I'm trying to redirect the stdout and stderr of a long process. It's an Exe that could take 40 minutes until it finished processing.
The issue is that if I run the EXE from command-line (cmd), stdout and stderr are displayed on console in a certain order,
and that's the order I'd like it to be redirected from my application, but it does not work. The order is changed when I use the following function, and can't find out what the reason is. I'd appreciate an advice.
This is the code I use:
Public numOutputLines As Integer = 0
Public sortOutput As StringBuilder = Nothing
Public Function ProcessTask3New(ByVal ExeName As String, ByVal arguments As String, ByRef stdout As String, ByRef stderr As String, ByRef ExitCode As Integer, _
Optional ByVal Filename As String = "", Optional ByVal IsDeleteTempLogFiles As Boolean = False) As Boolean
' This fucntion executes cmd commands and arguments,
' Function returns standard output and startdard error. stderr contains data if error was generated
Try
ProcessTask3New = True
Dim p As Process
Dim psi As ProcessStartInfo
Dim currentTime As System.DateTime
currentTime = System.DateTime.Now
If Filename <> "" Then Filename = Replace(Filename & ".", "\", "")
Dim tmpStdoutFilename As String = System.IO.Path.GetTempPath & "stdout." & Filename & currentTime.Ticks.ToString()
Dim tmpStderrFilename As String = System.IO.Path.GetTempPath & "stderr." & Filename & currentTime.Ticks.ToString()
netOutput = New StringBuilder
p = New Process
psi = p.StartInfo
psi.Arguments = psi.Arguments.Replace("/C " & Chr(34), "/C " & Chr(34) & Chr(34))
psi.FileName = ExeName
psi.UseShellExecute = False
psi.WindowStyle = ProcessWindowStyle.Minimized
' Redirect the standard output of the sort command.
' Read the stream asynchronously using an event handler.
psi.RedirectStandardOutput = True
psi.RedirectStandardError = True
psi.CreateNoWindow = True
sortOutput = New StringBuilder()
' Set our event handler to asynchronously read the sort output.
AddHandler p.OutputDataReceived, _
AddressOf SortOutputHandler
AddHandler p.ErrorDataReceived, AddressOf SortOutputHandler
If IsDebug Then Write2Log("ProcessTask3New: " + psi.FileName.ToString + " " + psi.Arguments.ToString)
Try
Write2Log(My.Computer.FileSystem.CurrentDirectory)
p.Start()
Catch w As System.ComponentModel.Win32Exception
Write2Log("ProcessTask3New: " & w.Message)
Write2Log("ProcessTask3New: " & w.ErrorCode.ToString())
Write2Log("ProcessTask3New: " & w.NativeErrorCode.ToString())
Write2Log("ProcessTask3New: " & w.StackTrace)
Write2Log("ProcessTask3New: " & w.Source)
Dim e As New Exception()
e = w.GetBaseException()
Write2Log("ProcessTask3New: " & e.Message)
End Try
' Start the asynchronous read of the sort output stream.
p.BeginOutputReadLine()
p.BeginErrorReadLine()
p.WaitForExit()
ExitCode = p.ExitCode
p.Close()
netOutput = Nothing
Catch ex As Exception
Write2Log("error at ProcessTask3New function: " & ex.ToString & " : " + ex.StackTrace)
End Try
End Function
Private Sub SortOutputHandler(ByVal sendingProcess As Object, _
ByVal outLine As DataReceivedEventArgs)
' Collect the sort command output.
If Not String.IsNullOrEmpty(outLine.Data) Then
numOutputLines += 1
Add the text to the collected output.
sortOutput.Append(Environment.NewLine + "[" _
+ numOutputLines.ToString() + "] - " _
+ outLine.Data)
End If
End Sub
Now the output.
This is how it looks when I run it from cmd window (this is GOOD):
Processing key file: vob_db.k01(1), total of 291 nodes
Processing delete chain: 1 node on delete chain. Processing nodes:
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%+++++++++70%+++++++++80%+++++++++90%+++++++++100 %
Processing key file: vob_db.k02(2), total of 1246 nodes
Processing delete chain: 2 nodes on delete chain. Processing nodes:
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%+++++++++70%+++++++++80%+++++++++90%+++++++++100 %
Processing key file: vob_db.k03(5), total of 1 node
Processing delete chain: 0 nodes on delete chain. Processing nodes:
100%
Processing key file: vob_db.k04(6), total of 277 nodes
Processing delete chain: 7 nodes on delete chain. Processing nodes:
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%+++++++++70%+++++++++80%+++++++++90%+++++++++100 %
(the lines like this one come from stderr.)
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%++
This is how it looks when I run it from my application (this is BAD):
ProcessTask3New: cmd.exe /C ""C:\Program Files
(x86).." -a -k -R -r1
-p29000 vob_db" E:\backup2\db db_VISTA Version 3.20 Database Consistency Check Utility Copyright (C) 1985-1990 Raima
Corporation, All Rights Reserved
------------------------------------------------------------------------ Processing key file: vob_db.k01(1), total of 291 nodes
------------------------------------------------------------------------ Processing key file: vob_db.k02(2), total of 1246 nodes
------------------------------------------------------------------------ Processing key file: vob_db.k03(5), total of 1 node
------------------------------------------------------------------------ Processing key file: vob_db.k04(6), total of 277 nodes
------------------------------------------------------------------------ Processing data file: vob_db.d01(0), total of 7107 records
------------------------------------------------------------------------ Processing data file: vob_db.d02(3), total of 20516 records
------------------------------------------------------------------------ Processing data file: vob_db.d03(4), total of 1 record
------------------------------------------------------------------------ Processing data file: vob_db.d04(7), total of 0 records
------------------------------------------------------------------------ Processing data file: vob_db.d05(8), total of 39938 records Processing
delete chain: 1 node on delete chain. 0 errors were encountered in 0
records/nodes
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%+++++++++70%+++++++++80%+++++++++90%+++++++++100% Processing delete chain: 2 nodes on delete chain. Processing nodes:
+++++++++10%+++++++++20%+++++++++30%+++++++++40%+++++++++50%+++++++++60%+++++++++70%+++++++++80%+++++++++90%+++++++++100% Processing delete chain: 0 nodes on delete chain. Processing nodes:
100%
Please advice. THANK YOU!
You have the option to "mix" the stdout and stderr, redirecting the stderr into stdout
cmd /c "commandToRun 2>&1"
Here we are asking cmd to execute some command and redirect the stream 2 (stderr) sending its output to stream 1 (stdout).
BUT, obviously you will not retrieve any data on stderr.
The reason behind different orders is that when in console, a single output stream is indeed assigned to both "standard output" and "error output", and when called by your application, there are two streams that are not synchronized, thus your SortOutputHandler events are called in any order (if there were extremely long pauses between writes to the outputs or flushes it could get ordered correctly by chance).
The only solution you have to get this well sorted is to ensure that there is a single stream. The problem is that I am not aware of a solution allowing this using the ProcessStartInfo class.
One possibility would be to start the process in "pause" mode, then redirect forcefully its standard error handle to the output handle, then let it run (as cmd.exe does)
Related
I am working with ffmpeg via C#'s Process class.
I have a script that runs ffmpeg to generate thumbnails from video. Initially it was called manually from command line - .\ffmpeg.exe -i .\input.mp4 -ss 00:00:01.000 -vframes:v 1 output.png, it starts ffmpeg instance, outputs some warnings/errors during the execution:
[image2 # 000001e51095ec80] The specified filename 'output.png' does not contain an image sequence pattern or a pattern is invalid.
[image2 # 000001e51095ec80] Use a pattern such as %03d for an image sequence or use the -update option (with -frames:v 1 if needed) to write a single image.
frame= 1 fps=0.0 q=-0.0 Lsize=N/A time=00:00:00.00 bitrate=N/A speed= 0x
video:73kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
but anyway exits the process and correctly generates thumbnail image at output.png.
I want to execute it from C#.
Let's see the code:
var ffmpegProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _config.FfmpegExecutablePath,
Arguments = CreateArgumentsForFfmpegProcessToRun(videoTempFilePath, thumbnailTempFilePath),
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
},
EnableRaisingEvents = true
};
ffmpegProcess.Start();
await ffmpegProcess.WaitForExitAsync();
Method CreateArgumentsForFfmpegProcessToRun returns exactly the same arguments as in the script above -i .\input.mp4 -ss 00:00:01.000 -vframes:v 1 output.png.
However, when I run this code, it stucks/blocked at line ffmpegProcess.WaitForExitAsync() infinitely and no output written to output path.
If I omit WaitForExitAsync call and just go to the next line, then it doesn't stuck and writes the output as expected and finish the process with -1 exit code.
I am trying to figure out why block/stuck happens and what is the best way to resolve this situation? As far I know, WaitForExitAsync should return as process ends, no matter how process ends - 0 or another exit code, am I right?
Update #1:
Community advised to search if somewhere up the stack I am blocking my code. I wrote xunit-test and it still stucks.
[Theory]
[InlineData("assets/input.mp4", "assets/ffmpeg.exe")]
public async Task CreateThumbnailFromVideo(string videoFilePath, string ffmpegExePath)
{
var config = new VideoThumbnailServiceConfig
{
FfmpegExecutablePath = ffmpegExePath,
ThumbnailImageExtension = ".png"
};
var sut = new VideoThumbnailService(config);
using var fileStream = File.OpenRead(videoFilePath);
await sut.CreateThumbnailFromVideo(fileStream);
}
Inside sut.CreateThumbnailFromVideom I call process start method and awaits WaitForExitAsync().
I came across some issue I cannot explain to myself.
I get a list of all running processes and their parent process using the following code:
foreach (var process in allProcesses)
{
try
{
int parent_process_id = 0;
//Get ProcessID of Parent Process
ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = " + process.Id + "");
foreach (ManagementObject reader in searcher.Get())
{
parent_process_id = Convert.ToInt32(reader["ParentProcessId"]);
searcher.Dispose();
}
//Get Main Process Information
string process_path = process.MainModule.FileName;
string process_name_full = process.ProcessName;
//Get Parent Process Information
string parent_process_path = null;
string parent_process_name_full = null;
var parent_process = Process.GetProcessById(parent_process_id);
parent_process_name_full = parent_process.ProcessName;
parent_process_path = parent_process.MainModule.FileName;
process_log_information = "Periodic Scan detected and submitted the following processes:" + Environment.NewLine + "Main ProcessID: " + process.Id + Environment.NewLine + "Main Process Name: " + process.ProcessName + Environment.NewLine + "Main Process Path: " + process.MainModule.FileName + Environment.NewLine + "Parent ProcessID: " + parent_process_id + Environment.NewLine + "Parent Process Name: " + parent_process_name_full + Environment.NewLine + "Parent Process Path: " + parent_process_path;
Application_Control.Application_Control.Check_Process(process.Id, process_name_full, process_path, parent_process_id, parent_process_name_full, parent_process_path, true, process_log_information);
}
catch (Exception ex)
{
Logging.Logging.log_microsoft_defender_antivirus_cfa("Microsoft_Defender_AntiVirus.Periodic_Check.Check_Processes_Tick", "Process Query.", ex.Message);
}
}
Now if I look on the collected file paths behind the processes, it looks like this for example:
Main ProcessID: 4792
Main Process Name: ctfmon
Main Process Path: C:\Windows\system32\ctfmon.exe
Parent ProcessID: 4664
Parent Process Name: svchost
Parent Process Path: C:\Windows\System32\svchost.exe
Why is our main process path first letter of "system32" not in upper case?
Opposite example where the main process is in upper case, but the parent isn't:
Main ProcessID: 6344
Main Process Name: SystemSettingsBroker
Main Process Path: C:\Windows\System32\SystemSettingsBroker.exe
Parent ProcessID: 500
Parent Process Name: svchost
Parent Process Path: C:\Windows\system32\svchost.exe
As far as I know windows is not case sensitive, as long as you don't set a flag in the registry to be. But in any case I would like to get the correct case sensitive path.
I hope you understand my issue description and got some tips on this case.
Thank you!
Edit: Process Hacker displays the paths correctly.
Edit2: I need to correct myself. I wrote a small test application to collect process information:
var processes = Process.GetProcesses();
foreach (var proc in processes)
{
try
{
MessageBox.Show(proc.MainModule.FileName + " " + proc.Id.ToString());
}
catch
{
}
}
The first process that woke up my interest was svchost.exe because the path shown was: C:\Windows\system32\svchost.exe
Since the first letter of system32 was again lower case I decided to look on the process using Process Hacker. Following information is being displayed:
The image file names path is in upper case. If I look on the process modules and it's main module, it is also in upper case.
So why does Process.GetProcesses(); -> process.MainModule.Filename; gets the path with a lower case?
Another time the svchost process (different pid) is being shown correctly by process.MainModule.Filename; as:
C:\Windows\System32\svchost.exe
On Process Hacker I see the following information:
What differs is the "Command line" information. So I think while describing my issue I actually found the reason? I need to sleep now. Maybe you have a hint to this. Otherwise I will further investigate this issue tomorrow I think. Thank you
Edit: Before going to sleep I was to curious and looked on the information provided by the WMI. The executable path differs regarding the lower and upper case situation. For me I found out, the issue is not on my side. But maybe someone has a idea why that happens? I guess it is the path provided using a command line execution. Maybe I will check that theory tomorrow.
I'm using the redmon to redirect de output postcript my c# .exe to process.
The print configuration is the same as if I'd do to configure the ghostscript, but instead of the ghost path I informed my .exe path.
To convert the postscript in PDF I'm using the ghostscript, as the code bellow
Stream content = Console.OpenStandardInput();
using BinaryReader standardInputReader = new BinaryReader(content);
using (FileStream standardInputFile = new FileStream(psFile, FileMode.Create, FileAccess.ReadWrite))
{
standardInputReader.BaseStream.CopyTo(standardInputFile);
}
standardInputReader.Close();
string ghostScriptPath = #"C:\gs\gs9.52\bin\gswin64.exe";
String ars = #"-Ic:\gs\gs925\lib -dNOPAUSE -dBATCH -q -dSAFER -sDEVICE=pdfwrite -sOutputFile=teste.pdf psFile.ps";
Process proc = new Process();
proc.StartInfo.FileName = ghostScriptPath;
proc.StartInfo.Arguments = ars;
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proc.Start();
proc.WaitForExit();
But the ghostscript always throws the error %%[ Error: undefined; OffendingCommand: Redmon ]%%.
If I use the ghostscript instead of my program everything works fine, but if I "intercept", the error always happens. I logged the output on the two cases, but the log is the same, a piece bellow.
The full log can be seen here
%%BeginResource: file Pscript_WinNT_ErrorHandler 5.0 0
/currentpacking where{pop/oldpack currentpacking def/setpacking where{pop false
setpacking}if}if/$brkpage 64 dict def $brkpage begin/prnt{dup type/stringtype
ne{=string cvs}if dup length 6 mul/tx exch def/ty 10 def currentpoint/toy exch
def/tox exch def 1 setgray newpath tox toy 2 sub moveto 0 ty rlineto tx 0
rlineto 0 ty neg rlineto closepath fill tox toy moveto 0 setgray show}bind def
/nl{currentpoint exch pop lmargin exch moveto 0 -10 rmoveto}def/=={/cp 0 def
typeprint nl}def/typeprint{dup type exec}readonly def/lmargin 72 def/rmargin 72
def/tprint{dup length cp add rmargin gt{nl/cp 0 def}if dup length cp add/cp
exch def prnt}readonly def/cvsprint{=string cvs tprint( )tprint}readonly def
/integertype{cvsprint}readonly def/realtype{cvsprint}readonly def/booleantype
{cvsprint}readonly def/operatortype{(--)tprint =string cvs tprint(-- )tprint}
readonly def/marktype{pop(-mark- )tprint}readonly def/dicttype{pop
(-dictionary- )tprint}readonly def/nulltype{pop(-null- )tprint}readonly def
/filetype{pop(-filestream- )tprint}readonly def/savetype{pop(-savelevel- )
tprint}readonly def/fonttype{pop(-fontid- )tprint}readonly def/nametype{dup
xcheck not{(/)tprint}if cvsprint}readonly def/stringtype{dup rcheck{(\()tprint
tprint(\))tprint}{pop(-string- )tprint}ifelse}readonly def/arraytype{dup rcheck
{dup xcheck{({)tprint{typeprint}forall(})tprint}{([)tprint{typeprint}forall(])
tprint}ifelse}{pop(-array- )tprint}ifelse}readonly def/packedarraytype{dup
rcheck{dup xcheck{({)tprint{typeprint}forall(})tprint}{([)tprint{typeprint}
forall(])tprint}ifelse}{pop(-packedarray- )tprint}ifelse}readonly def/courier
/Courier findfont 10 scalefont def end errordict/handleerror{systemdict begin
$error begin $brkpage begin newerror{/newerror false store vmstatus pop pop 0
ne{grestoreall}if errorname(VMerror)ne{showpage}if initgraphics courier setfont
lmargin 720 moveto errorname(VMerror)eq{userdict/ehsave known{clear userdict
/ehsave get restore 2 vmreclaim}if vmstatus exch pop exch pop PrtVMMsg}{
(ERROR: )prnt errorname prnt nl(OFFENDING COMMAND: )prnt/command load prnt
$error/ostack known{nl nl(STACK:)prnt nl nl $error/ostack get aload length{==}
repeat}if}ifelse systemdict/showpage get exec(%%[ Error: )print errorname
=print(; OffendingCommand: )print/command load =print( ]%%)= flush}if end end
end}dup 0 systemdict put dup 4 $brkpage put bind readonly put/currentpacking
where{pop/setpacking where{pop oldpack setpacking}if}if
%%EndResource
obs: I'm using Windows 10
OK well if you look at the file you posted it begins:
RedMon - Redirection Port Monitor
Copyright (C) 1997-2012, Ghostgum Software Pty Ltd. All Rights Reserved.
2012-06-21 Version 1.9
get_filename_as_user sent: C:\Users\Marina\Desktop\teste.pdf
....
...
REDMON WritePort: about to write 4096 bytes to port.
%!PS-Adobe-3.0
Everything up to the %!PS-Adobe is not PostScript and is not being sent by the printer driver. That's all being sent by the RedMon Port Monitor. The output then ends with:
%%EOF
REDMON WritePort: OK count=2489 written=2489
REDMON EndDocPort: starting
REDMON WriteThread: ending
%%[Page: 1]%%
%%[LastPage]%%
REDMON EndDocPort: process finished after 1 second
REDMON EndDocPort: 0 bytes written to printer
REDMON EndDocPort: ending
and there everything following the %%EOF is not PostScript and is coming from the Port Monitor. Since all of that doesn't form a valid PostScript program, sending it to Ghostscript will cause an error of some kind.
You need to work out how to strip that verbiage out and only send the PostScript language program to Ghostscript.
I'm afraid I can't help you with that, I don't know how your application is receiving data from RedMon.
In any event this isn't really a Ghostscript, or PostScript, question.
If you execute backup using SMO, after successful completion, I test the SqlError for null, considering that the backup completed without errors:
But, as you can see, it actually return an error of class 0 number 3014 meaning success.
So the question is:
Q: How can I find out if backup completed successfully or not, and how should I handle these messages and statuses cleanly?
I'm afraid that there are multiple of "gotchas" here that I don't want to bite me in the ass later when this code goes production :)
I agree there are multiple "gotchas" and I personally think Microsoft SMO events are poorly implemented as the ServerMessageEventArgs contains a Property Error which not always provides information about an error but messages about successful operations!!
As an example:
The "error" with code 4035 is an information message. Not an error.
The "error" with code 3014 is an information message that happens when completed successfully. Not an error
Also notice that the Information event does not always happen when an error has occurred. It actually happens whenever SQL Server sends a message that could be information about the operation that has finished successfully
I am also concerned about not handling the errors/success properly, but checking sqlError as Null is a bad idea since it will not be ever null and it will always contain some message about successful operation or an error. And assuming that Information event occurs when there is an error is also a bad idea.
I suggest to handle the errors through the SqlError.Number and depending on the SMO event it has been triggered.
I have made the following code to create a database backup. It is in VB.NET but in C# would be similar. It receives necessary parameters including a delegate where to invoke events (to be handled at GUI), a progress to report the percentage and error handling depending on event triggered and the SqlError.Number as mentioned
Public Sub BackupDatabase(databaseName As String, backupFileFullPath As String, optionsBackup As OptionsBackupDatabase, operationProgress As IProgress(Of Integer),
operationResult As Action(Of OperationResult)) Implements IDatabaseManager.BackupDatabase
Dim sqlBackup As New Smo.Backup()
sqlBackup.Action = Smo.BackupActionType.Database
sqlBackup.BackupSetName = databaseName & " Backup"
sqlBackup.BackupSetDescription = "Full Backup of " & databaseName
sqlBackup.Database = databaseName
Dim bkDevice As New Smo.BackupDeviceItem(backupFileFullPath, Smo.DeviceType.File)
sqlBackup.Devices.Add(bkDevice)
sqlBackup.Initialize = optionsBackup.Overwrite
sqlBackup.Initialize = True
sqlBackup.PercentCompleteNotification = 5
AddHandler sqlBackup.PercentComplete, Sub(sender As Object, e As PercentCompleteEventArgs)
operationProgress.Report(e.Percent)
End Sub
AddHandler sqlBackup.Complete, Sub(sender As Object, e As ServerMessageEventArgs)
Dim sqlMessage As SqlClient.SqlError = e.Error
Dim opResult As New OperationResult()
Select Case sqlMessage.Number
Case 3014
opResult.operationResultType = OperationResultType.SmoSuccess
opResult.message = "Backup successfully created at " & backupFileFullPath & ". " & sqlMessage.Number & ": " & sqlMessage.Message
Case Else
opResult.operationResultType = OperationResultType.SmoError
opResult.message = "ERROR CODE " & sqlMessage.Number & ": " & sqlMessage.Message
End Select
operationResult.Invoke(opResult)
End Sub
AddHandler sqlBackup.NextMedia, Sub(sender As Object, e As ServerMessageEventArgs)
Dim sqlMessage As SqlClient.SqlError = e.Error
Dim opResult As New OperationResult()
opResult.operationResultType = OperationResultType.SmoError
opResult.message = "ERROR CODE: " & sqlMessage.Number & ": " & sqlMessage.Message
operationResult.Invoke(opResult)
End Sub
AddHandler sqlBackup.Information, Sub(sender As Object, e As ServerMessageEventArgs)
Dim sqlMessage As SqlClient.SqlError = e.Error
Dim opResult As New OperationResult()
Select Case sqlMessage.Number
Case 4035
opResult.operationResultType = OperationResultType.SmoInformation
opResult.message = sqlMessage.Number & ": " & sqlMessage.Message
Case Else
opResult.operationResultType = OperationResultType.SmoError
opResult.message = "ERROR CODE " & sqlMessage.Number & ": " & sqlMessage.Message
End Select
operationResult.Invoke(opResult)
End Sub
Try
sqlBackup.SqlBackupAsync(smoServer)
Catch ex As Exception
Throw New BackupManagerException("Error backing up database " & databaseName, ex)
End Try
End Sub
I could be wrong, but I believe that the Complete event firing is the main indicator that the backup was successful - errors would be reported through the Information event.
Since the class of the error is 0 (or any value below 10), it indicates that it's an informational message, not an actual error (Error is somewhat misnamed). And 3014 is defined as the message that's sent when the backup is successful.
I'm not sure what other "gotchas" you're concerned with, since you haven't documented them.
I'm trying to find a way to get mail attachments (programaticaly) , and then open it... can someone tell me how to do this or point me to a right direction?
This is a VBA script (use in a Macro in Outlook) which will loop over all attachments in all selected items in the current folder and Save them to disk.
It should get you started and I don't think it would take much to add some kind of process launch rather than save logic to it.
Public Sub SaveAttachments()
'Note, this assumes you are in the a folder with e-mail messages when you run it.
'It does not have to be the inbox, simply any folder with e-mail messages
Dim Exp As Outlook.Explorer
Dim Sel As Outlook.Selection
Dim AttachmentCnt As Integer
Dim AttTotal As Integer
Dim MsgTotal As Integer
Dim outputDir As String
Dim outputFile As String
Dim fileExists As Boolean
Dim cnt As Integer
'Requires reference to Microsoft Scripting Runtime (SCRRUN.DLL)
Dim fso As FileSystemObject
Set Exp = Application.ActiveExplorer
Set Sel = Exp.Selection
Set fso = New FileSystemObject
outputDir = "C:\Path"
If outputDir = "" Then
MsgBox "You must pick an directory to save your files to. Exiting SaveAttachments.", vbCritical, "SaveAttachments"
Exit Sub
End If
Dim att As Attachment
'Loop thru each selected item in the inbox
For cnt = 1 To Sel.Count
'If the e-mail has attachments...
If Sel.Item(cnt).Attachments.Count > 0 Then
MsgTotal = MsgTotal + 1
'For each attachment on the message...
For AttachmentCnt = 1 To Sel.Item(cnt).Attachments.Count
'Get the attachment
Set att = Sel.Item(cnt).Attachments.Item(AttachmentCnt)
outputFile = att.FileName
outputFile = Format(Sel.Item(cnt).ReceivedTime, "yyyy-mm-dd_hhmmss - ") + outputFile
fileExists = fso.fileExists(outputDir + outputFile)
'Save it to disk if the file does not exist
If fileExists = False Then
att.SaveAsFile (outputDir + outputFile)
AttTotal = AttTotal + 1
End If
Set att = Nothing
Sel.Item(cnt).Close (Outlook.OlInspectorClose.olDiscard)
Next
End If
Next
'Clean up
Set Sel = Nothing
Set Exp = Nothing
Set fso = Nothing
'Let user know we are done
Dim doneMsg As String
doneMsg = "Completed saving " + Format$(AttTotal, "#,0") + " attachments in " + Format$(MsgTotal, "#,0") + " Messages."
MsgBox doneMsg, vbOKOnly, "Save Attachments"
Exit Sub
ErrorHandler:
Dim errMsg As String
errMsg = "An error has occurred. Error " + Err.Number + " " + Err.Description
Dim errResult As VbMsgBoxResult
errResult = MsgBox(errMsg, vbAbortRetryIgnore, "Error in Save Attachments")
Select Case errResult
Case vbAbort
Exit Sub
Case vbRetry
Resume
Case vbIgnore
Resume Next
End Select
End Sub
Look for the COM-reference of Outlook.
You can also use a macro written in vba to do this.