forked from tannerhelland/PhotoDemon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pdPipe.cls
331 lines (250 loc) · 20.2 KB
/
pdPipe.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "pdPipe"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'***************************************************************************
'Simple (Named) Pipe class
'Copyright 2020-2022 by Tanner Helland
'Created: 14/September/20
'Last updated: 14/September/20
'Last update: initial build
'
'This class is currently used to implement single-session behavior in PD. If the user has set
' this preference, parallel PD sessions will detect the original session, and forward their
' command-line parameters via a named pipe implemented through this class.
'
'When this class references MSDN, it typically refers to the API name in question, or to the
' article on named pipes (link good as of September 2020):
' https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes
'
'Unless otherwise noted, all source code in this file is shared under a simplified BSD license.
' Full license details are available in the LICENSE.md file, or at https://photodemon.org/license/
'
'***************************************************************************
Option Explicit
'The type of raised event depends on the pipe's mode (currently only byte mode is implemented).
Public Event BytesArrived(ByVal initStreamPosition As Long, ByVal numOfBytes As Long)
Public Enum PD_PipeOpenMode
'One of these modes *MUST* be specified or CreateNamedPipe will fail. (MSDN: The function fails if dwOpenMode specifies anything other than 0 or the flags listed in the following tables. This parameter must specify one of the following pipe access modes. The same mode must be specified for each instance of the pipe.)
pom_BiDirectional = &H3& 'The pipe is bi-directional; both server and client processes can read from and write to the pipe. This mode gives the server the equivalent of GENERIC_READ and GENERIC_WRITE access to the pipe. The client can specify GENERIC_READ or GENERIC_WRITE, or both, when it connects to the pipe using the CreateFile function.
pom_ClientToServer = &H1& 'The flow of data in the pipe goes from client to server only. This mode gives the server the equivalent of GENERIC_READ access to the pipe. The client must specify GENERIC_WRITE access when connecting to the pipe. If the client must read pipe settings by calling the GetNamedPipeInfo or GetNamedPipeHandleState functions, the client must specify GENERIC_WRITE and FILE_READ_ATTRIBUTES access when connecting to the pipe.
pom_ServerToClient = &H2& 'The flow of data in the pipe goes from server to client only. This mode gives the server the equivalent of GENERIC_WRITE access to the pipe. The client must specify GENERIC_READ access when connecting to the pipe. If the client must change pipe settings by calling the SetNamedPipeHandleState function, the client must specify GENERIC_READ and FILE_WRITE_ATTRIBUTES access when connecting to the pipe.
'These parameters are *OPTIONAL*. (MSDN: This parameter can also include one or more of the following flags, which enable the write-through and overlapped modes. These modes can be different for different instances of the same pipe.)
pom_FlagFirstPipeInstance = &H80000 'If you attempt to create multiple instances of a pipe with this flag, creation of the first instance succeeds, but creation of the next instance fails with ERROR_ACCESS_DENIED.
pom_FlagWriteThrough = &H80000000 'Write-through mode is enabled. This mode affects only write operations on byte-type pipes and, then, only when the client and server processes are on different computers. If this mode is enabled, functions writing to a named pipe do not return until the data written is transmitted across the network and is in the pipe's buffer on the remote computer. If this mode is not enabled, the system enhances the efficiency of network operations by buffering data until a minimum number of bytes accumulate or until a maximum time elapses.
pom_FlagOverlapped = &H40000000 'Overlapped mode is enabled. If this mode is enabled, functions performing read, write, and connect operations that may take a significant time to be completed can return immediately. This mode enables the thread that started the operation to perform other operations while the time-consuming operation executes in the background. For example, in overlapped mode, a thread can handle simultaneous input and output (I/O) operations on multiple instances of a pipe or perform simultaneous read and write operations on the same pipe handle. If overlapped mode is not enabled, functions performing read, write, and connect operations on the pipe handle do not return until the operation is finished. The ReadFileEx and WriteFileEx functions can only be used with a pipe handle in overlapped mode. The ReadFile, WriteFile, ConnectNamedPipe, and TransactNamedPipe functions can execute either synchronously or as overlapped operations.
'These security flags are *OPTIONAL*. (MSDN: This parameter can include any combination of the following security access modes. These modes can be different for different instances of the same pipe.)
pom_SecurityWriteDAC = &H40000 'The caller will have write access to the named pipe's discretionary access control list (ACL).
pom_SecurityWriteOwner = &H80000 'The caller will have write access to the named pipe's owner.
pom_SecurityAccessSystemSecurity = &H1000000 'The caller will have write access to the named pipe's SACL. For more information, see Access-Control Lists (ACLs) and SACL Access Right.
End Enum
#If False Then
Private Const pom_BiDirectional = &H3&, pom_ClientToServer = &H1&, pom_ServerToClient = &H2&
Private Const pom_FlagFirstPipeInstance = &H80000, pom_FlagWriteThrough = &H80000000, pom_FlagOverlapped = &H40000000
Private Const pom_SecurityWriteDAC = &H40000, pom_SecurityWriteOwner = &H80000, pom_SecurityAccessSystemSecurity = &H1000000
#End If
Public Enum PD_PipeMode
'The function fails if dwPipeMode specifies anything other than 0 or the flags listed in the following tables.
'One of the following type modes can be specified. The same type mode must be specified for each instance of the pipe.
pm_TypeByte = &H0& 'Data is written to the pipe as a stream of bytes. This mode cannot be used with PIPE_READMODE_MESSAGE. The pipe does not distinguish bytes written during different write operations.
pm_TypeMessage = &H4& 'Data is written to the pipe as a stream of messages. The pipe treats the bytes written during each write operation as a message unit. The GetLastError function returns ERROR_MORE_DATA when a message is not read completely. This mode can be used with either PIPE_READMODE_MESSAGE or PIPE_READMODE_BYTE.
'One of the following read modes can be specified. Different instances of the same pipe can specify different read modes.
pm_ReadModeByte = &H0& 'Data is read from the pipe as a stream of bytes. This mode can be used with either PIPE_TYPE_MESSAGE or PIPE_TYPE_BYTE.
pm_ReadModeMessage = &H2& 'Data is read from the pipe as a stream of messages. This mode can be only used if PIPE_TYPE_MESSAGE is also specified.
'One of the following wait modes can be specified. Different instances of the same pipe can specify different wait modes.
pm_ModeWait = &H0& 'Blocking mode is enabled. When the pipe handle is specified in the ReadFile, WriteFile, or ConnectNamedPipe function, the operations are not completed until there is data to read, all data is written, or a client is connected. Use of this mode can mean waiting indefinitely in some situations for a client process to perform an action.
pm_ModeDontWait = &H1& 'Nonblocking mode is enabled. In this mode, ReadFile, WriteFile, and ConnectNamedPipe always return immediately.
'IMPORTANT: Note that nonblocking mode is supported for compatibility with Microsoft LAN Manager version 2.0 and should not be used to achieve asynchronous I/O with named pipes. For more information on asynchronous pipe I/O, see Synchronous and Overlapped Input and Output.
'One of the following remote-client modes can be specified. Different instances of the same pipe can specify different remote-client modes.
pm_RemoteClientsAccept = &H0& 'Connections from remote clients can be accepted and checked against the security descriptor for the pipe.
pm_RemoteClientsReject = &H8& 'Connections from remote clients are automatically rejected.
End Enum
#If False Then
Private Const pm_TypeByte = &H0&, pm_TypeMessage = &H4&
Private Const pm_ReadModeByte = &H0&, pm_ReadModeMessage = &H2&
Private Const pm_ModeWait = &H0&, pm_ModeDontWait = &H1&
Private Const pm_RemoteClientsAccept = &H0&, pm_RemoteClientsReject = &H8&
#End If
Private Const INVALID_HANDLE_VALUE As Long = -1
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
'There is no -W CreateNamedPipe, so we use -A wrappers for CreateFile as well
Private Declare Function CreateFileA Lib "kernel32" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long
Private Declare Function CreateNamedPipeA Lib "kernel32" (ByVal lpName As String, ByVal dwOpenMode As PD_PipeOpenMode, ByVal dwPipeMode As Long, ByVal nMaxInstances As Long, ByVal nOutBufferSize As Long, ByVal nInBufferSize As Long, ByVal nDefaultTimeOut As Long, ByVal lpSecurityAttributes As Long) As Long
Private Declare Function DisconnectNamedPipe Lib "kernel32" (ByVal hNamedPipe As Long) As Long
'Private Declare Function GetNamedPipeInfo Lib "kernel32" (ByVal hNamedPipe As Long, ByRef lpFlags As Long, ByRef outBufferSize As Long, ByRef inBufferSize As Long, ByRef nMaxInstances As Long) As Long
Private Declare Function PeekNamedPipe Lib "kernel32" (ByVal hNamedPipe As Long, ByVal lpDstBuffer As Long, ByVal nBufSize As Long, ByVal ptrToOutBytes As Long, ByRef lpTotalBytesAvail As Long, ByVal lpBytesLeftThisMessage As Long) As Long
Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, ByVal lpBuf As Long, ByVal nNumberOfBytesToRead As Long, ByRef lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
'Private Declare Function WaitNamedPipeA Lib "kernel32" (ByVal lpName As String, ByVal nTimeOut As Long) As Long
Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, ByVal ptrToSourceBuffer As Long, ByVal nNumberOfBytesToWrite As Long, ByRef lpNumberOfBytesWritten As Long, ByVal ptrToOverlappedStruct As Long) As Long
'non-zero if a pipe is active
Private m_Pipe As Long, m_PipeName As String
'Type of pipe; we use different functions to assess pipe state depending on pipe type
Private m_PipeIsInByteMode As Boolean
'Waiting for pipe responses is currently handled by timer
Private WithEvents m_Timer As pdTimer
Attribute m_Timer.VB_VarHelpID = -1
'Here's the cool thing about doing this inside PD - instead of forcing the caller to manually
' manage a buffer, we instead ask them to pass an instantiated pdStream object to us. They can
' back the stream however they want (file, memory-mapped file, just memory, whatever), and we'll
' fill the stream with data as it arrives, then raise events to notify the caller of the new data.
' It's an elegant solution to a complex problem.
Private m_Stream As pdStream
'Create a pipe with the given name.
' pipeName: must be passed. Any Windows-required prefixes are handled automatically, so don't worry about adding them yourself.
Friend Function CreatePipe(ByRef pipeName As String, ByRef instantiatedDstStream As pdStream, ByVal pipeOpenMode As PD_PipeOpenMode, ByVal pipeMode As PD_PipeMode, Optional ByVal maxInstances As Long = 1, Optional ByVal inputBufferSize As Long = 1024, Optional ByVal outputBufferSize As Long = 1024) As Boolean
If (m_Pipe <> 0) Then Me.ClosePipe
m_PipeName = AppendPipeNameBits(pipeName)
'Store pipe mode; this affects some read/write functions
m_PipeIsInByteMode = ((pipeMode And pm_TypeByte) = pm_TypeByte)
'Basic validation on some parameters
If (maxInstances < 1) Then maxInstances = 1
If (maxInstances > 255) Then maxInstances = 255
'Attempt to create the pipe
m_Pipe = CreateNamedPipeA(m_PipeName, pipeOpenMode, pipeMode, maxInstances, outputBufferSize, inputBufferSize, 0&, 0&)
CreatePipe = (m_Pipe <> 0) And (m_Pipe <> INVALID_HANDLE_VALUE)
If CreatePipe Then
PDDebug.LogAction "Successfully created pipe: " & m_Pipe & ", " & Err.LastDllError
'Point our stream reference at the caller's stream; all pipe data will be placed there
' as it arrives.
Set m_Stream = instantiatedDstStream
Else
PDDebug.LogAction "WARNING! pdPipe.CreatePipe failed to create pipe: " & m_PipeName & "; lastDLLError: " & Err.LastDllError
End If
End Function
'Connect to an already existing pipe instance.
' IMPORTANTLY, you need to ensure that *all* pipe details (not just name) match the initial pipe settings.
' This is a Microsoft requirement, not a PD one. Mismatched settings can result in connection failure
' (or worse).
'
'pipeName: must be passed. Any Windows-required prefixes are handled automatically, so don't worry about adding them yourself.
Friend Function ConnectToExistingPipe(ByRef pipeName As String, Optional ByVal needWriteAccess As Boolean = True, Optional ByVal needReadAccess As Boolean = True, Optional ByVal waitUntilAvailable As Boolean = True) As Boolean
m_PipeName = AppendPipeNameBits(pipeName)
'Determine connection flags based on access requirements
Const GENERIC_READ As Long = &H80000000
Const GENERIC_WRITE As Long = &H40000000
Dim cfFlags As Long
If needReadAccess Then cfFlags = cfFlags Or GENERIC_READ
If needWriteAccess Then cfFlags = cfFlags Or GENERIC_WRITE
'Attempt to connect
Const OPEN_EXISTING As Long = 3, FILE_ATTRIBUTE_NORMAL As Long = &H80&
m_Pipe = CreateFileA(m_PipeName, cfFlags, 0&, 0&, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0&)
ConnectToExistingPipe = (m_Pipe <> 0) And (m_Pipe <> INVALID_HANDLE_VALUE)
If (Not ConnectToExistingPipe) Then PDDebug.LogAction "WARNING! pdPipe.ConnectToExistingPipe failed to connect to pipe: " & pipeName & "; lastDLLError: " & Err.LastDllError
'TODO: implement wait for pipe via WaitNamedPipeA?
End Function
'Server instances only: disconnect this pipe instance from a connected (or recently connected
' but now closed) pipe client.
Friend Function DisconnectFromClient() As Boolean
If (m_Pipe <> 0) Then DisconnectFromClient = (DisconnectNamedPipe(m_Pipe) <> 0)
End Function
'Once connected to a pipe, call this to pass binary data.
Friend Function WriteDataToPipe(ByVal ptrToData As Long, ByVal sizeOfData As Long) As Boolean
If (m_Pipe <> 0) Then
Dim numBytesWritten As Long
WriteDataToPipe = (WriteFile(m_Pipe, ptrToData, sizeOfData, numBytesWritten, 0&) <> 0)
If WriteDataToPipe Then
'Ensure written byte count was the same
Dim totalBytesWritten As Long
totalBytesWritten = numBytesWritten
Do While (totalBytesWritten < sizeOfData)
'Write again from the current offset
numBytesWritten = 0
WriteDataToPipe = (WriteFile(m_Pipe, ptrToData + totalBytesWritten, sizeOfData - totalBytesWritten, numBytesWritten, 0&) <> 0)
'Quit on failure
If (Not WriteDataToPipe) Then
PDDebug.LogAction "WARNING! pdPipe.WriteDataToPipe failed to write additional data; " & totalBytesWritten & " of " & sizeOfData & " bytes written"
Exit Function
End If
'Update counts and offsets
totalBytesWritten = totalBytesWritten + numBytesWritten
Loop
'TODO: add an option to force a flush op here
Else
PDDebug.LogAction "WARNING! pdPipe.WriteDataToPipe failed to write."
End If
Else
PDDebug.LogAction "WARNING! pdPipe.WriteDataToPipe can't write to a pipe that doesn't exist!"
End If
End Function
'Once a pipe has been created, call this function to start waiting for a response.
Friend Sub Server_WaitForResponse(Optional ByVal waitIntervalInMS As Long = 1000)
'Start the wait timer
Set m_Timer = New pdTimer
m_Timer.Interval = waitIntervalInMS
m_Timer.StartTimer
End Sub
'Manually shutdown our pipe (if one exists)
Friend Sub ClosePipe()
Set m_Stream = Nothing
Set m_Timer = Nothing
If (m_Pipe <> 0) Then
If (CloseHandle(m_Pipe) = 0) Then PDDebug.LogAction "WARNING! pdPipe failed to close its handle; last error was " & Err.LastDllError()
m_Pipe = 0
m_PipeName = vbNullString
End If
End Sub
'Convert a bare pipe name to a Windows-formatted local pipe name (with Win 10 workarounds)
Private Function AppendPipeNameBits(ByRef srcName As String) As String
'Per MSDN:
' "The entire pipe name string can be up to 256 characters long. Pipe names are not case-sensitive."
' "The pipe server cannot create a pipe on another computer, so CreateNamedPipe must use a period for
' the server name, as shown in the following example: \\.\pipe\PipeName"
' "Windows 10, version 1709: Pipes are only supported within an app-container; ie, from one
' UWP process to another UWP process that's part of the same app. Also, named pipes must use
' the syntax "\.\pipe\LOCAL" for the pipe name."
AppendPipeNameBits = "\\.\pipe\"
If (OS.GetWin10Build() >= 16299) Or (Not OS.IsProgramCompiled()) Then AppendPipeNameBits = AppendPipeNameBits & "LOCAL\"
AppendPipeNameBits = AppendPipeNameBits & srcName
End Function
Private Sub Class_Terminate()
ClosePipe
End Sub
Private Sub m_Timer_Timer()
'In the future, handling may need to vary by pipe mode... but for now, we use the same
' approach for both message- and byte-based pipes
Dim numBytesAvailable As Long, peekResult As Long
peekResult = PeekNamedPipe(m_Pipe, 0&, 0&, 0&, numBytesAvailable, 0&)
'Peek at the pipe
If (peekResult <> 0) Then
'Anything for us?
If (numBytesAvailable > 0) Then
Dim initStreamPosition As Long
'Prep the stream to receive the new data
m_Stream.EnsureBufferSpaceAvailable numBytesAvailable
initStreamPosition = m_Stream.GetPosition()
Dim numBytesRead As Long
If (ReadFile(m_Pipe, m_Stream.Peek_PointerOnly(peekLength:=numBytesAvailable), numBytesAvailable, numBytesRead, 0&) <> 0) Then
'Ensure retrieved byte amount was accurate
If (numBytesAvailable <> numBytesRead) Then PDDebug.LogAction "WARNING! pdPipe.Timer got a weird result for bytes read."
'Manually increase the stream's size to account for the write,
' and move the pointer accordingly.
m_Stream.SetSizeExternally initStreamPosition + numBytesRead
m_Stream.SetPosition numBytesRead, FILE_CURRENT
'Notify the caller of the update
RaiseEvent BytesArrived(initStreamPosition, numBytesRead)
Else
PDDebug.LogAction "WARNING! pdPipe.Timer failed to read."
End If
End If
Else
Dim errLast As Long
errLast = Err.LastDllError
If (errLast = 230) Then
'No clients connected yet; this is fine, and 99% of the time it's the expected result.
ElseIf (errLast = 109) Then
'A client *was* connected, but isn't now; this is also fine, and will occur after
' a second (or third, etc) instance connects, then disconnects
Else
PDDebug.LogAction "WARNING! pdPipe.Timer failed to peek pipe: " & m_Pipe & ", lastDllError: " & Err.LastDllError
End If
End If
End Sub