-
Notifications
You must be signed in to change notification settings - Fork 9
fix: implement singleton noop IDispatch callback for async WUA methods #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
44a5fe5
8bd1d23
180de69
cb7aece
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| module github.com/ceshihao/windowsupdate | ||
|
|
||
| go 1.23 | ||
| go 1.24 | ||
|
|
||
| require github.com/go-ole/go-ole v1.3.0 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,142 @@ | ||||||||||||||||||||||||||||
| //go:build windows | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||
| Copyright 2026 Zheng Dayu | ||||||||||||||||||||||||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||
| you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||
| You may obtain a copy of the License at | ||||||||||||||||||||||||||||
| http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||
| Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||
| See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||
| limitations under the License. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| package windowsupdate | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||
| "sync/atomic" | ||||||||||||||||||||||||||||
| "syscall" | ||||||||||||||||||||||||||||
| "unsafe" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| "github.com/go-ole/go-ole" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // The asynchronous WUA methods (BeginSearch, BeginDownload, BeginInstall) require | ||||||||||||||||||||||||||||
| // a non-NULL IUnknown* callback argument. Passing NULL (VT_NULL) makes them fail | ||||||||||||||||||||||||||||
| // with DISP_E_TYPEMISMATCH (0x80020005). newNoopDispatch returns a minimal | ||||||||||||||||||||||||||||
| // IDispatch whose Invoke does nothing (returns S_OK): completion is obtained | ||||||||||||||||||||||||||||
| // through the blocking EndXxx methods and progress through IXxxJob.GetProgress(). | ||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||
| // The handler signatures are 100% uintptr because that is required by | ||||||||||||||||||||||||||||
| // syscall.NewCallback. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // noopCallbackVtbl is the COM virtual function table layout for IDispatch. | ||||||||||||||||||||||||||||
| // The order of fields MUST match the IUnknown + IDispatch v-table layout. | ||||||||||||||||||||||||||||
| type noopCallbackVtbl struct { | ||||||||||||||||||||||||||||
| pQueryInterface uintptr | ||||||||||||||||||||||||||||
| pAddRef uintptr | ||||||||||||||||||||||||||||
| pRelease uintptr | ||||||||||||||||||||||||||||
| pGetTypeInfoCount uintptr | ||||||||||||||||||||||||||||
| pGetTypeInfo uintptr | ||||||||||||||||||||||||||||
| pGetIDsOfNames uintptr | ||||||||||||||||||||||||||||
| pInvoke uintptr | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // noopCallback is a stateless dummy IDispatch implementation. lpVtbl MUST be | ||||||||||||||||||||||||||||
| // the first field because the COM interface pointer points directly to it. | ||||||||||||||||||||||||||||
| type noopCallback struct { | ||||||||||||||||||||||||||||
| lpVtbl *noopCallbackVtbl | ||||||||||||||||||||||||||||
| ref int32 | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // HRESULT values as uintptr (only the low 32 bits are significant). | ||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||
| hrSOK = uintptr(0x00000000) | ||||||||||||||||||||||||||||
| hrEPointer = uintptr(0x80004003) | ||||||||||||||||||||||||||||
| hrENoInterface = uintptr(0x80004002) | ||||||||||||||||||||||||||||
| hrENotImpl = uintptr(0x80004001) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To comply with COM standards,
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncQueryInterface(this, iid, ppvObject uintptr) uintptr { | ||||||||||||||||||||||||||||
| if ppvObject == 0 { | ||||||||||||||||||||||||||||
| return hrEPointer | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| out := (*uintptr)(unsafe.Pointer(ppvObject)) | ||||||||||||||||||||||||||||
| if iid == 0 { | ||||||||||||||||||||||||||||
| *out = 0 | ||||||||||||||||||||||||||||
| return hrENoInterface | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| guid := (*ole.GUID)(unsafe.Pointer(iid)) | ||||||||||||||||||||||||||||
| if ole.IsEqualGUID(guid, ole.IID_IUnknown) || ole.IsEqualGUID(guid, ole.IID_IDispatch) { | ||||||||||||||||||||||||||||
| atomic.AddInt32(&globalNoop.ref, 1) | ||||||||||||||||||||||||||||
| *out = this | ||||||||||||||||||||||||||||
| return hrSOK | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
ceshihao marked this conversation as resolved.
|
||||||||||||||||||||||||||||
| *out = 0 | ||||||||||||||||||||||||||||
| return hrENoInterface | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In COM, func ncQueryInterface(this, iid, ppvObject uintptr) uintptr {
if ppvObject == 0 {
return hrEPointer
}
if iid == 0 {
*(*uintptr)(unsafe.Pointer(ppvObject)) = 0
return hrENoInterface
}
guid := (*ole.GUID)(unsafe.Pointer(iid))
out := (*uintptr)(unsafe.Pointer(ppvObject))
if ole.IsEqualGUID(guid, ole.IID_IUnknown) || ole.IsEqualGUID(guid, ole.IID_IDispatch) {
p := (*noopCallback)(unsafe.Pointer(this))
atomic.AddInt32(&p.ref, 1)
*out = this
return hrSOK
}
*out = 0
return hrENoInterface
} |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncAddRef(this uintptr) uintptr { | ||||||||||||||||||||||||||||
| return uintptr(uint32(atomic.AddInt32(&globalNoop.ref, 1))) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncRelease(this uintptr) uintptr { | ||||||||||||||||||||||||||||
| // Singleton object: it is never actually freed even if the count reaches | ||||||||||||||||||||||||||||
| // zero. We still maintain the counter so the value returned to the COM | ||||||||||||||||||||||||||||
| // caller is meaningful. | ||||||||||||||||||||||||||||
| return uintptr(uint32(atomic.AddInt32(&globalNoop.ref, -1))) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
ceshihao marked this conversation as resolved.
Comment on lines
+82
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of referencing the global func ncAddRef(this uintptr) uintptr {
p := (*noopCallback)(unsafe.Pointer(this))
return uintptr(uint32(atomic.AddInt32(&p.ref, 1)))
}
func ncRelease(this uintptr) uintptr {
// Singleton object: it is never actually freed even if the count reaches
// zero. We still maintain the counter so the value returned to the COM
// caller is meaningful.
p := (*noopCallback)(unsafe.Pointer(this))
return uintptr(uint32(atomic.AddInt32(&p.ref, -1)))
} |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncGetTypeInfoCount(this, pctinfo uintptr) uintptr { | ||||||||||||||||||||||||||||
| if pctinfo != 0 { | ||||||||||||||||||||||||||||
| *(*uint32)(unsafe.Pointer(pctinfo)) = 0 | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return hrSOK | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncGetTypeInfo(this, iTInfo, lcid, ppTInfo uintptr) uintptr { | ||||||||||||||||||||||||||||
| return hrENotImpl | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func ncGetIDsOfNames(this, riid, rgszNames, cNames, lcid, rgDispId uintptr) uintptr { | ||||||||||||||||||||||||||||
| return hrENotImpl | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // ncInvoke : no-op body. WUA calls DISPID 0 on progress/completion; we ignore it | ||||||||||||||||||||||||||||
| // and return S_OK. Completion is detected through EndXxx (blocking). | ||||||||||||||||||||||||||||
| func ncInvoke(this, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr { | ||||||||||||||||||||||||||||
| if pVarResult != 0 { | ||||||||||||||||||||||||||||
| v := (*ole.VARIANT)(unsafe.Pointer(pVarResult)) | ||||||||||||||||||||||||||||
| v.VT = ole.VT_EMPTY | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return hrSOK | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
ceshihao marked this conversation as resolved.
Comment on lines
+110
to
+116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using raw pointer dereferencing with magic numbers (
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||
| noopOnce sync.Once | ||||||||||||||||||||||||||||
| globalNoop *noopCallback | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // newNoopDispatch returns a pointer to a global singleton IDispatch usable as | ||||||||||||||||||||||||||||
| // a WUA callback. Because the callback is completely stateless, a single | ||||||||||||||||||||||||||||
| // instance can be safely shared across all async calls. This avoids the | ||||||||||||||||||||||||||||
| // unbounded memory leak that would result from allocating a new callback on | ||||||||||||||||||||||||||||
| // every invocation and pinning it in a global slice. | ||||||||||||||||||||||||||||
| func newNoopDispatch() *ole.IDispatch { | ||||||||||||||||||||||||||||
| noopOnce.Do(func() { | ||||||||||||||||||||||||||||
| vtbl := &noopCallbackVtbl{ | ||||||||||||||||||||||||||||
| pQueryInterface: syscall.NewCallback(ncQueryInterface), | ||||||||||||||||||||||||||||
| pAddRef: syscall.NewCallback(ncAddRef), | ||||||||||||||||||||||||||||
| pRelease: syscall.NewCallback(ncRelease), | ||||||||||||||||||||||||||||
| pGetTypeInfoCount: syscall.NewCallback(ncGetTypeInfoCount), | ||||||||||||||||||||||||||||
| pGetTypeInfo: syscall.NewCallback(ncGetTypeInfo), | ||||||||||||||||||||||||||||
| pGetIDsOfNames: syscall.NewCallback(ncGetIDsOfNames), | ||||||||||||||||||||||||||||
| pInvoke: syscall.NewCallback(ncInvoke), | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| globalNoop = &noopCallback{lpVtbl: vtbl, ref: 1} | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| return (*ole.IDispatch)(unsafe.Pointer(globalNoop)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| //go:build !windows | ||
|
|
||
| /* | ||
| Copyright 2026 Zheng Dayu | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package windowsupdate | ||
|
|
||
| import "github.com/go-ole/go-ole" | ||
|
|
||
| // newNoopDispatch is a no-op stub on non-Windows platforms. | ||
| // The COM async methods are only functional on Windows. | ||
| func newNoopDispatch() *ole.IDispatch { | ||
| return nil | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.