WebKit Bugzilla
New
Browse
Search+
Log In
×
Sign in with GitHub
or
Remember my login
Create Account
·
Forgot Password
Forgotten password account recovery
[patch]
Minor style tweak
bug-231710-20211013174045.patch (text/plain), 20.16 KB, created by
Wenson Hsieh
on 2021-10-13 17:40:46 PDT
(
hide
)
Description:
Minor style tweak
Filename:
MIME Type:
Creator:
Wenson Hsieh
Created:
2021-10-13 17:40:46 PDT
Size:
20.16 KB
patch
obsolete
>Subversion Revision: 284092 >diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog >index 213128fa026dfbc3695a5adadfe6e7eb40811aa8..19d6c20a57d82c104a15d222675801de58f5814e 100644 >--- a/Source/WebKit/ChangeLog >+++ b/Source/WebKit/ChangeLog >@@ -1,3 +1,54 @@ >+2021-10-13 Wenson Hsieh <wenson_hsieh@apple.com> >+ >+ [JS IPC] Make it possible to establish a stream connection using the JS IPC testing API >+ https://bugs.webkit.org/show_bug.cgi?id=231710 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ Work towards fixing IPCTestingAPI.CanReceiveIPCSemaphore and IPCTestingAPI.CanReceiveSharedMemory, which broke >+ after I refactored RemoteRenderingBackend to be a IPC stream message receiver (instead of a standard IPC message >+ receiver). To achieve this, we introduce two new JS wrapper objects to encapsulate an IPC stream client >+ connection, as well as the stream buffer used to service that connection. >+ >+ This allows us to establish a streaming IPC connection from the web process to the GPU process via JS IPC API, >+ like so: >+ >+ ``` >+ const bufferSize = 1 << 16; >+ const streamConnection = IPC.createStreamClientConnection('GPU', bufferSize); >+ IPC.sendMessage('GPU', 0, IPC.messages.GPUConnectionToWebProcess_CreateRenderingBackend.name, [ >+ { type: 'RemoteRenderingBackendCreationParameters', 'identifier': 123, 'pageProxyID': IPC.webPageProxyID, 'pageID': IPC.pageID }, >+ { type: 'StreamConnectionBuffer', value: streamConnection.streamBuffer() } >+ ]); >+ ``` >+ >+ * WebProcess/WebPage/IPCTestingAPI.cpp: >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::create): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::connection): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::JSIPCStreamClientConnection): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::create): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::encode const): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::JSIPCStreamConnectionBuffer): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::createJSWrapper): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::wrapperClass): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::unwrap): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::toWrapped): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::initialize): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::finalize): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::staticFunctions): >+ (WebKit::IPCTestingAPI::JSIPCStreamClientConnection::streamBuffer): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::createJSWrapper): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::wrapperClass): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::unwrap): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::toWrapped): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::initialize): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::finalize): >+ (WebKit::IPCTestingAPI::JSIPCStreamConnectionBuffer::staticFunctions): >+ (WebKit::IPCTestingAPI::JSIPC::staticFunctions): >+ (WebKit::IPCTestingAPI::encodeStreamConnectionBuffer): >+ (WebKit::IPCTestingAPI::encodeArgument): >+ (WebKit::IPCTestingAPI::JSIPC::createStreamClientConnection): >+ > 2021-10-13 Wenson Hsieh <wenson_hsieh@apple.com> > > Support both VK and VKC-prefixed SPI when softlinking VisionKitCore classes >diff --git a/Source/WebKit/WebProcess/WebPage/IPCTestingAPI.cpp b/Source/WebKit/WebProcess/WebPage/IPCTestingAPI.cpp >index 60054aa93e3b5a27b7c58b231a7daaabfc38b444..24987c7dd383dcdcea6c963f4789d8693c6dfe06 100644 >--- a/Source/WebKit/WebProcess/WebPage/IPCTestingAPI.cpp >+++ b/Source/WebKit/WebProcess/WebPage/IPCTestingAPI.cpp >@@ -36,6 +36,8 @@ > #include "MessageArgumentDescriptions.h" > #include "NetworkProcessConnection.h" > #include "RemoteRenderingBackendCreationParameters.h" >+#include "StreamClientConnection.h" >+#include "StreamConnectionBuffer.h" > #include "WebCoreArgumentCoders.h" > #include "WebFrame.h" > #include "WebPage.h" >@@ -93,6 +95,61 @@ private: > IPC::Semaphore m_semaphore; > }; > >+class JSIPCStreamClientConnection : public RefCounted<JSIPCStreamClientConnection> { >+public: >+ static Ref<JSIPCStreamClientConnection> create(IPC::Connection& connection, size_t bufferSize) >+ { >+ return adoptRef(*new JSIPCStreamClientConnection(connection, bufferSize)); >+ } >+ >+ JSObjectRef createJSWrapper(JSContextRef); >+ static JSIPCStreamClientConnection* toWrapped(JSContextRef, JSValueRef); >+ >+ IPC::StreamClientConnection& connection() { return m_streamConnection; } >+ >+private: >+ JSIPCStreamClientConnection(IPC::Connection& connection, size_t bufferSize) >+ : m_streamConnection { connection, bufferSize } >+ { } >+ >+ static JSClassRef wrapperClass(); >+ static JSIPCStreamClientConnection* unwrap(JSObjectRef); >+ static void initialize(JSContextRef, JSObjectRef); >+ static void finalize(JSObjectRef); >+ >+ static const JSStaticFunction* staticFunctions(); >+ static JSValueRef streamBuffer(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); >+ >+ IPC::StreamClientConnection m_streamConnection; >+}; >+ >+class JSIPCStreamConnectionBuffer : public RefCounted<JSIPCStreamConnectionBuffer> { >+public: >+ static Ref<JSIPCStreamConnectionBuffer> create(JSIPCStreamClientConnection& streamConnection) >+ { >+ return adoptRef(*new JSIPCStreamConnectionBuffer(streamConnection)); >+ } >+ >+ JSObjectRef createJSWrapper(JSContextRef); >+ static JSIPCStreamConnectionBuffer* toWrapped(JSContextRef, JSValueRef); >+ >+ void encode(IPC::Encoder& encoder) const { m_streamConnection->connection().streamBuffer().encode(encoder); } >+ >+private: >+ JSIPCStreamConnectionBuffer(JSIPCStreamClientConnection& streamConnection) >+ : m_streamConnection(streamConnection) >+ { } >+ >+ static JSClassRef wrapperClass(); >+ static JSIPCStreamConnectionBuffer* unwrap(JSObjectRef); >+ static void initialize(JSContextRef, JSObjectRef); >+ static void finalize(JSObjectRef); >+ >+ static const JSStaticFunction* staticFunctions(); >+ >+ Ref<JSIPCStreamClientConnection> m_streamConnection; >+}; >+ > class JSSharedMemory : public RefCounted<JSSharedMemory> { > public: > static Ref<JSSharedMemory> create(size_t size) >@@ -185,6 +242,7 @@ private: > static JSValueRef sendMessage(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); > static JSValueRef sendSyncMessage(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); > >+ static JSValueRef createStreamClientConnection(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); > static JSValueRef createSemaphore(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); > static JSValueRef createSharedMemory(JSContextRef, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); > >@@ -281,6 +339,132 @@ const JSStaticFunction* JSIPCSemaphore::staticFunctions() > return functions; > } > >+JSObjectRef JSIPCStreamClientConnection::createJSWrapper(JSContextRef context) >+{ >+ auto* globalObject = toJS(context); >+ auto& vm = globalObject->vm(); >+ JSC::JSLockHolder lock(vm); >+ auto scope = DECLARE_CATCH_SCOPE(vm); >+ JSObjectRef wrapperObject = JSObjectMake(toGlobalRef(globalObject), wrapperClass(), this); >+ scope.clearException(); >+ return wrapperObject; >+} >+ >+JSClassRef JSIPCStreamClientConnection::wrapperClass() >+{ >+ static JSClassRef jsClass; >+ if (!jsClass) { >+ JSClassDefinition definition = kJSClassDefinitionEmpty; >+ definition.className = "StreamClientConnection"; >+ definition.parentClass = nullptr; >+ definition.staticValues = nullptr; >+ definition.staticFunctions = staticFunctions(); >+ definition.initialize = initialize; >+ definition.finalize = finalize; >+ jsClass = JSClassCreate(&definition); >+ } >+ return jsClass; >+} >+ >+inline JSIPCStreamClientConnection* JSIPCStreamClientConnection::unwrap(JSObjectRef object) >+{ >+ return static_cast<JSIPCStreamClientConnection*>(JSObjectGetPrivate(object)); >+} >+ >+JSIPCStreamClientConnection* JSIPCStreamClientConnection::toWrapped(JSContextRef context, JSValueRef value) >+{ >+ if (!context || !value || !JSValueIsObjectOfClass(context, value, wrapperClass())) >+ return nullptr; >+ return unwrap(JSValueToObject(context, value, nullptr)); >+} >+ >+void JSIPCStreamClientConnection::initialize(JSContextRef, JSObjectRef object) >+{ >+ unwrap(object)->ref(); >+} >+ >+void JSIPCStreamClientConnection::finalize(JSObjectRef object) >+{ >+ unwrap(object)->deref(); >+} >+ >+const JSStaticFunction* JSIPCStreamClientConnection::staticFunctions() >+{ >+ static const JSStaticFunction functions[] = { >+ { "streamBuffer", streamBuffer, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, >+ { 0, 0, 0 } >+ }; >+ return functions; >+} >+ >+JSValueRef JSIPCStreamClientConnection::streamBuffer(JSContextRef context, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) >+{ >+ RefPtr jsStreamConnection = toWrapped(context, thisObject); >+ if (!jsStreamConnection) { >+ *exception = createTypeError(context, "Wrong type"_s); >+ return JSValueMakeUndefined(context); >+ } >+ >+ return JSIPCStreamConnectionBuffer::create(*jsStreamConnection)->createJSWrapper(context); >+} >+ >+JSObjectRef JSIPCStreamConnectionBuffer::createJSWrapper(JSContextRef context) >+{ >+ auto* globalObject = toJS(context); >+ auto& vm = globalObject->vm(); >+ JSC::JSLockHolder lock(vm); >+ auto scope = DECLARE_CATCH_SCOPE(vm); >+ JSObjectRef wrapperObject = JSObjectMake(toGlobalRef(globalObject), wrapperClass(), this); >+ scope.clearException(); >+ return wrapperObject; >+} >+ >+JSClassRef JSIPCStreamConnectionBuffer::wrapperClass() >+{ >+ static JSClassRef jsClass; >+ if (!jsClass) { >+ JSClassDefinition definition = kJSClassDefinitionEmpty; >+ definition.className = "StreamConnectionBuffer"; >+ definition.parentClass = nullptr; >+ definition.staticValues = nullptr; >+ definition.staticFunctions = staticFunctions(); >+ definition.initialize = initialize; >+ definition.finalize = finalize; >+ jsClass = JSClassCreate(&definition); >+ } >+ return jsClass; >+} >+ >+inline JSIPCStreamConnectionBuffer* JSIPCStreamConnectionBuffer::unwrap(JSObjectRef object) >+{ >+ return static_cast<JSIPCStreamConnectionBuffer*>(JSObjectGetPrivate(object)); >+} >+ >+JSIPCStreamConnectionBuffer* JSIPCStreamConnectionBuffer::toWrapped(JSContextRef context, JSValueRef value) >+{ >+ if (!context || !value || !JSValueIsObjectOfClass(context, value, wrapperClass())) >+ return nullptr; >+ return unwrap(JSValueToObject(context, value, nullptr)); >+} >+ >+void JSIPCStreamConnectionBuffer::initialize(JSContextRef, JSObjectRef object) >+{ >+ unwrap(object)->ref(); >+} >+ >+void JSIPCStreamConnectionBuffer::finalize(JSObjectRef object) >+{ >+ unwrap(object)->deref(); >+} >+ >+const JSStaticFunction* JSIPCStreamConnectionBuffer::staticFunctions() >+{ >+ static const JSStaticFunction functions[] = { >+ { 0, 0, 0 } >+ }; >+ return functions; >+} >+ > JSValueRef JSIPCSemaphore::signal(JSContextRef context, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) > { > auto* globalObject = toJS(context); >@@ -576,6 +760,7 @@ const JSStaticFunction* JSIPC::staticFunctions() > { "addOutgoingMessageListener", addOutgoingMessageListener, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, > { "sendMessage", sendMessage, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, > { "sendSyncMessage", sendSyncMessage, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, >+ { "createStreamClientConnection", createStreamClientConnection, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, > { "createSemaphore", createSemaphore, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, > { "createSharedMemory", createSharedMemory, kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly }, > { 0, 0, 0 } >@@ -812,6 +997,16 @@ static bool encodeSharedMemory(IPC::Encoder& encoder, JSC::JSGlobalObject* globa > return true; > } > >+static bool encodeStreamConnectionBuffer(IPC::Encoder& encoder, JSC::JSGlobalObject* globalObject, JSC::JSValue jsValue, JSC::CatchScope& scope) >+{ >+ RefPtr jsIPCStreamConnectionBuffer = JSIPCStreamConnectionBuffer::toWrapped(toRef(globalObject), toRef(jsValue)); >+ if (!jsIPCStreamConnectionBuffer) >+ return false; >+ >+ jsIPCStreamConnectionBuffer->encode(encoder); >+ return true; >+} >+ > static bool encodeSemaphore(IPC::Encoder& encoder, JSC::JSGlobalObject* globalObject, JSC::JSValue jsValue, JSC::CatchScope& scope) > { > RefPtr jsIPCSemaphore = JSIPCSemaphore::toWrapped(toRef(globalObject), toRef(jsValue)); >@@ -966,6 +1161,14 @@ static bool encodeArgument(IPC::Encoder& encoder, JSIPC& jsIPC, JSContextRef con > if (scope.exception()) > return false; > >+ if (type == "StreamConnectionBuffer") { >+ if (!encodeStreamConnectionBuffer(encoder, globalObject, jsValue, scope)) { >+ *exception = createTypeError(context, "Failed to convert StreamConnectionBuffer"_s); >+ return false; >+ } >+ return true; >+ } >+ > if (type == "Semaphore") { > if (!encodeSemaphore(encoder, globalObject, jsValue, scope)) { > *exception = createTypeError(context, "Failed to convert Semaphore"_s); >@@ -1245,6 +1448,34 @@ JSValueRef JSIPC::sendSyncMessage(JSContextRef context, JSObjectRef, JSObjectRef > return JSValueMakeUndefined(context); > } > >+JSValueRef JSIPC::createStreamClientConnection(JSContextRef context, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) >+{ >+ auto* globalObject = toJS(context); >+ JSC::JSLockHolder lock(globalObject->vm()); >+ RefPtr jsIPC = toWrapped(context, thisObject); >+ if (!jsIPC) { >+ *exception = createTypeError(context, "Wrong type"_s); >+ return JSValueMakeUndefined(context); >+ } >+ >+ if (argumentCount < 2) { >+ *exception = createTypeError(context, "Must specify the target process and buffer size as the first two arguments"_s); >+ return JSValueMakeUndefined(context); >+ } >+ >+ auto connection = processTargetFromArgument(globalObject, arguments[0], exception); >+ if (!connection) >+ return JSValueMakeUndefined(context); >+ >+ auto bufferSize = argumentCount ? convertToUint64(toJS(globalObject, arguments[1])) : std::optional<uint64_t> { }; >+ if (!bufferSize) { >+ *exception = createTypeError(context, "Must specify the size"_s); >+ return JSValueMakeUndefined(context); >+ } >+ >+ return JSIPCStreamClientConnection::create(*connection, *bufferSize)->createJSWrapper(context); >+} >+ > JSValueRef JSIPC::createSemaphore(JSContextRef context, JSObjectRef, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) > { > return JSIPCSemaphore::create()->createJSWrapper(context); >diff --git a/Tools/ChangeLog b/Tools/ChangeLog >index 87aa3c7852ae000d042b837022aa347e8f74befe..6a05e2bd0c37aa35373d6bee8584217402841f4c 100644 >--- a/Tools/ChangeLog >+++ b/Tools/ChangeLog >@@ -1,3 +1,21 @@ >+2021-10-13 Wenson Hsieh <wenson_hsieh@apple.com> >+ >+ [JS IPC] Make it possible to establish a stream connection using the JS IPC testing API >+ https://bugs.webkit.org/show_bug.cgi?id=231710 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ See WebKit ChangeLog for more details. >+ >+ * TestWebKitAPI/Tests/WebKitCocoa/IPCTestingAPI.mm: >+ >+ Adjust these two API tests so that they actually end up creating a new remote rendering backend in the GPU >+ process, instead of failing to decode because we're missing a StreamConnectionBuffer. This allows us to make it >+ to `GPUConnectionToWebProcess::createRenderingBackend` and actually create the corresponding rendering backend; >+ however, this is still insufficient to fix the failing API tests, since there is still no mechanism to (1) >+ wait for and receive the stream wakeup semaphore in the web process, and (2) send stream messages using an >+ instance of the stream client connection. >+ > 2021-10-12 Jonathan Bedard <jbedard@apple.com> > > [AppleSilicon] Support native simulator testing >diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/IPCTestingAPI.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/IPCTestingAPI.mm >index 5aa9bb7b30a17ca6323edb6bddefce2aa185d89b..d92cec18fe04801bbdda593fec287adaddb79532 100644 >--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/IPCTestingAPI.mm >+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/IPCTestingAPI.mm >@@ -291,14 +291,19 @@ TEST(IPCTestingAPI, CanReceiveIPCSemaphore) > [webView setUIDelegate:delegate.get()]; > > done = false; >- [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html><script>" >- "const semaphore = IPC.createSemaphore();" >- "IPC.sendMessage('GPU', 0, IPC.messages.GPUConnectionToWebProcess_CreateRenderingBackend.name," >- " [{type: 'RemoteRenderingBackendCreationParameters', 'identifier': 123, semaphore, 'pageProxyID': IPC.webPageProxyID, 'pageID': IPC.pageID}]);" >+ auto html = @"<!DOCTYPE html>" >+ "<script>" >+ "const bufferSize = 1 << 16;" >+ "const streamConnection = IPC.createStreamClientConnection('GPU', bufferSize);" >+ "IPC.sendMessage('GPU', 0, IPC.messages.GPUConnectionToWebProcess_CreateRenderingBackend.name, [" >+ " { type: 'RemoteRenderingBackendCreationParameters', 'identifier': 123, 'pageProxyID': IPC.webPageProxyID, 'pageID': IPC.pageID }," >+ " { type: 'StreamConnectionBuffer', value: streamConnection.streamBuffer() }," >+ "]);" > "const result = IPC.sendSyncMessage('GPU', 123, IPC.messages.RemoteRenderingBackend_SemaphoreForGetPixelBuffer.name, 100, []);" > "semaphore.signal();" > "alert(result.arguments.length + ':' + result.arguments[0].type + ':' + result.arguments[0].value.waitFor(100));" >- "</script>"]; >+ "</script>"; >+ [webView synchronouslyLoadHTMLString:html]; > TestWebKitAPI::Util::run(&done); > > EXPECT_STREQ([alertMessage UTF8String], "1:Semaphore:false"); >@@ -311,13 +316,17 @@ TEST(IPCTestingAPI, CanReceiveSharedMemory) > auto delegate = adoptNS([[IPCTestingAPIDelegate alloc] init]); > [webView setUIDelegate:delegate.get()]; > >- auto* html = @R"HTML(<!DOCTYPE html> >-<script> >-IPC.sendMessage('GPU', 0, IPC.messages.GPUConnectionToWebProcess_CreateRenderingBackend.name, >- [{type: 'RemoteRenderingBackendCreationParameters', 'identifier': 123, semaphore: IPC.createSemaphore(), 'pageProxyID': IPC.webPageProxyID, 'pageID': IPC.pageID}]); >-const result = IPC.sendSyncMessage('GPU', 123, IPC.messages.RemoteRenderingBackend_UpdateSharedMemoryForGetPixelBuffer.name, 100, [{type: 'uint32_t', value: 8}]); >-alert(result.arguments.length); >-</script>)HTML"; >+ auto html = @"<!DOCTYPE html>" >+ "<script>" >+ "const bufferSize = 1 << 16;" >+ "const streamConnection = IPC.createStreamClientConnection('GPU', bufferSize);" >+ "IPC.sendMessage('GPU', 0, IPC.messages.GPUConnectionToWebProcess_CreateRenderingBackend.name, [" >+ " { type: 'RemoteRenderingBackendCreationParameters', 'identifier': 123, 'pageProxyID': IPC.webPageProxyID, 'pageID': IPC.pageID }," >+ " { type: 'StreamConnectionBuffer', value: streamConnection.streamBuffer() }," >+ "]);" >+ "const result = IPC.sendSyncMessage('GPU', 123, IPC.messages.RemoteRenderingBackend_UpdateSharedMemoryForGetPixelBuffer.name, 100, [{type: 'uint32_t', value: 8}]);" >+ "alert(result.arguments.length);" >+ "</script>"; > > done = false; > [webView synchronouslyLoadHTMLString:html];
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 231710
:
441163
| 441166