Left: | ||
Right: |
LEFT | RIGHT |
---|---|
1 #include <AdblockPlus.h> | 1 #include <AdblockPlus.h> |
2 #include <iostream> | 2 #include <iostream> |
3 #include <Lmcons.h> | |
3 #include <ShlObj.h> | 4 #include <ShlObj.h> |
4 #include <sstream> | 5 #include <sstream> |
5 #include <vector> | 6 #include <vector> |
6 #include <Windows.h> | 7 #include <Windows.h> |
7 #include <Sddl.h> | 8 #include <Sddl.h> |
Wladimir Palant
2013/05/16 11:52:16
I think that the usual order of includes is from g
Felix Dahlke
2013/05/17 09:11:36
We're using alphabetical in most places, but it's
| |
8 | 9 |
9 namespace | 10 namespace |
10 { | 11 { |
11 const std::wstring pipeName = L"\\\\.\\pipe\\adblockplusengine"; | 12 std::wstring GetUserName() |
13 { | |
14 const DWORD maxLength = UNLEN + 1; | |
15 std::auto_ptr<wchar_t> buffer(new wchar_t[maxLength]); | |
16 DWORD length = maxLength; | |
17 if (!::GetUserName(buffer.get(), &length)) | |
18 { | |
19 std::stringstream stream; | |
20 stream << "Failed to get the current user's name (Error code: " << GetLast Error() << ")"; | |
21 throw std::runtime_error("Failed to get the current user's name"); | |
22 } | |
23 return std::wstring(buffer.get(), length); | |
24 } | |
25 | |
26 const std::wstring pipeName = L"\\\\.\\pipe\\adblockplusengine_" + GetUserName (); | |
12 const int bufferSize = 1024; | 27 const int bufferSize = 1024; |
13 std::auto_ptr<AdblockPlus::FilterEngine> filterEngine; | 28 std::auto_ptr<AdblockPlus::FilterEngine> filterEngine; |
14 HANDLE filterEngineMutex; | |
15 | 29 |
16 class AutoHandle | 30 class AutoHandle |
17 { | 31 { |
18 public: | 32 public: |
19 AutoHandle() | 33 AutoHandle() |
20 { | 34 { |
21 } | 35 } |
22 | 36 |
23 AutoHandle(HANDLE handle) : handle(handle) | 37 AutoHandle(HANDLE handle) : handle(handle) |
24 { | 38 { |
25 } | 39 } |
26 | 40 |
27 ~AutoHandle() | 41 ~AutoHandle() |
28 { | 42 { |
29 CloseHandle(handle); | 43 CloseHandle(handle); |
30 } | 44 } |
31 | 45 |
32 HANDLE get() | 46 HANDLE get() |
33 { | 47 { |
34 return handle; | 48 return handle; |
35 } | 49 } |
36 | 50 |
37 private: | 51 private: |
38 HANDLE handle; | 52 HANDLE handle; |
39 static int references; | |
Wladimir Palant
2013/05/16 11:52:16
Unused member?
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
40 | 53 |
41 AutoHandle(const AutoHandle& autoHandle); | 54 AutoHandle(const AutoHandle& autoHandle); |
42 AutoHandle& operator=(const AutoHandle& autoHandle); | 55 AutoHandle& operator=(const AutoHandle& autoHandle); |
43 }; | 56 }; |
44 | 57 |
45 void Log(const std::string& message) | 58 void Log(const std::string& message) |
46 { | 59 { |
47 // TODO: Log to a log file | 60 // TODO: Log to a log file |
48 MessageBoxA(0, ("AdblockPlusEngine: " + message).c_str(), "", MB_OK); | 61 MessageBoxA(0, ("AdblockPlusEngine: " + message).c_str(), "", MB_OK); |
49 } | 62 } |
50 | 63 |
51 void LogLastError(const std::string& message) | 64 void LogLastError(const std::string& message) |
52 { | 65 { |
53 std::stringstream stream; | 66 std::stringstream stream; |
54 stream << message << " (Error code: " << GetLastError() << ")"; | 67 stream << message << " (Error code: " << GetLastError() << ")"; |
55 Log(stream.str()); | 68 Log(stream.str()); |
56 } | 69 } |
57 | 70 |
58 void LogException(const std::exception& exception) | 71 void LogException(const std::exception& exception) |
59 { | 72 { |
60 Log(std::string("An exception occurred: ") + exception.what()); | 73 Log(std::string("An exception occurred: ") + exception.what()); |
61 } | 74 } |
62 | 75 |
63 std::string MarshalStrings(const std::vector<std::string>& strings) | 76 std::string MarshalStrings(const std::vector<std::string>& strings) |
64 { | 77 { |
65 // TODO: This is some pretty hacky marshalling, replace it with something mo re robust | 78 // TODO: This is some pretty hacky marshalling, replace it with something mo re robust |
Wladimir Palant
2013/05/16 11:52:16
That's an understatement :)
| |
66 std::string marshalledStrings; | 79 std::string marshalledStrings; |
67 for (std::vector<std::string>::const_iterator it = strings.begin(); it != st rings.end(); it++) | 80 for (std::vector<std::string>::const_iterator it = strings.begin(); it != st rings.end(); it++) |
68 marshalledStrings += *it + ';'; | 81 marshalledStrings += *it + ';'; |
69 return marshalledStrings; | 82 return marshalledStrings; |
70 } | 83 } |
71 | 84 |
72 std::vector<std::string> UnmarshalStrings(const std::string& message) | 85 std::vector<std::string> UnmarshalStrings(const std::string& message) |
73 { | 86 { |
74 std::stringstream stream(message); | 87 std::stringstream stream(message); |
75 std::vector<std::string> strings; | 88 std::vector<std::string> strings; |
76 std::string string; | 89 std::string string; |
77 while (std::getline(stream, string, ';')) | 90 while (std::getline(stream, string, ';')) |
78 strings.push_back(string); | 91 strings.push_back(string); |
79 return strings; | 92 return strings; |
80 } | 93 } |
81 | 94 |
82 std::string ToString(std::wstring value) | 95 std::string ToUtf8String(std::wstring str) |
83 { | 96 { |
84 int size = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, 0, 0, 0, 0); | 97 size_t length = str.size(); |
Wladimir Palant
2013/05/16 11:52:16
WideCharToMultiByte() will return 0 on error, this
Felix Dahlke
2013/05/23 12:31:20
Done. Learnt something here: I thought std::string
| |
85 char* converted = new char[size]; | 98 if (length == 0) |
Wladimir Palant
2013/05/16 11:52:16
std::auto_ptr please, don't delete manually.
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
86 WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, converted, size, 0, 0); | 99 return std::string(); |
87 std::string string(converted); | 100 |
Wladimir Palant
2013/05/16 11:52:16
string(converted, size) please - you have an expli
Felix Dahlke
2013/05/17 09:11:36
Writing directly to the string buffer is still not
Felix Dahlke
2013/05/23 12:31:20
Decided to keep the function after all. ToUTF8Stri
Wladimir Palant
2013/05/23 14:25:01
No real objections but the function name should at
Felix Dahlke
2013/05/23 19:10:44
Just copied the function from libadblockplus, alth
| |
88 delete converted; | 101 DWORD utf8StringLength = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), length , 0, 0, 0, 0); |
89 return string; | 102 if (utf8StringLength == 0) |
103 throw std::runtime_error("Failed to determine the required buffer size"); | |
104 | |
105 std::string utf8String(utf8StringLength, '\0'); | |
106 WideCharToMultiByte(CP_UTF8, 0, str.c_str(), length, &utf8String[0], utf8Str ingLength, 0, 0); | |
107 return utf8String; | |
90 } | 108 } |
91 | 109 |
92 std::string ReadMessage(HANDLE pipe) | 110 std::string ReadMessage(HANDLE pipe) |
93 { | 111 { |
94 std::stringstream stream; | 112 std::stringstream stream; |
95 std::auto_ptr<char> buffer(new char[bufferSize]); | 113 std::auto_ptr<char> buffer(new char[bufferSize]); |
96 bool hasError; | 114 bool doneReading = false; |
97 do | 115 while (!doneReading) |
98 { | 116 { |
99 DWORD bytesRead; | 117 DWORD bytesRead; |
100 hasError = !ReadFile(pipe, buffer.get(), bufferSize * sizeof(char), &bytes Read, 0); | 118 if (ReadFile(pipe, buffer.get(), bufferSize * sizeof(char), &bytesRead, 0) ) |
101 if (hasError && GetLastError() != ERROR_MORE_DATA) | 119 doneReading = true; |
120 else if (GetLastError() != ERROR_MORE_DATA) | |
102 { | 121 { |
103 std::stringstream stream; | 122 std::stringstream stream; |
104 stream << "Error reading from pipe: " << GetLastError(); | 123 stream << "Error reading from pipe: " << GetLastError(); |
105 throw std::runtime_error(stream.str()); | 124 throw std::runtime_error(stream.str()); |
106 } | 125 } |
107 stream << std::string(buffer.get(), bytesRead); | 126 stream << std::string(buffer.get(), bytesRead); |
108 } while (hasError); | 127 } |
Wladimir Palant
2013/05/16 11:52:16
The flow is rather hard to follow here, particular
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
109 return stream.str(); | 128 return stream.str(); |
110 } | 129 } |
111 | 130 |
112 void WriteMessage(HANDLE pipe, const std::string& message) | 131 void WriteMessage(HANDLE pipe, const std::string& message) |
113 { | 132 { |
114 DWORD bytesWritten; | 133 DWORD bytesWritten; |
115 if (!WriteFile(pipe, message.c_str(), message.length(), &bytesWritten, 0)) | 134 if (!WriteFile(pipe, message.c_str(), message.length(), &bytesWritten, 0)) |
116 throw std::runtime_error("Failed to write to pipe"); | 135 throw std::runtime_error("Failed to write to pipe"); |
Wladimir Palant
2013/05/16 11:52:16
What if bytesWritten < message.length()? According
Felix Dahlke
2013/05/17 09:11:36
Yes, it's blocking. And writing long strings works
| |
117 } | 136 } |
118 | 137 |
119 std::string HandleRequest(const std::vector<std::string>& strings) | 138 std::string HandleRequest(const std::vector<std::string>& strings) |
120 { | 139 { |
121 std::string procedureName = strings[0]; | 140 std::string procedureName = strings[0]; |
122 if (procedureName == "Matches") | 141 if (procedureName == "Matches") |
123 return filterEngine->Matches(strings[1], strings[2], strings[3]) ? "1" : " 0"; | 142 return filterEngine->Matches(strings[1], strings[2], strings[3]) ? "1" : " 0"; |
124 if (procedureName == "GetElementHidingSelectors") | 143 if (procedureName == "GetElementHidingSelectors") |
125 return MarshalStrings(filterEngine->GetElementHidingSelectors(strings[1])) ; | 144 return MarshalStrings(filterEngine->GetElementHidingSelectors(strings[1])) ; |
126 return ""; | 145 return ""; |
127 } | 146 } |
128 | 147 |
129 DWORD WINAPI ClientThread(LPVOID param) | 148 DWORD WINAPI ClientThread(LPVOID param) |
130 { | 149 { |
131 HANDLE pipe = static_cast<HANDLE>(param); | 150 HANDLE pipe = static_cast<HANDLE>(param); |
132 | 151 |
133 try | 152 try |
134 { | 153 { |
135 std::string message = ReadMessage(pipe); | 154 std::string message = ReadMessage(pipe); |
136 std::vector<std::string> strings = UnmarshalStrings(message); | 155 std::vector<std::string> strings = UnmarshalStrings(message); |
137 WaitForSingleObject(filterEngineMutex, INFINITE); | |
Wladimir Palant
2013/05/16 11:52:16
Doesn't this duplicate the functionality of mutexe
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
138 std::string response = HandleRequest(strings); | 156 std::string response = HandleRequest(strings); |
139 ReleaseMutex(filterEngineMutex); | |
140 WriteMessage(pipe, response); | 157 WriteMessage(pipe, response); |
141 } | 158 } |
142 catch (const std::exception& e) | 159 catch (const std::exception& e) |
143 { | 160 { |
144 LogException(e); | 161 LogException(e); |
145 } | 162 } |
146 | 163 |
164 // TODO: Keep the pipe open until the client disconnects | |
147 FlushFileBuffers(pipe); | 165 FlushFileBuffers(pipe); |
148 DisconnectNamedPipe(pipe); | 166 DisconnectNamedPipe(pipe); |
149 CloseHandle(pipe); | 167 CloseHandle(pipe); |
Wladimir Palant
2013/05/16 11:52:16
Opening a new connection for each calls sounds lik
Felix Dahlke
2013/05/17 09:11:36
I previously read that there is a limit to the num
Felix Dahlke
2013/05/23 12:31:20
Began to work on this, but it's a bit tricky and I
| |
150 return 0; | 168 return 0; |
151 } | 169 } |
152 } | 170 |
153 | 171 bool IsWindowsVistaOrLater() |
154 bool IsWindowsVistaOrLater() | 172 { |
Wladimir Palant
2013/05/16 11:52:16
Any reason why some functions are inside an empty
Felix Dahlke
2013/05/17 09:11:36
Forgot to move it there.
| |
155 { | 173 OSVERSIONINFOEX osvi; |
156 OSVERSIONINFOEX osvi; | 174 ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); |
157 ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); | 175 osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); |
158 osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); | 176 GetVersionEx(reinterpret_cast<LPOSVERSIONINFO>(&osvi)); |
159 GetVersionEx(reinterpret_cast<LPOSVERSIONINFO>(&osvi)); | 177 return osvi.dwMajorVersion >= 6; |
160 return osvi.dwMajorVersion >= 6; | 178 } |
161 } | 179 } |
162 | 180 |
163 std::wstring GetAppDataPath() | 181 std::wstring GetAppDataPath() |
164 { | 182 { |
165 wchar_t appDataPath[MAX_PATH]; | 183 std::wstring appDataPath; |
166 if (IsWindowsVistaOrLater()) | 184 if (IsWindowsVistaOrLater()) |
167 { | 185 { |
168 WCHAR* dataPath; | 186 WCHAR* pathBuffer; |
169 if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, 0, &dataPath))) | 187 if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, 0, &pathBuffer) )) |
Wladimir Palant
2013/05/16 11:52:16
I don't think that statically compiling against SH
Felix Dahlke
2013/05/23 12:31:20
I'll leave this to Oleksandr, since he's working o
Oleksandr
2013/05/23 12:38:11
We are not statically compiling against SHGetKnown
| |
170 throw std::runtime_error("Unable to find app data directory"); | 188 throw std::runtime_error("Unable to find app data directory"); |
171 wcscpy_s(appDataPath, dataPath); | 189 appDataPath.assign(pathBuffer); |
Wladimir Palant
2013/05/16 11:52:16
Why do we need two copy operations here? Convertin
Felix Dahlke
2013/05/17 09:11:36
I think that's because SHGetKnownFolderPath wants
Wladimir Palant
2013/05/17 10:35:02
Then maybe get rid of that assumption? :)
Declare
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
172 CoTaskMemFree(dataPath); | 190 CoTaskMemFree(pathBuffer); |
173 } | 191 } |
174 else | 192 else |
175 { | 193 { |
176 if (!SHGetSpecialFolderPath(0, appDataPath, CSIDL_LOCAL_APPDATA, true)) | 194 std::auto_ptr<wchar_t> pathBuffer(new wchar_t[MAX_PATH]); |
195 if (!SHGetSpecialFolderPath(0, pathBuffer.get(), CSIDL_LOCAL_APPDATA, true)) | |
177 throw std::runtime_error("Unable to find app data directory"); | 196 throw std::runtime_error("Unable to find app data directory"); |
178 } | 197 appDataPath.assign(pathBuffer.get()); |
179 return std::wstring(appDataPath) + L"\\AdblockPlus"; | 198 } |
199 return appDataPath + L"\\AdblockPlus"; | |
180 } | 200 } |
181 | 201 |
182 std::auto_ptr<AdblockPlus::FilterEngine> CreateFilterEngine() | 202 std::auto_ptr<AdblockPlus::FilterEngine> CreateFilterEngine() |
183 { | 203 { |
184 // TODO: Pass appInfo in, which should be sent by the client | 204 // TODO: Pass appInfo in, which should be sent by the client |
185 AdblockPlus::JsEnginePtr jsEngine = AdblockPlus::JsEngine::New(); | 205 AdblockPlus::JsEnginePtr jsEngine = AdblockPlus::JsEngine::New(); |
186 std::string dataPath = ToString(GetAppDataPath()); | 206 std::string dataPath = ToUtf8String(GetAppDataPath()); |
187 dynamic_cast<AdblockPlus::DefaultFileSystem*>(jsEngine->GetFileSystem().get()) ->SetBasePath(dataPath); | 207 dynamic_cast<AdblockPlus::DefaultFileSystem*>(jsEngine->GetFileSystem().get()) ->SetBasePath(dataPath); |
188 std::auto_ptr<AdblockPlus::FilterEngine> filterEngine(new AdblockPlus::FilterE ngine(jsEngine)); | 208 std::auto_ptr<AdblockPlus::FilterEngine> filterEngine(new AdblockPlus::FilterE ngine(jsEngine)); |
189 int timeout = 5000; | |
190 while (!filterEngine->IsInitialized()) | |
Wladimir Palant
2013/05/16 11:52:16
TODO comment here please - we don't really want to
Felix Dahlke
2013/05/17 09:11:36
I presume that's not necessary anymore, now that y
Wladimir Palant
2013/05/17 10:35:02
Not pushed yet but - yes, once that review is done
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
191 { | |
192 const int step = 10; | |
193 Sleep(step); | |
194 timeout -= step; | |
195 if (timeout <= 0) | |
196 throw std::runtime_error("Timeout while waiting for FilterEngine initializ ation"); | |
197 } | |
198 std::vector<AdblockPlus::SubscriptionPtr> subscriptions = filterEngine->FetchA vailableSubscriptions(); | 209 std::vector<AdblockPlus::SubscriptionPtr> subscriptions = filterEngine->FetchA vailableSubscriptions(); |
199 // TODO: Select a subscription based on the language, not just the first one. | 210 // TODO: Select a subscription based on the language, not just the first one. |
200 // This should ideally be done in libadblockplus. | 211 // This should ideally be done in libadblockplus. |
201 AdblockPlus::SubscriptionPtr subscription = subscriptions[0]; | 212 AdblockPlus::SubscriptionPtr subscription = subscriptions[0]; |
202 subscription->AddToList(); | 213 subscription->AddToList(); |
203 return filterEngine; | 214 return filterEngine; |
204 } | 215 } |
205 | 216 |
206 HANDLE CreatePipe(const std::wstring& pipeName) | 217 HANDLE CreatePipe(const std::wstring& pipeName) |
207 { | 218 { |
208 SECURITY_ATTRIBUTES sa; | 219 SECURITY_ATTRIBUTES sa; |
209 memset(&sa, 0, sizeof(SECURITY_ATTRIBUTES)); | 220 memset(&sa, 0, sizeof(SECURITY_ATTRIBUTES)); |
210 sa.nLength = sizeof(SECURITY_ATTRIBUTES); | 221 sa.nLength = sizeof(SECURITY_ATTRIBUTES); |
211 | 222 |
212 // Low mandatory label. See http://msdn.microsoft.com/en-us/library/bb625958.a spx | 223 // Low mandatory label. See http://msdn.microsoft.com/en-us/library/bb625958.a spx |
213 LPCWSTR LOW_INTEGRITY_SDDL_SACL_W = L"S:(ML;;NW;;;LW)"; | 224 LPCWSTR accessControlEntry = L"S:(ML;;NW;;;LW)"; |
214 PSECURITY_DESCRIPTOR securitydescriptor; | 225 PSECURITY_DESCRIPTOR securitydescriptor; |
215 ConvertStringSecurityDescriptorToSecurityDescriptor( | 226 ConvertStringSecurityDescriptorToSecurityDescriptor(accessControlEntry, SDDL_R EVISION_1, &securitydescriptor, 0); |
216 LOW_INTEGRITY_SDDL_SACL_W, SDDL_REVISION_1, &securitydescriptor, 0); | |
Wladimir Palant
2013/05/16 11:52:16
You are leaking this security descriptor, it needs
Felix Dahlke
2013/05/23 12:31:20
Done.
| |
217 | 227 |
218 sa.lpSecurityDescriptor = securitydescriptor; | 228 sa.lpSecurityDescriptor = securitydescriptor; |
219 sa.bInheritHandle = TRUE; | 229 sa.bInheritHandle = TRUE; |
220 | 230 |
221 return CreateNamedPipe(pipeName.c_str(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, | 231 HANDLE pipe = CreateNamedPipe(pipeName.c_str(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_ MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, |
222 PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &s a); | 232 PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize , 0, &sa); |
233 LocalFree(securitydescriptor); | |
234 return pipe; | |
223 } | 235 } |
224 | 236 |
225 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) | 237 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) |
Wladimir Palant
2013/05/16 11:52:16
Second HINSTANCE parameter is hPrevInstance - you
Felix Dahlke
2013/05/23 12:31:20
Seems to be always 0 in Win32: http://blogs.msdn.c
Wladimir Palant
2013/05/23 14:25:01
Right, a bit of outdated knowledge on my end :-(
F
Felix Dahlke
2013/05/23 19:10:44
How about we just try to create the pipe initially
Wladimir Palant
2013/05/23 20:10:15
Yes, that's something I thought about as well. Thi
| |
226 { | 238 { |
227 filterEngine = CreateFilterEngine(); | 239 filterEngine = CreateFilterEngine(); |
228 filterEngineMutex = CreateMutex(0, false, 0); | |
229 | 240 |
230 for (;;) | 241 for (;;) |
231 { | 242 { |
232 HANDLE pipe = CreatePipe(pipeName); | 243 HANDLE pipe = CreatePipe(pipeName); |
233 if (pipe == INVALID_HANDLE_VALUE) | 244 if (pipe == INVALID_HANDLE_VALUE) |
234 { | 245 { |
235 LogLastError("CreateNamedPipe failed"); | 246 LogLastError("CreateNamedPipe failed"); |
236 return 1; | 247 return 1; |
237 } | 248 } |
238 | 249 |
239 if (!ConnectNamedPipe(pipe, 0)) | 250 if (!ConnectNamedPipe(pipe, 0)) |
240 { | 251 { |
241 LogLastError("Client failed to connect"); | 252 LogLastError("Client failed to connect"); |
242 CloseHandle(pipe); | 253 CloseHandle(pipe); |
243 continue; | 254 continue; |
244 } | 255 } |
245 | 256 |
246 // TODO: Count established connections, kill the engine when none are left | 257 // TODO: Count established connections, kill the engine when none are left |
247 | 258 |
248 AutoHandle thread(CreateThread(0, 0, ClientThread, static_cast<LPVOID>(pipe) , 0, 0)); | 259 AutoHandle thread(CreateThread(0, 0, ClientThread, static_cast<LPVOID>(pipe) , 0, 0)); |
249 if (!thread.get()) | 260 if (!thread.get()) |
250 { | 261 { |
251 LogLastError("CreateThread failed"); | 262 LogLastError("CreateThread failed"); |
252 return 1; | 263 return 1; |
253 } | 264 } |
254 } | 265 } |
255 | 266 |
256 return 0; | 267 return 0; |
257 } | 268 } |
Wladimir Palant
2013/05/16 11:52:16
IMHO, ReadMessage(), WriteMessage(), MarshalString
Felix Dahlke
2013/05/17 09:11:36
Same here, there's a TODO for that :)
| |
LEFT | RIGHT |