VB6: Single-instance application across all user sessions - vb6

I have an application that needs to be a single-instance app across all user sessions on a Windows PC. My research thus far has centered around using a mutex to accomplish this, but I am having an issue that I am not sure is really an issue, this is really a best-practice question I believe.
Here's the code first of all:
Private Const AppVer = "Global\UNIQUENAME" ' This is not what i am using but the name is unique
Public Sub Main()
Dim mutexValue As Long
mutexValue = CreateMutex(ByVal 0&, 1, AppVer)
If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
SaveTitle$ = App.Title
App.Title = "... duplicate instance."
MsgBox "A duplicate instance of this program exists."
CloseHandle mutexValue
Exit Sub
End If
' Else keep on truckin'
Now, based on this article I believe I understand that by passing the NULL pointer to the CreateMutex function as I am above I'm basically assigning whatever security descriptor is associated with the currently logged in user.
If that means what I think it does (I may need more guidance here) that tells me that other users who log in will not be able to "see" the mutex created under the original user's session, nor will they be able to create a mutex with the same name.
Now, emperical evidence seems to back this up. I used a message box to pop the "LastDLLError" I was receiving, and when another user attempted to launch the application (while it was already running under another user account) I would receive an ERROR_ACCESS_DENIED code. I am OK with testing against this along with the ERROR_ALREADY_EXISTS code and just exiting on either/or. However, this feels sort of hackish and I'm wondering if someone can suggest an alternative. The "right" thing to do seems to be to pass the proper pointer to the CreateMutex function such that any user has the proper permissions to view any existing mutexes (mutices?), but I'm not so sure this is possible without the currently logged in user being an admin (which is unacceptible). Any assistance/guidance is greatly appreciated. Thanks in advance!

You don't need admin priveleges to set security on you own mutexes. Here is a simple demo app that basicly gives Everyone/Full control to the mutex.
Option Explicit
Private Const STANDARD_RIGHTS_REQUIRED As Long = &HF0000
Private Const SYNCHRONIZE As Long = &H100000
Private Const MUTANT_QUERY_STATE As Long = &H1
Private Const MUTANT_ALL_ACCESS As Long = (STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or MUTANT_QUERY_STATE)
Private Const SECURITY_DESCRIPTOR_REVISION As Long = 1
Private Const DACL_SECURITY_INFORMATION As Long = 4
Private Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" (lpMutexAttributes As Any, ByVal bInitialOwner As Long, ByVal lpName As String) As Long
Private Declare Function OpenMutex Lib "kernel32" Alias "OpenMutexA" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal lpName As String) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function InitializeSecurityDescriptor Lib "advapi32.dll" (pSecurityDescriptor As Any, ByVal dwRevision As Long) As Long
Private Declare Function SetSecurityDescriptorDacl Lib "advapi32.dll" (pSecurityDescriptor As Any, ByVal bDaclPresent As Long, pDacl As Any, ByVal bDaclDefaulted As Long) As Long
Private Declare Function SetKernelObjectSecurity Lib "advapi32.dll" (ByVal Handle As Long, ByVal SecurityInformation As Long, pSecurityDescriptor As SECURITY_DESCRIPTOR) As Long
Private Type SECURITY_DESCRIPTOR
Revision As Byte
Sbz1 As Byte
Control As Long
Owner As Long
Group As Long
pSacl As Long
pDacl As Long
End Type
Private Const MUTEX_NAME As String = "Global\20b70e57-1c2e-4de9-99e5-20f3961e6812"
Private m_hCurrentMutex As Long
Private Sub Form_Load()
Dim hMutex As Long
Dim uSec As SECURITY_DESCRIPTOR
hMutex = OpenMutex(MUTANT_ALL_ACCESS, 0, MUTEX_NAME)
If hMutex <> 0 Then
Call CloseHandle(hMutex)
MsgBox "Already running", vbExclamation
Unload Me
Exit Sub
End If
m_hCurrentMutex = CreateMutex(ByVal 0&, 1, MUTEX_NAME)
Call InitializeSecurityDescriptor(uSec, SECURITY_DESCRIPTOR_REVISION)
Call SetSecurityDescriptorDacl(uSec, 1, ByVal 0, 0)
Call SetKernelObjectSecurity(m_hCurrentMutex, DACL_SECURITY_INFORMATION, uSec)
End Sub
Private Sub Form_Unload(Cancel As Integer)
If m_hCurrentMutex <> 0 Then
Call CloseHandle(m_hCurrentMutex)
m_hCurrentMutex = 0
End If
End Sub

I was looking for a similar solution in VB6 late last year. At the time I was unable to find any examples of VB6 apps communicating across the user boundary, so I had to write my own.
See: Interprocess Communication via Semaphores
You can use the class to create and check for a global semaphore which will tell you if your app is already running under any user. I didn't look at the Mutex APIs but their usage is very similar. The GetSecurityDescriptor function is what you'll want to transpose if you've already got some Mutex code written.

I think your instincts are exactly right. I don't know any reason why it wouldn't be safe to infer from ERROR_ACCESS_DENIED that some other process has the mutex, so effectively it's the same as ERROR_ALREADY_EXISTS (in this context.) But at the same time, it doesn't feel quite right.
As you suggest, setting a proper security descriptor is indeed the right way to do it. MSDN says that granting MUTEX_ALL_ACCESS privileges increases the risk that the user will have to be an admin, and I think you do need MUTEX_ALL_ACCESS. But in my experience it works fine for non-admins.
Your question intrigued me enough do a quick test. That means I have some source code, and so here it is:
int wmain(int argc, wchar_t* argv[])
{
ACL *existing_dacl = NULL;
ACL *new_dacl = NULL;
PSECURITY_DESCRIPTOR security_descriptor = NULL;
bool owner = false;
HANDLE mutex = CreateMutex(NULL,FALSE,L"Global\\blah");
if(mutex == NULL)
wprintf(L"CreateMutex failed: 0x%08x\r\n",GetLastError());
if(GetLastError() == ERROR_ALREADY_EXISTS)
wprintf(L"Got handle to existing mutex\r\n");
else
{
wprintf(L"Created new mutex\r\n");
owner = true;
}
if(owner)
{
// Get the DACL on the mutex
HRESULT hr = GetSecurityInfo(mutex,SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,NULL,NULL,
&existing_dacl,NULL,
&security_descriptor);
if(hr != S_OK)
wprintf(L"GetSecurityInfo failed: 0x%08x\r\n",hr);
// Add an ACE to the ACL
EXPLICIT_ACCESSW ace;
memset(&ace,0,sizeof(ace));
ace.grfAccessPermissions = MUTEX_ALL_ACCESS;
ace.grfAccessMode = GRANT_ACCESS;
ace.grfInheritance = NO_INHERITANCE;
ace.Trustee.pMultipleTrustee = NULL;
ace.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
ace.Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ace.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ace.Trustee.ptstrName = L"EVERYONE";
hr = SetEntriesInAcl(1,&ace,existing_dacl,&new_dacl);
if(hr != S_OK)
wprintf(L"SetEntriesInAcl failed: 0x%08x\r\n",hr);
// Set the modified DACL on the mutex
hr = SetSecurityInfo(mutex,SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,NULL,NULL,new_dacl,NULL);
if(hr != S_OK)
wprintf(L"SetSecurityInfo failed: 0x%08x\r\n",hr);
else
wprintf(L"Changed ACL\r\n");
LocalFree(existing_dacl);
LocalFree(new_dacl);
LocalFree(security_descriptor);
}
wprintf(L"Press any key...");
_getch();
CloseHandle(mutex);
return 0;
}

Related

How to find correct values for HWND window handles?

WinRestore,% hwnd([1])
i have found in many programming language the use of hwnd. after searching on google it comes out to be handle. I didnt got more information on this. how programmer knows the value to put in, eg.
Const LB_GETTEXTLEN = &H18A
Const LB_GETTEXT = &H189
Const LB_GETCOUNT = &H18B
&h18a how he known, how will he use this?
this is the example program
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Const LB_GETTEXTLEN = &H18A
Const LB_GETTEXT = &H189
Const LB_GETCOUNT = &H18B
Private Function GetListItems(ByVal hList As Long) As Variant
Dim i As Long, nCount As Long, lItemLength As Long
Dim sItem() As String
nCount = SendMessage(hList, LB_GETCOUNT, 0, ByVal 0&)
For i = 0 To nCount - 1
lItemLength = SendMessage(hList, LB_GETTEXTLEN, i, ByVal 0&)
ReDim Preserve sItem(i)
sItem(i) = String(lItemLength, 0)
Call SendMessage(hList, LB_GETTEXT, i, ByVal sItem(i))
Next i
GetListItems = sItem
End Function
there are many such examples in all different languages but concept will be the same. so i want to learn it. what does it mean and how to use it.
another example from ahk
Gui,2:+hwndhwnd
hwnd(2,hwnd)
Those are all window messages that you can find information about on the MSDN Documentation by googling them. See below links:
LB_GETTEXTLEN
LB_GETTEXT
LB_GETCOUNT
You can find them and other related messages by checking the documentation for the native List Box control.
As for the numbers they're hexadecimal numbers which are (usually) mentioned in the documentation. But since these aren't you'll have to google them and check other websites/forums, or find their values on your own by experimenting with them in C or C++ .
In VB hexadecimal numbers are represented by prepending the number with &H, whereas in C, C++, C# or alike they're prepended with 0x.
In a forms editor each window/control has a hwnd property. For windows not created by your forms package you use the API calls FindWindow (easiest but not reliable) or EnumWindows. Also GetForegroundWindow and GetDesktopWindow.
To find out the value of constants you download the C header files as part of the Windows SDK https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk. It also has the documentation for all these API calls. This is online documentation listing all the windows' functions https://msdn.microsoft.com/en-us/library/windows/desktop/ms633505(v=vs.85).aspx.

VB6 - How to detect a file is finished copying from an external source

My software (written in VB6) needs to import csv files that can be large. Users are using copy/paste to place the files in the input folder.
How can I be sure the files I want to read are fully copied before processing them?
Things I tried :
Compare GetFileSizeString over a span of seconds : doesn't work, I get the final value even if the file has just begun to copy.
FileSystemObject.DateLastModified : same
FileSystemObject.DateLastAccessed : same
FileLen : same
FileDateTime : same
EDIT - Added Samba/Linux Info (from comments):
I'm having a hard time dealing with is from Samba/Linux. I don't know why, but when the file is copied by Samba, the read only attribute doesn't matter.
I've used this method which uses the API to test for exclusive access. I've never tried it on a platform other than Windows so, I guess result may vary. I use this in a module of frequently used methods, but I believe I have included the API calls, types, and constants used.
Private Const ERROR_SHARING_VIOLATION = 32&
Private Const GENERIC_WRITE = &H40000000
Private Const INVALID_HANDLE_VALUE = -1
Private Const OPEN_EXISTING = 3
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, lpSecurityAttributes As SECURITY_ATTRIBUTES, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Any) As Long
Public Function CanOpenExclusive(ByVal vFileName As String) As Boolean
Dim lngResult As Long
Dim udtSA As SECURITY_ATTRIBUTES
On Error GoTo errCanOpenExclusive
If Len(vFileName) > 0 Then
udtSA.nLength = Len(udtSA)
udtSA.bInheritHandle = 1&
udtSA.lpSecurityDescriptor = 0&
lngResult = CreateFile(vFileName, GENERIC_WRITE, 0&, udtSA, OPEN_EXISTING, 0&, 0&)
If lngResult <> INVALID_HANDLE_VALUE Then
Call CloseHandle(lngResult)
CanOpenExclusive = True
Else
Select Case Err.LastDllError 'some errors may indicate the file exists, but there was an error opening it
Case Is = ERROR_SHARING_VIOLATION
CanOpenExclusive = False
Case Else
GoTo errCanOpenExclusive
End Select
End If
End If
Exit Function
errCanOpenExclusive:
Err.Raise Err.Number, Err.Source & ":CanOpenExclusive", Err.Description
End Function
I would use the FileDateTime function to work with to calculate when a date/time value indicating the date and time that a file was created or last modified.
You can read up more on the file system usage HERE.
In the syntax above, pathname is a string expression specifying a valid path (it may optionally include the drive); drive is a string expression specifying a drive letter; and filespec, oldfilespec, and newfilespec are string expressions that specify a file (they may optionally include the drive and path).
Following is a set of functions that can be used with files (all are functions except SetAttr, which is a statement):
Function
Description
Syntax
FileDateTime
Returns a date/time value indicating the date and time that a file was created or last modified.
FileDateTime(filespec)
GetAttr
Returns an integer representing the attributes of a file, directory, or folder
GetAttr(filespec)
SetAttr
Statement that lets you specify the attributes for a file
SetAttr(filespec, attributes)
CurDir$ (or CurDir)
Returns a string that indicates the current path for a specified disk drive. In the syntax on the right, drivename is a string expression that specifies a valid disk drive designation
CurDir$(drivename)
Dir$ (or DIr)
Returns a string that indicates a file or directory matching specified conditions
Dir$(filespec [,attributes])
FileLen
Returns a Long specifying the length of a file in bytes. If the specified file is open when the FileLen function is called, the value returned represents the size of the file immediately before it was opened.
FileLen(pathname)
LOF
Returns a Long representing the size, in bytes, of a file opened using the Open statement.
FileLen(filenumber)

Shutdown computer at specific time programmatically

I need an application with a feature that user should be able to set time of computer automatic shutdown (closing all opened applications) in visual basic 6.
I just need to know is this doable? if yes, what specific topics should I search? honestly, never did system programs in vb just m a db programmer that too of a primitive nature. (so gurus, thanks for understanding ;)
Option Explicit
Declare Function ExitWindowsEx Lib "user32" (ByVal uFlags As Long, ByVal dwReserved As Long) As Long
Public Const EWX_FORCE = 4
Public Const EWX_LOGOFF = 0
Public Const EWX_REBOOT = 2
Public Const EWX_SHUTDOWN = 1
Public Sub Main()
Dim Res As Long
MsgBox ("Your System Will Now Shutdown")
Res = ExitWindowsEx(EWX_SHUTDOWN, 0)
End Sub
source
this does the job as required by you.

CheckTokenMembership in VB6

I'm having a hard time converting this C++ code to VB6 code. I've search the net and haven't found anything. PInvoke.net only has reference to VB.NET code. Here's the code from MSDN:
BOOL IsUserAdmin(VOID)
/*++
Routine Description: This routine returns TRUE if the caller's
process is a member of the Administrators local group. Caller is NOT
expected to be impersonating anyone and is expected to be able to
open its own process and process token.
Arguments: None.
Return Value:
TRUE - Caller has Administrators local group.
FALSE - Caller does not have Administrators local group. --
*/
{
BOOL b;
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
PSID AdministratorsGroup;
b = AllocateAndInitializeSid(
&NtAuthority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&AdministratorsGroup);
if(b)
{
if (!CheckTokenMembership( NULL, AdministratorsGroup, &b))
{
b = FALSE;
}
FreeSid(AdministratorsGroup);
}
return(b);
}
It would be great if somebody can help out in converting this to VB6 code.
Thanks!
EDIT:
I was originally going to use that function but MSDN says:
This function is a wrapper for CheckTokenMembership. It is recommended to call that function directly to determine Administrator group status rather than calling IsUserAnAdmin.
Try this
Option Explicit
Private Const SECURITY_BUILTIN_DOMAIN_RID As Long = &H20
Private Const DOMAIN_ALIAS_RID_ADMINS As Long = &H220
Private Declare Function AllocateAndInitializeSid Lib "advapi32.dll" (pIdentifierAuthority As Any, ByVal nSubAuthorityCount As Byte, ByVal nSubAuthority0 As Long, ByVal nSubAuthority1 As Long, ByVal nSubAuthority2 As Long, ByVal nSubAuthority3 As Long, ByVal nSubAuthority4 As Long, ByVal nSubAuthority5 As Long, ByVal nSubAuthority6 As Long, ByVal nSubAuthority7 As Long, lpPSid As Long) As Long
Private Declare Sub FreeSid Lib "advapi32.dll" (ByVal pSid As Long)
Private Declare Function CheckTokenMembership Lib "advapi32.dll" (ByVal hToken As Long, ByVal pSidToCheck As Long, pbIsMember As Long) As Long
Private Type SID_IDENTIFIER_AUTHORITY
Value(0 To 5) As Byte
End Type
Private Function pvIsAdmin() As Boolean
Dim uAuthNt As SID_IDENTIFIER_AUTHORITY
Dim pSidAdmins As Long
Dim lResult As Long
uAuthNt.Value(5) = 5
If AllocateAndInitializeSid(uAuthNt, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, pSidAdmins) <> 0 Then
If CheckTokenMembership(0, pSidAdmins, lResult) <> 0 Then
pvIsAdmin = (lResult <> 0)
End If
Call FreeSid(pSidAdmins)
End If
End Function
You've posted the MSDN sample code for CheckTokenMembership - it uses CheckTokenMembership to determine whether the user is an administrator.
In VB6 it's easier to use IsUserAnAdmin, which is a wrapper for CheckTokenMembership. The MSDN docs do say IsUserAnAdmin is deprecated, but it's so much easier to call than CheckTokenMembership.
Private Declare Function IsUserAnAdmin Lib "Shell32" Alias "#680" () As Integer
If IsUserAnAdmin() = 0 Then
MsgBox "Not admin"
Else
MsgBox "Admin"
End If
Unless there is a reason to convert the code, use the API
Private Declare Function IsUserAdmin Lib "Shell32" Alias "#680" () As Boolean
Private Sub Form_Load()
If IsUserAdmin Then MsgBox "User is Admin"
End Sub

VB6, File Doesn't Exist, How do I handle Gracefully?

I am testing an application that checks if a file exists across a network. In my testing, I am purposefully pulling the network plug so the file will not be found. The problem is this causes my app to go unresponsive for at least 15 seconds. I have used both the FileExists() and GetAttr() functions in VB6. Does anyone know how to fix this problem? (No, I can't stop using VB6)
Thanks,
Charlie
Unfortunately, VB doesn't make this easy, but luckily the Win32 API does, and it's quite simple to call Win32 functions from within VB.
For the LAN/WAN, you can use a combination of the following Win32 API calls to tell you whether the remote connection exists without having to deal with a network time-out:
Private Declare Function WNetGetConnection Lib "mpr.dll" Alias _
"WNetGetConnectionA" (ByVal lpszLocalName As String, _
ByVal lpszRemoteName As String, ByRef cbRemoteName As Long) As Long
Private Declare Function PathIsNetworkPath Lib "shlwapi.dll" Alias _
"PathIsNetworkPathA" (ByVal pszPath As String) As Long
Private Declare Function PathIsUNC Lib "shlwapi.dll" Alias "PathIsUNCA" _
(ByVal pszPath As String) As Long
For the Internet, you can use the Win32 API call:
Private Declare Function InternetGetConnectedState Lib "wininet.dll" _
(ByRef lpdwFlags As Long, ByVal dwReserved As Long) As Long
Const INTERNET_CONNECTION_MODEM = 1
Const INTERNET_CONNECTION_LAN = 2
Const INTERNET_CONNECTION_PROXY = 4
Const INTERNET_CONNECTION_MODEM_BUSY = 8
This VB site has more discussion on path oriented functions you can call in the Win32 API through VB.
use this too
Dim FlSize as long
flsize=filelen("yourfilepath")
if err.number=53 then msgbox("file not found")
if err.number=78 then msgbox("Path Does no Exist")
I'm not sure you can handle this much more gracefully - if the network is having problems it can take a while for timeouts to indicate that the problem is severe enough that things aren't working.
If VB6 supports threading (I honestly don't recall) you could spin the file open into a background thread, and have the UI allow the user to cancel it (or perform other operations if that makes sense), but that introduces a pretty significant amount of additional complexity.
VB6 has some networking functions that can test to see if the network is connected. You should be able to add in under 'References' the 'NetCon 1.0 Type Library'. This adds for you the NETCONLib. Once implemented, you should be able to test for network connectivity first, then test for the FileExists and GetAttr.
Let me know if this helps!
VB is inherently single threaded, but you can divert work to a COM component to do an asynchronous file check and flag an event when it is done. This way the UI thread stays at responsive at least. Trouble is - this is all theory, I don't know such a component.
But wait! Google just turned up this: Visual Basic 6 Asynchronous File I/O Using the .NET Framework. Does that help, maybe?
Also, they have something similar over at CodeProject: Asynchronous processing - Basics and a walkthrough with VB6/ VB.NET
this code only used for check connection (maybe can help you) for one of your problems :
Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef dwFlags As Long, ByVal dwReserved As Long) As Long
Private Const CONNECT_LAN As Long = &H2
Private Const CONNECT_MODEM As Long = &H1
Private Const CONNECT_PROXY As Long = &H4
Private Const CONNECT_OFFLINE As Long = &H20
Private Const CONNECT_CONFIGURED As Long = &H40
Public Function checknet() As Boolean
Dim Msg As String
If IsWebConnected(Msg) Then
checknet = True
Else
If (Msg = "LAN") Or (Msg = "Offline") Or (Msg = "Configured") Or (Msg = "Proxy") Then
checknet = False
End If
End If
End Function
Private Function IsWebConnected(Optional ByRef ConnType As String) As Boolean
Dim dwFlags As Long
Dim WebTest As Boolean
ConnType = ""
WebTest = InternetGetConnectedState(dwFlags, 0&)
Select Case WebTest
Case dwFlags And CONNECT_LAN: ConnType = "LAN"
Case dwFlags And CONNECT_MODEM: ConnType = "Modem"
Case dwFlags And CONNECT_PROXY: ConnType = "Proxy"
Case dwFlags And CONNECT_OFFLINE: ConnType = "Offline"
Case dwFlags And CONNECT_CONFIGURED: ConnType = "Configured"
Case dwFlags And CONNECT_RAS: ConnType = "Remote"
End Select
IsWebConnected = WebTest
End Function
in your event :
If checknet = False Then
...
else
...
end if
I agree with Will. Something like this is simple to handle with Script.FileSystemObject:
Dim objFSO As New FileSystemObject
If objFSO.FileExists("C:\path\to\your_file.txt") Then
' Do some stuff with the file
Else
' File isn't here...be nice to the user.
EndIf
Accessing files over a network can cause these hangs.
It's been a while, but I remember multi-threading in VB6 being relatively painful to implement. A quick solution would be to have a small .exe (perhaps also coded in VB) that can handle this. You could use DDE for inter-app communication or the ever so easy but kludgey file-based pipe, by which I mean a file that both apps will mutually read/write to handle inter-app communication. Of course, using file-based pipes, depending on the details of this scenario, may simply exaggerate the File I/O lag.
If there's a reasonable degree with which you can predict where the user will be selecting files from, you may consider preemptively caching a directory listing and reading that rather than the file directly - assuming the directory contents aren't expected to change frequently. Note: getting a directory listing over a network will cause the same lag issues as individual file I/O over a network. Keep that in mind.

Resources