Files not opening as read-only in VB.NET when read-only attribute set - vb.net

I'm having trouble with a bit of code designed to intentionally open files as read-only. My team often needs to be able to peek into each others' files without locking the file owner out, so in a form being designed for document management I want users to be able to open files optionally as read-only.
Coming from VBA I'm still somewhat new to VB.NET and also bitwise operations generally, but I believe the "read-only" interpretation of this code from MS Docs has been correctly implemented:
Dim attributes As FileAttributes
attributes = File.GetAttributes(path)
If Not (attributes And FileAttributes.ReadOnly) = FileAttributes.ReadOnly Then
' Make file readonly.
File.SetAttributes(path, File.GetAttributes(path) Or FileAttributes.ReadOnly)
End If
' Open the file
System.Diagnostics.Process.Start(path)
' Reset the file to read/write.
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly)
File.SetAttributes(path, attributes)
When I use "GetAttributes" before and after the line to open the file I get a return of 1 or sometimes as 33, which the FileAttributes enumeration documentation suggests is correct for what I'm trying to do. Before and after the attribute change "GetAttributes" returns 128 or in certain cases 32, which also should be correct.
However despite the fact the above code appears correctly implemented and seems to be producing the correct affect in the file's attributes, files opened this way (namely Excel files) open as read-write. I'm also fine with other ways of opening a file read-only provided that it can be used equally well on any document you would commonly encounter in an office setting (Excel, Word, etc.) with its default program. That being said, I've tried several methods and haven't had any success, and this one by far has seemed the cleanest and most promising.
Thanks in advance!

As described in comments, the file attributes are restored to their
previous state right after the Process.Start() command: the
application that opens the file has not been started yet; when it
finally access the file, the read-ony attribute has already been
removed.
A possible solution is to subscribe to the Process.Exited event and restore the original file attributes when the Process termination is notified.
A modified version of your code: the EnableRaisingEvents property causes a process to raise the Process.Exited event. I subscribed to the event using an in-line delegate (a Lambda), but I added an example that uses a standard delegate method using the AddressOf operator (since you said you have to learn about events).
Since we want to run a file and not an executable, we need to also set UseShellExecute = True, so the Shell will find and execute the registered application associated with the file extension.
If UseShellExecute = True is not specified, an exception is raised (the file is not an executable).
The name of the file to execute is assigned to the Process.StartInfo.FileName
When the Process terminates, the Exited event is raised. In the event handler, the file attributes are restored to the previous state.
Private Sub SomeMethod(filePath As String)
' filePath is File's Full path
Dim attributes As FileAttributes = File.GetAttributes(filePath)
File.SetAttributes(filePath, (attributes) Or FileAttributes.ReadOnly)
Dim proc As Process = New Process()
proc.StartInfo.FileName = filePath
proc.StartInfo.UseShellExecute = True
proc.EnableRaisingEvents = True
AddHandler proc.Exited,
Sub()
File.SetAttributes(filePath, attributes)
proc?.Dispose()
End Sub
proc.Start()
End Sub
If you want to use a standard method as the Exited event handler, you have to declare the filePath and attributes variables in a different scope. Neither can be a local variable, they won't be accessible from the method delegate.
If you need to run just one file, these can be instance fields (declared in the scope of the current class).
If you instead can have multiple processes running different files, all waiting for the associated applicationt to terminate, these informations should to be stored in a list of objects, a Dictionary or a similar container.
For example, using a Dictionary, declared as a Field:
(the Dictionary Key is the File path. If a file can be opened multiple times - a .txt file maybe, use a different identifier)
Private myRunningFiles As New Dictionary(Of String, FileAttributes)
' (...)
Private Sub SomeMethod(filePath As String)
Dim attributes As FileAttributes = File.GetAttributes(filePath)
If Not myRunningFiles.ContainsKey(filePath) Then
myRunningFiles.Add(filePath, attributes)
Else
' Notify that the file is already opened
Return
End If
Dim proc As Process = New Process()
' (... same ...)
AddHandler proc.Exited, AddressOf OnProcessExited
End Sub
Protected Sub OnProcessExited(sender As Object, e As EventArgs)
Dim proc = DirectCast(sender, Process)
Dim filePath = proc.StartInfo.FileName
Dim attributes = myRunningFiles(filePath)
File.SetAttributes(filePath, attributes)
myRunningFiles.Remove(filePath)
proc?.Dispose()
End Sub

Thanks to #Jimi I was able to come up with the solution. In my application Excel files are the most important to open in ReadOnly so I'm happy with an Excel-only solution for the time being. While his answer for using and releasing attributes was great it had the problem of not restoring the attributes to their default until the file is closed, which to my understanding would cause others to also open the file ReadOnly while the file is open. The point of this in my application is to "peek" at a file without locking other users on a network out of ReadWrite access, so unfortunately his-well-thought out solution won't work.
I was however able to use the /r switch with a bit of investigating. It was a bit tricky (for someone of my skill level) since some switches need to be placed before the file path and some after. Solution below:
Process.Start("EXCEL.exe", "/r " & Chr(34) & path & Chr(34))

Related

Using the same file error

On a button click I have the following code to write what Is in my textboxes.
Dim file As System.IO.StreamWriter
file = My.Computer.FileSystem.OpenTextFileWriter("C:/Users/Nick/Documents/Dra.txt", False)
file.WriteLine(NameBasic)
file.WriteLine(LastBasic)
file.WriteLine(PhoneBasic)
file.WriteLine(NameEmer1)
On my form load, I load what is in the notepad from what was written, It is saying It is already being used(the file) which is true, how can I have two different functions(write, and read) manipulating the same file with out this error?
The process cannot access the file 'C:\Users\Nick\Documents\Dra.txt' because it is being used by another process.
And here is the code for my onformload
Dim read As System.IO.StreamReader
read = My.Computer.FileSystem.OpenTextFileReader("C:/Users/Nick/Documents/Dra.txt")
lblNameBasic.Text = read.ReadLine
I am sort of stuck on this problem, thank you
You need to close the file when you are done writing to it.
Dim file As System.IO.StreamWriter
file = My.Computer.FileSystem.OpenTextFileWriter("C:/Users/Nick/Documents/Dra.txt", False)
file.WriteLine(NameBasic)
file.WriteLine(LastBasic)
file.WriteLine(PhoneBasic)
file.WriteLine(NameEmer1)
file.Close()
To answer your question:
how can I have two different functions(write, and read) manipulating the same file with out this error?
If you really want to simultaneously read and write to the same file from two processes, you need to open the file with the FileShare.ReadWrite option. The My.Computer.FileSystem methods don't do that.¹
However, I suspect that you don't really wan't to do that. I suspect that you want to write and, after you are finished writing, you want to read. In that case, just closing the file after using it will suffice. The easiest way to do that is to use the Using statement:
Using file = My.Computer.FileSystem.OpenTextFileWriter(...)
file.WriteLine(...)
file.WriteLine(...)
...
End Using
This ensures that the file will be closed properly as soon as the Using block is left (either by normal execution or by an exception). In fact, it is good practice to wrap use of every object whose class implements IDisposable (such as StreamWriter) in a Using statement.
¹ According to the reference source, OpenTextFileWriter creates a New IO.StreamWriter(file, append, encoding), which in turn creates a new FileStream(..., FileShare.Read, ...).

Releasing memory held in objects in VB.NET

I am using a 3rd party DLL from pdf-tools to parse PDF files inside my VB.NET application and write the extracted data to a SQL localDB database. I'm giving the user two options: to select a PDF file for parsing or to point to a folder and the application will loop through all PDF files inside it. Both options call the same procedure doPDFFile() as below.
The problem is: if I import a number of files individually by selecting a file every time, the program runs fine. However, If I select a folder that contains the same files, the memory used by the program will keep growing further and further. In Windows task manager, it can reach to 1 GB after importing around 30 files.
I used ANTS memory profiler from redgate, and it showed that one object called "GraphicsState" which is part of the pdf-tools object is growing too big when looping inside a folder. This does not happen if I select the same files one by one. Besides the memory problem, the application becomes very slow after parsing some files. My questions is: why is this happening? and how to prevent it? The user should be able to point the program to a folder with hundreds of PDF files, how can I achieve this?
Below is a snapshot of the code:
'When user selects one file
Private Sub OpenToolStripMenuItem_Click(...)
OpenFileDialog1.FileName.ToString
doPDFFile()
End Sub
'When user selects a folder
Private Sub LoopToolStripMenuItem_Click() Handles LoopToolStripMenuItem.Click
FolderBrowserDialog1.ShowDialog()
sPath = FolderBrowserDialog1.SelectedPath
For Each fileName As String In IO.Directory.GetFiles(...)
sPath = fileName
doPDFFile()
Next
End Sub
Inside the doPDFFile() procedure, I'm doing the following, I'm using the document object from pdf-tools and I'm passing it byRef to another procedure:
Public Sub doPDFFile()
Using document As New Pdftools.PdfExtract.Document
document.open(sPath)
findFirstPage(document) 'passing by reference
ParseFirstPage(document) 'passing by reference
'storing the parsed text in an array
'.......
do
'extracting the colors from the graphicsStateObject inside the document object:
Using objGraphicsState As Pdftools.PdfExtract.GraphicsState = content.GraphicsState
sColor = objGraphicsState.FillColorRGB
End Using
'save text and color in an array of objects
until endOfText
end using
end sub

Program not able to write to a newly made text file

After some great arguments made by other users in this question: How to write to a text file inside of the application, I decided to not use a resource file, instead create a file in a folder & then read/write from there.
Although, for some reason, I can't seem to write to the file in question, it keeps throwing an exception telling me the file is already in use by another process.
Here's the code which I use for writing to this file.
If System.IO.File.Exists(credentials) Then
Dim objWriter As New System.IO.StreamWriter(credentials, False)
objWriter.WriteLine(remember)
objWriter.Close()
Else
System.IO.Directory.CreateDirectory(Mid(My.Application.Info.DirectoryPath, 1, 1) & ":\ProgramData\DayZAdminPanel")
System.IO.File.Create(credentials)
Dim objWriter As New System.IO.StreamWriter(credentials, False)
objWriter.WriteLine(remember)
objWriter.Close()
End If
Any ideas on how I can write to the text file in question?
There's a good chance that a previous iteration of your application failed to properly close access to the file in the StreamWriter. Since your constructor is set to overwrite (and not append) to the file, this could be the source.
Try setting up your application with a "Using" statement to properly open/close the file:
If System.IO.File.Exists(credentials) Then
Using objWriter As StreamWriter = New StreamWriter(credentials, False)
objWriter.WriteLine(remember)
objWriter.Close()
End Using
Else
System.IO.Directory.CreateDirectory(Mid(My.Application.Info.DirectoryPath, 1, 1) & ":\ProgramData\DayZAdminPanel")
System.IO.File.Create(credentials)
Using objWriter As StreamWriter = New StreamWriter(credentials, False)
objWriter.WriteLine(remember)
objWriter.Close()
End Using
End If
It does look rather redundant to have a Using block and a close statement, but this ensures access to your file even if an exception occurs.
You are trying to create a directory in the common application data directory. This directory should be found using the Environment class methods and enums because is different between operating systems. However you use the value credentials for the filename. I suppose that you want to store your datafile in the common application directory and not in a place where there is no permission to write data files (Like C:\program files (x86)).
Then, to avoid problems with file stream not correctly closed try to use the Using statement that assures a correct closure and disposal of your file resource (No need to call close inside a Using).
Also, notice that StreamWriter is perfectly capable to create the file if it doesn't exists or if you wish to overwrite the previous contents (passing false for the Append flag).
So your code could be reduced to these lines.
' Get the common application data directory (could be different from Win7 and XP)
Dim workDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)
' Combine with your own working data directory
workDir = Path.Combine(workdir, "DayZAdminPanel")
' Create if not exists
If Not Directory.Exists(workDir) Then
Directory.CreateDirectory(workDir)
End If
' Create/Overwrite your data file in a subfolder of the common application data folder
Dim saveFile = Path.Combine(workDir, Path.GetFileName(credentials))
Using objWriter = New System.IO.StreamWriter(saveFile, False)
objWriter.WriteLine(remember)
End Using
File.Create returns an opened FileStream that you should be passing into the StreamWriter constructor on the subsequent, rather than passing the filename again, or you can just omit the File.Create call altogether.
You might want to look into a Using block for the StreamWriter so it gets predictably closed.

VB.Net File.Exists() Returns True but Excel cannot Open

I check for the existence of the file with File.Exists(filePath). Then I try to open the file from within Excel with Excel.Workbooks.OpenText(filePath). But Excel complains that the file's not there. What the heck?
The context is that I am shelling out to another application to process a given file and produce a .out file, which I then convert to an Excel workbook.
'' At this point, filePath is a .txt file.
Dim args As String = String.Format("""{0}""", filePath)
...
Dim exe As String = Config.ExtractEXE
Dim i As New ProcessStartInfo(exe)
i.Arguments = args
Dim p As Process = Process.Start(i)
p.WaitForExit()
...
'' filePath now becomes the .out file.
'' Then eventually, I get around to checking:
'If Not File.Exists(filePath) Then
' MsgBox("Please ensure...")
' Exit Sub
'End If
'' In response to an answer, I no longer check for the existence of the file, but
'' instead try to open the file.
Private Function fileIsReady(filePath As String) As Boolean
Try
Using fs As FileStream = File.OpenRead(filePath)
Return True
End Using
Catch
Return False
End Try
End Function
Do Until fileIsReady(filePath)
'' Wait.
Loop
ExcelFile.Convert(filePath...)
'' Wherein I make the call to:
Excel.Workbooks.OpenText(filePath...)
'' Which fails because filePath can't be found.
Is there a latency issue, such that .Net recognizes the existence of the file before it's accessible to other applications? I just don't understand why File.Exists() can tell me the file is there and then Excel can't find it.
As far as I know, the only application that might have the file open is the application I call to do the processing. But that application should be finished with the file by the time p.WaitForExit() finishes, right?
I've had to deploy the application with this as a known bug, which really sucks. There's an easy workaround for the user; but still--this bug should not be. Hope you can help.
Whether or not a file exists is not the only factor in whether you can open it. You also need to look at file system permissions and locking.
File.Exists can lie to you (it returns false if you pass a directory path or if any error occurs, even if the file does exist)
The file system is volatile, and things can change even in the brief period between an if (File.Exists(...)) line and trying to open the file in the next line.
In summary: you should hardly ever use file.exists(). Almost any time you are tempted to do so, just try to open the file and make sure you have a good exception handler instead.

Let VB Form prepare working environment in chosen directory

I am working on a GUI for a simulation program. The simulation program is a single .exe which is driven by an input file (File.inp placed in the same directory).
The Original.inp file functions as a template from which the form reads all the values into an array. Then it changes these values reflecting the changes done by the user in the form. After that it writes all the new values to File.inp.
By pushing the "Run" button the Simulation.exe file is executed.
The folder structure looks like this:
root
|
|---input
| |
| |--Original.inp
|
|---GUI.exe
|---Simulation.exe
|---File.inp
Ideally I would supply only the GUI, the user would select the working directory and then the GUI.exe would create an input folder and extract the Original.inp and Simulation.exe in the appropriate locations. So far I have only managed to include Original.inp and Simulation.exe as "EmbeddedResources" in my VB project and I have let my code create an input folder in the working directory chosen by the user.
Can someone please explain to me how I can extract the .inp and .exe file into the correct directories? I've searched on google, tried File.WriteAllBytes and Filestream.WriteByte but did not get the desired results.
The problem with File.WriteAllBytes was that I could not point to the embedded resource ("Simulation.exe is not a member of Resources" and with Filestream.WriteByte I got a 0 kb file.
The question commenters are correct, this is probably a task best left for a setup program. However, that having been stated, in the interest of answering the question as asked I offer the following approach.
Contrary to your supposition in your question comment, you do need to "read" the embedded resource from the GUI's executable file, since it's an embedded resource and not an external resource. It won't magically extract itselt from the executable file. You need to do the manual read from the assembly and write to your specified locations. To do this, you need to read the resource using .Net Reflection, via the currently executing assembly's GetManifestResourceStream method.
The Simulation.exe file is a binary file so it must be handled as such. I assumed that the Orginal.inp file was a text file since it afforded the opportunity to demonstrate different types of file reads and writes. Any error handling (and there should be plenty) is omitted for brevity.
The code could look something like this:
Imports System.IO
Imports System.Reflection
Module Module1
Sub Main()
'Determine where the GUI executable is located and save for later use
Dim thisAssembly As Assembly = Assembly.GetExecutingAssembly()
Dim appFolder As String = Path.GetDirectoryName(thisAssembly.Location)
Dim fileContents As String = String.Empty
'Read the contents of the template file. It was assumed this is in text format so a
'StreamReader, adept at reading text files, was used to read the entire file into a string
'N.B. The namespace that prefixes the file name in the next line is CRITICAL. An embedded resource
'is placed in the executable with the namespace noted in the project file, so it must be
'dereferenced in the same manner.
Using fileStream As Stream = thisAssembly.GetManifestResourceStream("SOQuestion10613051.Original.inp")
If fileStream IsNot Nothing Then
Using textStreamReader As New StreamReader(fileStream)
fileContents = textStreamReader.ReadToEnd()
textStreamReader.Close()
End Using
fileStream.Close()
End If
End Using
'Create the "input" subfolder if it doesn't already exist
Dim inputFolder As String = Path.Combine(appFolder, "input")
If Not Directory.Exists(inputFolder) Then
Directory.CreateDirectory(inputFolder)
End If
'Write the contents of the resource read above to the input sub-folder
Using writer As New StreamWriter(Path.Combine(inputFolder, "Original.inp"))
writer.Write(fileContents)
writer.Close()
End Using
'Now read the simulation executable. The same namespace issues noted above still apply.
'Since this is a binary file we use a file stream to read into a byte buffer
Dim buffer() As Byte = Nothing
Using fileStream As Stream = thisAssembly.GetManifestResourceStream("SOQuestion10613051.Simulation.exe")
If fileStream IsNot Nothing Then
ReDim buffer(fileStream.Length)
fileStream.Read(buffer, 0, fileStream.Length)
fileStream.Close()
End If
End Using
'Now write the byte buffer with the contents of the executable file to the root folder
If buffer IsNot Nothing Then
Using exeStream As New FileStream(Path.Combine(appFolder, "Simulation.exe"), FileMode.Create, FileAccess.Write, FileShare.None)
exeStream.Write(buffer, 0, buffer.Length)
exeStream.Close()
End Using
End If
End Sub
End Module
You will also have to add logic to determine if the files have been extracted so it doesn't happen every time the GUI is invoked. That's a big reason why an installation program might be the correct answer.