System.out of Memory Exception for String Builder in SSIS Script Task - vb.net

I am using a VB script in SSIS Script Task to add header and Trailer to a flat file. The code was working fine until recently i came across a problem where the rows in the file are more than usual and resulting in a failure on script task with error`Error:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
at System.String.GetStringForStringBuilder(String value, Int32 startIndex, Int32 length, Int32 capacity)
at System.Text.StringBuilder.GetNewString(String currentString, Int32 requiredLength)
at System.Text.StringBuilder.Append(Char[] value, Int32 startIndex, Int32 charCount)
at System.IO.StreamReader.ReadToEnd()
at System.IO.File.ReadAllText(String path, Encoding encoding)
at System.IO.File.ReadAllText(String path)`
Can any one help me in fixing the problem please.I think instead of "String Builder" i need to use other string related method. I am getting error at
fileContents.Append(File.ReadAllText(Dts.Connections("DestinationConnection").ConnectionString))
Here is my code:
Public Sub Main()
Dim fileContents As New StringBuilder()
Dim finalfile As String
Dim firstline As String
Dim lastline As String
Dts.VariableDispenser.LockForRead("FirstLine")
Dts.VariableDispenser.LockForRead("LastLine")
Dts.VariableDispenser.LockForRead("FileName")
firstline = CType(Dts.Variables("FirstLine").Value, String)
finalfile = CType(Dts.Variables("FileName").Value, String)
lastline= CType(Dts.Variables("LastLine").Value, String)
'Write header, then append file contents and write back out.
fileContents.AppendLine(String.Format("{0}", firstline))
fileContents.Append(File.ReadAllText(Dts.Connections("DestinationConnection").ConnectionString))
fileContents.AppendLine(String.Format("{0}", lastline))
File.WriteAllText(finalfile, fileContents.ToString())
Dts.TaskResult = ScriptResults.Success
End Sub

Well, one simple way would be to just avoid the StringBuilder: open a TextWriter with File.CreateText, write the first line, then write File.ReadAllText(...), then write the final line.
However, that will only save you some memory - it will roughly halve the memory required, as you won't need it in both the StringBuilder and a string (which is what I think will happen now).
A much better alternative would be to:
Open the writer
Write the header line
Open the other file for reading
Loop over the file, reading a chunk of characters at a time and writing it to the new file, until you're done
Close the other file implicitly (use a Using statement for this)
Write the trailing line
Close the write implicitly (use a Using statement)
That way even if you've got huge files, you only need a small chunk of data in memory at a time.

The problem is File.ReadAllText has limitations when it comes to reading a large file because the entire file is read into memory.
What you will need to do is replace the File.ReadAllText with reading the file line by line and append it accordingly.
EDITED FOR AN EXAMPLE:
Option Explicit
Dim oFSO, sFile, oFile, sText
Set oFSO = CreateObject("Scripting.FileSystemObject")
sFile = "your text file"
If oFSO.FileExists(sFile) Then
Set oFile = oFSO.OpenTextFile(sFile, 1)
Do While Not oFile.AtEndOfStream
sText = oFile.ReadLine
If Trim(sText) <> "" Then
fileContents.AppendLine(sText)
End If
Loop
oFile.Close
Else
WScript.Echo "The file was not there."
End If
It's possible you may still have an issue with the fileContents StringBuilder. The original error shown though was thrown from the File.ReadAllText method. So hopefully, this does the trick.
If not, I would just forget about the fileContents StringBuilder all together and write out the header. Then read from the file line by line and write it out line by line, then finally write the footer.

An alternative (and much more SSIS-like) solution would be to create a Data Flow Task that reads your existing file, pipes it through a Script Component that adds the header and footer, and writes it to the file system. Here's what it might look like in SSIS 2005:
The Script Component will be a Transformation with the SynchronousInputID of its output set to False, so that it can generate header and footer rows:
And the VB source of the transform should look something like this:
Public Class ScriptMain
Inherits UserComponent
Dim headerWritten As Boolean = False
Public Overrides Sub IncomingRows_ProcessInputRow(ByVal Row As IncomingRowsBuffer)
If Not headerWritten Then
WriteHeader()
End If
OutgoingRowsBuffer.AddRow()
OutgoingRowsBuffer.theLine = Row.theLine
End Sub
Public Overrides Sub FinishOutputs()
MyBase.FinishOutputs()
WriteFooter()
End Sub
Private Sub WriteHeader()
OutgoingRowsBuffer.AddRow()
OutgoingRowsBuffer.theLine = "The First Header Line"
headerWritten = True
End Sub
Private Sub WriteFooter()
OutgoingRowsBuffer.AddRow()
OutgoingRowsBuffer.theLine = "Here's a footer line"
OutgoingRowsBuffer.AddRow()
OutgoingRowsBuffer.theLine = "Here's another one"
End Sub
End Class
This lets you use the streaming capabilities of SSIS to your advantage.

Related

VB.NET: The process cannot access the file 'filename' because it is being used by another process

I should add a list of files into a ZIP. Procedure code is like this
Sub CreateZip
Dim FileList As New ArrayList 'List of File Paths to be added to the ZIP
For Each path in FileList
Try
AddFileToZip(ZipFilePath, path.ToString)
Catch (ex as New Exception)
....
End Try
Next
End Sub
And this is AddFileToZip
Public Sub AddFileToZip(ByVal zipFilename As String, ByVal fileToAdd As String)
Using zip As Package = System.IO.Packaging.Package.Open(zipFilename, FileMode.OpenOrCreate)
Dim destFilename As String = ".\" & Path.GetFileName(fileToAdd)
Dim uri As Uri = PackUriHelper.CreatePartUri(New Uri(destFilename, UriKind.Relative))
If zip.PartExists(uri) Then
zip.DeletePart(uri)
End If
Dim part As PackagePart = zip.CreatePart(uri, "", CompressionOption.Normal)
Using fileStream As New FileStream(fileToAdd, FileMode.Open, FileAccess.Read)
Using dest As Stream = part.GetStream()
CopyStream(fileStream, dest)
End Using
End Using
End Using
End Sub
At runtime, I get this error message
The process cannot access the file [ZipFilePath] because it is being used by another process
this error is raised randomly, maybe on adding small files into the Zip. If I place breakpoints and run procedure in debug mode, it works.
It seems clear that procedure thread is not synchronized with IO, so that my procedure loop continues even if IO is still adding processed file into Zip (ie VB.NET is faster than IO).
I also tried to place a Thread.Sleep(1000) before AddFileToZip, but this may be not enough to synchronize the two processes. Placing Thread.Sleep(2000) seems to make procedure work, but it may slow down dramaticly performances (I should pack more than 50 files into my Zip).
So, how can I force my loop to "wait" until IO Process has released ZIP file?

Remove double quotes in the content of text files

I am using a legacy application where all the source code is in vb.net. I am checking if the file exists and if the condition is true replace all the " in the contents of the file. For instance "text" to be replaced as text. I am using the below code.
vb.net
Dim FileFullPath As String
FileFullPath = "\\Fileshare\text\sample.txt"
If File.Exists(FileFullPath) Then
Dim stripquote As String = FileFullPath
stripquote = stripquote.Replace("""", "").Trim()
Else
'
End If
I get no errors and at the same time the " is not being replaced in the content of the file.
Data:
ID, Date, Phone, Comments
1,05/13/2021,"123-000-1234","text1"
2,05/13/2021,"123-000-2345","text2"
3,05/13/2021,"123-000-3456","text2"
Output:
1,05/13/2021,123-000-1234,text1
2,05/13/2021,123-000-2345,text2
3,05/13/2021,123-000-3456,text2
You can read each line of the file, remove the double-quotes, write that to a temporary file, then when all the lines are done delete the original and move/rename the temporary file as the filename:
Imports System.IO
'...
Sub RemoveDoubleQuotes(filename As String)
Dim tmpFilename = Path.GetTempFileName()
Using sr As New StreamReader(filename)
Using sw As New StreamWriter(tmpFilename)
While Not sr.EndOfStream
sw.WriteLine(sr.ReadLine().Replace("""", ""))
End While
End Using
End Using
File.Delete(filename)
File.Move(tmpFilename, filename)
End Sub
Add error handling as desired.
The best way to go about this depends on the potential size of the file. If the file is relatively small then there's no point processing it line by line and certainly not using a TextFieldParser. Just read the data in, process it and write it out:
File.WriteAllText(FileFullPath,
File.ReadAllText(FileFullPath).
Replace(ControlChars.Quote, String.Empty))
Only if the file is potentially large and reading it all in one go would require too much memory should you consider processing it line by line. In that case, I'd go this way:
'Let the system create a temp file.
Dim tempFilePath = Path.GetTempFileName()
'Open the temp file for writing text.
Using tempFile As New StreamWriter(tempFilePath)
'Open the source file and read it line by line.
For Each line In File.ReadLines(FileFullPath)
'Remove double-quotes from the current line and write the result to the temp file.
tempFile.WriteLine(line.Replace(ControlChars.Quote, String.Empty))
Next
End Using
'Overwrite the source file with the temp file.
File.Move(tempFilePath, FileFullPath, True)
Note the use of File.ReadLines rather than File.ReadAllLines. The former will only read one line at a time where the latter reads every line before you can process any of them.
EDIT:
Note that this:
File.Move(tempFilePath, FileFullPath, True)
only works in .NET Core 3.0 and later, including .NET 5.0. If you're targeting .NET Framework then you have three other options:
Delete the original file (File.Delete) and then move the temp file (File.Move).
Copy the temp file (File.Copy) and then delete the temp file (File.Delete).
Call My.Computer.FileSystem.MoveFile to move the temp file and overwrite the original file in one go.
TextFieldParser is probably the way to go.
Your code with a few changes.
Static doubleQ As String = New String(ControlChars.Quote, 2)
Dim FileFullPath As String
FileFullPath = "\\Fileshare\text\sample.txt"
If IO.File.Exists(FileFullPath) Then
Dim stripquote As String = IO.File.ReadAllText(FileFullPath)
stripquote = stripquote.Replace(doubleQ, "").Trim()
Else
'
End If
Note the static declaration. I adopted this approach because it confused the heck out of me.

Simple code that reads CSV values causes an error in System.IO.Directory

I can't seem to figure out why I'm getting a compilation error with this code that tries to find the most recently updated file (all CSV files) in a directory, to then pull the last line of the CSV and update a device.
The exception I get is:
Line 3 Character 10 expected end of statement.
Don't worry about the hs.SetDevice, I know that part is correct.
Imports System.IO
Sub Main()
Dim path = System.IO.DirectoryInfo.GetFiles("C:\Users\Ian\Documents\Wifi Sensor Software").OrderByDescending(Function(f) f.LastWriteTime).First()
Dim line = System.IO.File.ReadLines(path).Last()
Dim fields() = line.Split(",".ToCharArray())
Dim fileTemp = fields(2)
hs.SetDeviceValueByRef(124, fileTemp, True)
End Sub
EDIT:
Changed Directory to DirectoryInfo
The original problem was that Directory.GetFiles() returns an array of strings, a string doesn't have a LastWriteTime Property.
This property belongs to the FileInfo base class, FileSystemInfo, the object type returned by DirectoryInfo.GetFiles().
Then, a FileInfo object cannot be passed to File.ReadLines(), this method expects a string, so you need to pass [FileInfo].FullName.
Hard-coding a Path in that manner is not a good thing. Use Environment.GetFolderPath() to get the Path of special folders, as the MyDocuments folder, and Path.Combine() to build a valid path.
Better use the TextFieldParser class to parse a CSV file. It's very simple to use and safe enough.
The worst problem is Option Strict set to Off.
Turn it On in the Project's Properties (Project->Properties->Compile), or in the general options of Visual Studio (Tools->Options->Projects and Solutions->VB Defaults), so it's already set for new Projects.
You can also add it on top of a file, as shown here.
With Option Strict On, you are immediately informed when a mishap of this kind is found in your code, so you can fix it immediately.
With Option Strict Off, some issues that come up at run-time can be very hard to identify and fix. Setting it On to try and fix the problem later is almost useless, since all the mishaps will come up all at once and you'll have a gazillion of error notifications that will hide the issue at hand.
Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO
Dim filesPath = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments), "Wifi Sensor Software")
Dim mostRecentFile = New DirectoryInfo(filesPath).
GetFiles("*.csv").OrderByDescending(Function(f) f.LastWriteTime).First()
Using tfp As New TextFieldParser(mostRecentFile.FullName)
tfp.TextFieldType = FieldType.Delimited
tfp.SetDelimiters({","})
Dim fileTemp As String = String.Empty
Try
While Not tfp.EndOfData
fileTemp = tfp.ReadFields()(2)
End While
Catch fnfEx As FileNotFoundException
MessageBox.Show($"File not found: {fnfEx.Message}")
Catch exIDX As IndexOutOfRangeException
MessageBox.Show($"Invalid Data format: {exIDX.Message}")
Catch exIO As MalformedLineException
MessageBox.Show($"Invalid Data format at line {exIO.Message}")
End Try
If Not String.IsNullOrEmpty(fileTemp) Then
hs.SetDeviceValueByRef(124, fileTemp, True)
End If
End Using

VB.NET 2010 - Extracting an application resource to the Desktop

I am trying to extract an application resource from My.Resources.FILE
I have discovered how to do this with DLL & EXE files, but I still need help with the code for extracting PNG & ICO files.
Other file types also. (If possible)
Here is my current code that works with DLL & EXE files.
Dim File01 As System.IO.FileStream = New System.IO.FileStream("C:\Users\" + Environment.UserName + "\Desktop\" + "SAMPLE.EXE", IO.FileMode.Create)
File01.Write(My.Resources.SAMPLE, 0, My.Resources.SAMPLE.Length)
File01.Close()
First things first, the code you have is bad. When using My.Resources, every time you use a property, you extract a new copy of the data. That means that your second line is getting the data to write twice, with the second time being only to get its length. At the very least, you should be getting the data only once and assigning it to a variable, then using that variable twice. You should also be using a Using statement to create and destroy the FileStream. Even better though, just call File.WriteAllBytes, which means that you don't have to create your own FileStream or know the length of the data to write. You should also not be constructing the file path that way.
Dim filePath = Path.Combine(My.Computer.FileSystem.SpecialDirectories.Desktop, "SAMPLE.EXE")
File.WriteAllBytes(filePath, My.Resources.SAMPLE)
As for your question, the important thing to understand here is that it really has nothing to do with resources. The question is really how to save data of any particular type and that is something that you can look up for yourself. When you get the value of a property from My.Resources, the type of the data you get will depend on the type of the file you embedded in first place. In the case of a binary file, e.g. DLL or EXE, you will get back a Byte array and so you save that data to a file in the same way as you would any other Byte array. In the case of an image file, e.g. PNG, you will get back an Image object, so you save that like you would any other Image object, e.g.
Dim filePath = Path.Combine(My.Computer.FileSystem.SpecialDirectories.Desktop, "PICTURE.PNG")
Using picture = My.Resources.PICTURE
picture.Save(filePath, picture.RawFormat)
End Using
For an ICO file you will get back an Icon object. I'll leave it to you to research how to save an Icon object to a file.
EDIT:
It's important to identify what the actual problem is that you're trying to solve. You can obviously get an object from My.Resources so that is not the problem. You need to determine what type that object is and determine how to save an object of that type. How to do that will be the same no matter where that object comes from, so the resources part is irrelevant. Think about what it is that you have to do and write a method to do it, then call that method.
In your original case, you could start like this:
Dim data = My.Resources.SAMPLE
Once you have written that - even as you write it - Intellisense will tell you that the data is a Byte array. Your actual problem is now how to save a Byte array to a file, so write a method that does that:
Private Sub SaveToFile(data As Byte(), filePath As String)
'...
End Sub
You can now which you want to do first: write code to call that method as appropriate for your current scenario or write the implementation of the method. There are various specific ways to save binary data, i.e. a Byte array, to a file but, as I said, the simplest is File.WriteAllBytes:
Private Sub SaveToFile(data As Byte(), filePath As String)
File.WriteAllBytes(filePath, data)
End Sub
As for calling the method, you need to data, which you already have, and the file path:
Dim data = My.Resources.SAMPLE
Dim folderPath = My.Computer.FileSystem.SpecialDirectories.Desktop
Dim fileName = "SAMPLE.EXE"
Dim filePath = Path.Combine(folderPath, fileName)
SaveToFile(data, filePath)
Simple enough. You need to follow the same steps for any other resource. If you embedded a PNG file then you would find that the data is an Image object or, more specifically, a Bitmap. Your task is then to learn how to save such an object to a file. It shouldn't take you long to find out that the Image class has its own Save method, so you would use that in your method:
Private Sub SaveToFile(data As Image, filePath As String)
data.Save(filePath, data.RawFormat)
End Sub
The code to call the method is basically as before, with the exception that an image object needs to be disposed when you're done with it:
Dim data = My.Resources.PICTURE
Dim folderPath = My.Computer.FileSystem.SpecialDirectories.Desktop
Dim fileName = "SAMPLE.EXE"
Dim filePath = Path.Combine(folderPath, fileName)
SaveToFile(data, filePath)
data.Dispose()
The proper way to create and dispose an object in a narrow scope like this is with a Using block:
Dim folderPath = My.Computer.FileSystem.SpecialDirectories.Desktop
Dim fileName = "SAMPLE.EXE"
Dim filePath = Path.Combine(folderPath, fileName)
Using data = My.Resources.PICTURE
SaveToFile(data, filePath)
End Using
Now it is up to you to carry out the same steps for an ICO file. If you are a hands on learner then get your hands on.

dispose of IO.File.ReadAllLines

I am reading very large text files (6-10 MB). I am splitting the text files in to multiple new text files. There is common "header" and "footer" in the "read" text file that I will store as variable to be called at later time. I can't figure out how to properly dispose of IO.File.ReadAllLines. I'm concerned this will be held in memory if I don't dispose of it properly.
Text.Dispose or Text.Close isn't valid.
Dim testHeader As String
Dim testSite As String
Dim testStart As String
Dim testStop As String
Dim testTime As String
Dim text() As String = IO.File.ReadAllLines("C:\Users\anobis\Desktop\temp.txt")
testHeader = text(0)
testSite = text(text.Length - 4)
testStart = text(text.Length - 3)
testStop = text(text.Length - 2)
testTime = text(text.Length - 1)
text.dispose()
Later in the program I will be initiating another StreamReader and want to avoid conflicts and memory resource issues. I am new at coding so be gentle! Thanks!
' Open temp.txt with "Using" statement.
Using r As StreamReader = New StreamReader("C:\Users\anobis\Desktop\temp.txt")
' Store contents in this String.
Dim line As String
line = r.ReadLine
' Loop over each line in file, While list is Not Nothing.
Do While (Not line Is Nothing)
If line Like (sourceSN.Text + "*") Then 'Substitute in source serial number "xxxxxx*"
file.WriteLine(line)
End If
' Read in the next line of text file.
line = r.ReadLine
Loop
End Using
file.WriteLine(testSite)
file.WriteLine(testStart)
file.WriteLine(testStop)
file.WriteLine(testTime)
' Close transfer.txt file
file.Close()
You don't need to dispose of it. It returns a managed string array, who's lifetime is managed by the garbage collector. Internally, File.ReadAllLines is disposing of the underlying native file handle it created to read all of the lines for you.