@noita-ts/wininet 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asm_link.lua +47 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.lua +457 -0
- package/dist/lualib_bundle.lua +301 -0
- package/dist/thread.d.ts +46 -0
- package/dist/thread.lua +382 -0
- package/package.json +31 -4
- package/readme.md +66 -0
- package/README.md +0 -3
package/dist/thread.lua
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
local ____lualib = require("lualib_bundle")
|
|
2
|
+
local __TS__ArrayForEach = ____lualib.__TS__ArrayForEach
|
|
3
|
+
local __TS__Class = ____lualib.__TS__Class
|
|
4
|
+
local Error = ____lualib.Error
|
|
5
|
+
local RangeError = ____lualib.RangeError
|
|
6
|
+
local ReferenceError = ____lualib.ReferenceError
|
|
7
|
+
local SyntaxError = ____lualib.SyntaxError
|
|
8
|
+
local TypeError = ____lualib.TypeError
|
|
9
|
+
local URIError = ____lualib.URIError
|
|
10
|
+
local __TS__New = ____lualib.__TS__New
|
|
11
|
+
local __TS__ArrayMap = ____lualib.__TS__ArrayMap
|
|
12
|
+
local ____exports = {}
|
|
13
|
+
local ____ffi = require("@noita-ts/ffi")
|
|
14
|
+
local ffi = ____ffi.default
|
|
15
|
+
local ____schema = require("@noita-ts/ffi/schema")
|
|
16
|
+
local c = ____schema.default
|
|
17
|
+
local MAX_ARGS = 8
|
|
18
|
+
ffi.cdef("\n void* CreateThread(void* attributes, size_t stack_size, void* start, void* param, uint32_t flags, uint32_t* id);\n int CloseHandle(void* handle);\n uint32_t GetLastError();\n void* GetProcessHeap();\n void* HeapAlloc(void* heap, uint32_t flags, size_t bytes);\n int HeapFree(void* heap, uint32_t flags, void* mem);\n")
|
|
19
|
+
local C = ffi.C
|
|
20
|
+
--- The thread runs the call.
|
|
21
|
+
local RUNNING = 0
|
|
22
|
+
--- The thread is done; Lua frees the memory of the call.
|
|
23
|
+
local DONE = 1
|
|
24
|
+
--- The pool is released; the thread frees the memory of the call.
|
|
25
|
+
local ABANDONED = 2
|
|
26
|
+
--- `entry` is the thread procedure, `DWORD WINAPI proc(call*)`: it pushes the
|
|
27
|
+
-- arguments, the last one first, calls `fn`, stores its result and the
|
|
28
|
+
-- `GetLastError` of the thread, and swaps `state` to DONE (`xchg` with memory
|
|
29
|
+
-- is atomic, and orders the stores before it). If the state was ABANDONED,
|
|
30
|
+
-- the pool is gone, so the thread frees the `owned` memory and the call itself.
|
|
31
|
+
-- `esp` comes back from `ebp`, so that `fn` can be stdcall or cdecl.
|
|
32
|
+
--
|
|
33
|
+
-- `swap_state` is `uint32_t __cdecl swap_state(call*, uint32_t state)`: it
|
|
34
|
+
-- swaps the state of a call atomically and returns the old one, for the Lua
|
|
35
|
+
-- side, which has no atomic operations.
|
|
36
|
+
local CODE = setmetatable(
|
|
37
|
+
{raw = {
|
|
38
|
+
85,
|
|
39
|
+
137,
|
|
40
|
+
229,
|
|
41
|
+
83,
|
|
42
|
+
86,
|
|
43
|
+
139,
|
|
44
|
+
93,
|
|
45
|
+
8,
|
|
46
|
+
139,
|
|
47
|
+
75,
|
|
48
|
+
4,
|
|
49
|
+
133,
|
|
50
|
+
201,
|
|
51
|
+
116,
|
|
52
|
+
7,
|
|
53
|
+
255,
|
|
54
|
+
116,
|
|
55
|
+
139,
|
|
56
|
+
4,
|
|
57
|
+
73,
|
|
58
|
+
117,
|
|
59
|
+
249,
|
|
60
|
+
255,
|
|
61
|
+
19,
|
|
62
|
+
137,
|
|
63
|
+
67,
|
|
64
|
+
40,
|
|
65
|
+
255,
|
|
66
|
+
83,
|
|
67
|
+
52,
|
|
68
|
+
137,
|
|
69
|
+
67,
|
|
70
|
+
44,
|
|
71
|
+
141,
|
|
72
|
+
101,
|
|
73
|
+
248,
|
|
74
|
+
184,
|
|
75
|
+
1,
|
|
76
|
+
0,
|
|
77
|
+
0,
|
|
78
|
+
0,
|
|
79
|
+
135,
|
|
80
|
+
67,
|
|
81
|
+
48,
|
|
82
|
+
131,
|
|
83
|
+
248,
|
|
84
|
+
2,
|
|
85
|
+
117,
|
|
86
|
+
31,
|
|
87
|
+
139,
|
|
88
|
+
115,
|
|
89
|
+
64,
|
|
90
|
+
133,
|
|
91
|
+
246,
|
|
92
|
+
116,
|
|
93
|
+
15,
|
|
94
|
+
78,
|
|
95
|
+
255,
|
|
96
|
+
116,
|
|
97
|
+
179,
|
|
98
|
+
68,
|
|
99
|
+
106,
|
|
100
|
+
0,
|
|
101
|
+
255,
|
|
102
|
+
115,
|
|
103
|
+
60,
|
|
104
|
+
255,
|
|
105
|
+
83,
|
|
106
|
+
56,
|
|
107
|
+
235,
|
|
108
|
+
237,
|
|
109
|
+
83,
|
|
110
|
+
106,
|
|
111
|
+
0,
|
|
112
|
+
255,
|
|
113
|
+
115,
|
|
114
|
+
60,
|
|
115
|
+
255,
|
|
116
|
+
83,
|
|
117
|
+
56,
|
|
118
|
+
94,
|
|
119
|
+
91,
|
|
120
|
+
49,
|
|
121
|
+
192,
|
|
122
|
+
93,
|
|
123
|
+
194,
|
|
124
|
+
4,
|
|
125
|
+
0,
|
|
126
|
+
139,
|
|
127
|
+
76,
|
|
128
|
+
36,
|
|
129
|
+
4,
|
|
130
|
+
139,
|
|
131
|
+
68,
|
|
132
|
+
36,
|
|
133
|
+
8,
|
|
134
|
+
135,
|
|
135
|
+
65,
|
|
136
|
+
48,
|
|
137
|
+
195
|
|
138
|
+
}, vars = {}, labels = {entry = 0, swap_state = 88}},
|
|
139
|
+
{__call = require("asm_link").link}
|
|
140
|
+
)
|
|
141
|
+
--- A 32-bit FNV-1a hash of `bytes`, as an unsigned decimal number.
|
|
142
|
+
local function hash(bytes)
|
|
143
|
+
local h = bit.bor(2166136261, 0)
|
|
144
|
+
for ____, b in ipairs(bytes) do
|
|
145
|
+
h = bit.bxor(h, b)
|
|
146
|
+
h = bit.bor(
|
|
147
|
+
h * 403 + bit.lshift(h, 24),
|
|
148
|
+
0
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
return tostring(h < 0 and h + 4294967296 or h)
|
|
152
|
+
end
|
|
153
|
+
--- A call on a thread. The thread procedure has the offsets of its fields.
|
|
154
|
+
local Call = c.declare(
|
|
155
|
+
"NoitaTsWininetCall",
|
|
156
|
+
{
|
|
157
|
+
c.field("fn", c.voidptr),
|
|
158
|
+
c.field("argc", c.u32),
|
|
159
|
+
c.field(
|
|
160
|
+
"args",
|
|
161
|
+
c.u32:arr(MAX_ARGS)
|
|
162
|
+
),
|
|
163
|
+
c.field("result", c.u32),
|
|
164
|
+
c.field("error", c.u32),
|
|
165
|
+
c.field(
|
|
166
|
+
"state",
|
|
167
|
+
c.escape("volatile uint32_t", 4, 4)
|
|
168
|
+
),
|
|
169
|
+
c.field("get_last_error", c.voidptr),
|
|
170
|
+
c.field("heap_free", c.voidptr),
|
|
171
|
+
c.field("heap", c.voidptr),
|
|
172
|
+
c.field("owned_count", c.u32),
|
|
173
|
+
c.field(
|
|
174
|
+
"owned",
|
|
175
|
+
c.voidptr:arr(MAX_ARGS)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
local code
|
|
180
|
+
--- The address of the code. The name in the store comes from the bytes of the
|
|
181
|
+
-- code, which also fix the layout of the call, so another version of this
|
|
182
|
+
-- package that changes either one makes its own.
|
|
183
|
+
local function getCode()
|
|
184
|
+
if code == nil then
|
|
185
|
+
code = ffi.store.shared(
|
|
186
|
+
"wininet.proc." .. hash(CODE.raw),
|
|
187
|
+
function()
|
|
188
|
+
local mem = ffi.allocExec(#CODE.raw, {persistent = true})
|
|
189
|
+
__TS__ArrayForEach(
|
|
190
|
+
CODE.raw,
|
|
191
|
+
function(____, b, i)
|
|
192
|
+
mem[i] = b
|
|
193
|
+
end
|
|
194
|
+
)
|
|
195
|
+
return tonumber(ffi.cast("uint32_t", mem))
|
|
196
|
+
end
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
return code
|
|
200
|
+
end
|
|
201
|
+
local swapState
|
|
202
|
+
--- Swaps the state of `call` atomically, and returns the old one.
|
|
203
|
+
local function swap(call, state)
|
|
204
|
+
swapState = swapState or ffi.cast(
|
|
205
|
+
"uint32_t (__cdecl *)(void*, uint32_t)",
|
|
206
|
+
getCode() + CODE.labels.swap_state
|
|
207
|
+
)
|
|
208
|
+
local fn = swapState
|
|
209
|
+
return fn(
|
|
210
|
+
ffi.cast("void*", call),
|
|
211
|
+
state
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
local getLastError = ffi.cast("void*", C.GetLastError)
|
|
215
|
+
local heapFree = ffi.cast("void*", C.HeapFree)
|
|
216
|
+
local processHeap = C.GetProcessHeap()
|
|
217
|
+
local HEAP_ZERO_MEMORY = 8
|
|
218
|
+
--- The address of a pointer, as a key of a table.
|
|
219
|
+
local function address(ptr)
|
|
220
|
+
return tonumber(ffi.cast("uintptr_t", ptr))
|
|
221
|
+
end
|
|
222
|
+
--- Memory on the process heap and the calls on threads that use it, released
|
|
223
|
+
-- together.
|
|
224
|
+
____exports.Pool = __TS__Class()
|
|
225
|
+
local Pool = ____exports.Pool
|
|
226
|
+
Pool.name = "Pool"
|
|
227
|
+
function Pool.prototype.____constructor(self)
|
|
228
|
+
self.blocks = {}
|
|
229
|
+
self.calls = {}
|
|
230
|
+
self.released = false
|
|
231
|
+
end
|
|
232
|
+
function Pool.prototype.alloc(self, ctype, size)
|
|
233
|
+
if self.released then
|
|
234
|
+
error(
|
|
235
|
+
__TS__New(Error, "the pool is released"),
|
|
236
|
+
0
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
local mem = C.HeapAlloc(processHeap, HEAP_ZERO_MEMORY, size)
|
|
240
|
+
if mem == nil then
|
|
241
|
+
error(
|
|
242
|
+
__TS__New(
|
|
243
|
+
Error,
|
|
244
|
+
("cannot allocate " .. tostring(size)) .. " bytes"
|
|
245
|
+
),
|
|
246
|
+
0
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
self.blocks[address(mem)] = mem
|
|
250
|
+
return ffi.cast(ctype, mem)
|
|
251
|
+
end
|
|
252
|
+
function Pool.prototype.free(self, mem)
|
|
253
|
+
local key = address(mem)
|
|
254
|
+
local block = self.blocks[key]
|
|
255
|
+
if block ~= nil then
|
|
256
|
+
self.blocks[key] = nil
|
|
257
|
+
C.HeapFree(processHeap, 0, block)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
function Pool.prototype.run(self, fn, ...)
|
|
261
|
+
local args = {...}
|
|
262
|
+
if #args > MAX_ARGS then
|
|
263
|
+
error(
|
|
264
|
+
__TS__New(
|
|
265
|
+
Error,
|
|
266
|
+
("a call on a thread takes at most " .. tostring(MAX_ARGS)) .. " arguments"
|
|
267
|
+
),
|
|
268
|
+
0
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
local call = self:alloc(
|
|
272
|
+
Call:ptr().name,
|
|
273
|
+
Call.size
|
|
274
|
+
)
|
|
275
|
+
call.fn = ffi.cast("void*", fn)
|
|
276
|
+
call.argc = #args
|
|
277
|
+
call.get_last_error = getLastError
|
|
278
|
+
call.heap_free = heapFree
|
|
279
|
+
call.heap = processHeap
|
|
280
|
+
local copies = {}
|
|
281
|
+
local owned = {}
|
|
282
|
+
__TS__ArrayForEach(
|
|
283
|
+
args,
|
|
284
|
+
function(____, arg, i)
|
|
285
|
+
if type(arg) == "number" then
|
|
286
|
+
call.args[i] = arg
|
|
287
|
+
elseif type(arg) == "string" then
|
|
288
|
+
local copy = self:alloc("char*", #arg + 1)
|
|
289
|
+
ffi.copy(copy, arg)
|
|
290
|
+
copies[#copies + 1] = copy
|
|
291
|
+
owned[#owned + 1] = copy
|
|
292
|
+
call.args[i] = ffi.cast("uintptr_t", copy)
|
|
293
|
+
else
|
|
294
|
+
call.args[i] = ffi.cast("uintptr_t", arg)
|
|
295
|
+
if self.blocks[address(arg)] ~= nil then
|
|
296
|
+
owned[#owned + 1] = arg
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
)
|
|
301
|
+
__TS__ArrayForEach(
|
|
302
|
+
owned,
|
|
303
|
+
function(____, mem, i)
|
|
304
|
+
call.owned[i] = mem
|
|
305
|
+
end
|
|
306
|
+
)
|
|
307
|
+
call.owned_count = #owned
|
|
308
|
+
local pending = {
|
|
309
|
+
abandoned = false,
|
|
310
|
+
owned = __TS__ArrayMap(
|
|
311
|
+
owned,
|
|
312
|
+
function(____, mem) return address(mem) end
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
local function done()
|
|
316
|
+
for ____, copy in ipairs(copies) do
|
|
317
|
+
self:free(copy)
|
|
318
|
+
end
|
|
319
|
+
self:free(call)
|
|
320
|
+
end
|
|
321
|
+
local thread = C.CreateThread(
|
|
322
|
+
nil,
|
|
323
|
+
0,
|
|
324
|
+
ffi.cast(
|
|
325
|
+
"void*",
|
|
326
|
+
getCode() + CODE.labels.entry
|
|
327
|
+
),
|
|
328
|
+
ffi.cast("void*", call),
|
|
329
|
+
0,
|
|
330
|
+
nil
|
|
331
|
+
)
|
|
332
|
+
if thread == nil then
|
|
333
|
+
local ____error = C.GetLastError()
|
|
334
|
+
done()
|
|
335
|
+
error(
|
|
336
|
+
__TS__New(
|
|
337
|
+
Error,
|
|
338
|
+
"cannot create a thread: error " .. tostring(____error)
|
|
339
|
+
),
|
|
340
|
+
0
|
|
341
|
+
)
|
|
342
|
+
end
|
|
343
|
+
C.CloseHandle(thread)
|
|
344
|
+
self.calls[call] = pending
|
|
345
|
+
while true do
|
|
346
|
+
if pending.abandoned then
|
|
347
|
+
error(
|
|
348
|
+
__TS__New(Error, "the call was cancelled"),
|
|
349
|
+
0
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
if call.state ~= RUNNING then
|
|
353
|
+
break
|
|
354
|
+
end
|
|
355
|
+
coroutine.yield()
|
|
356
|
+
end
|
|
357
|
+
self.calls[call] = nil
|
|
358
|
+
local result = {call.result, call.error}
|
|
359
|
+
done()
|
|
360
|
+
return result
|
|
361
|
+
end
|
|
362
|
+
function Pool.prototype.release(self)
|
|
363
|
+
if self.released then
|
|
364
|
+
return
|
|
365
|
+
end
|
|
366
|
+
self.released = true
|
|
367
|
+
for call, pending in pairs(self.calls) do
|
|
368
|
+
pending.abandoned = true
|
|
369
|
+
if swap(call, ABANDONED) == RUNNING then
|
|
370
|
+
self.blocks[address(call)] = nil
|
|
371
|
+
for ____, mem in ipairs(pending.owned) do
|
|
372
|
+
self.blocks[mem] = nil
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
self.calls = {}
|
|
377
|
+
for ____, block in pairs(self.blocks) do
|
|
378
|
+
C.HeapFree(processHeap, 0, block)
|
|
379
|
+
end
|
|
380
|
+
self.blocks = {}
|
|
381
|
+
end
|
|
382
|
+
return ____exports
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noita-ts/wininet",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An HTTP client for unsafe Noita mods that uses WinINet on native threads, so the game does not stop.",
|
|
5
|
+
"author": "Anton Bulakh <him@necauq.ua> (https://necauq.ua/)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*.{lua,d.ts}"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tstl -p .",
|
|
21
|
+
"prepublishOnly": "rm -rf dist && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@noita-ts/ffi": "^0.7.0",
|
|
25
|
+
"@typescript-to-lua/language-extensions": "^1.19.0",
|
|
26
|
+
"lua-types": "^2.13.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@noita-ts/nasm": "^0.1.0",
|
|
30
|
+
"typescript": "^6.0.2",
|
|
31
|
+
"typescript-to-lua": "^1.36.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @noita-ts/wininet
|
|
2
|
+
|
|
3
|
+
An HTTP client for unsafe [noita-ts](https://github.com/necauqua/noita-ts) mods
|
|
4
|
+
that uses WinINet through the LuaJIT FFI. WinINet is part of Windows (and of
|
|
5
|
+
Wine), so the package ships no DLL. The blocking WinINet calls run on native
|
|
6
|
+
threads, so the game does not stop while a request waits.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
A mod that only makes HTTP requests does not have to ship the pollnet DLL of
|
|
11
|
+
`@noita-ts/pollnet` (3.5 MB) any more. This package is only Lua, and WinINet is
|
|
12
|
+
already on every Windows (and in Wine). Use pollnet when you need its other
|
|
13
|
+
parts, such as WebSockets, TCP or servers.
|
|
14
|
+
|
|
15
|
+
There is also a bug that WinINet does not have: in Wine (Proton), the HTTPS of
|
|
16
|
+
pollnet (rust `schannel` on the `schannel` of Wine) stops in a response that
|
|
17
|
+
has more than one TLS record (24 KB is enough). It waits until the server
|
|
18
|
+
closes the connection (65 s for nginx), and then it often fails. With WinINet,
|
|
19
|
+
a 1.6 MB download takes about 1 s, with the game running.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The API is the same as `HttpClient` of `@noita-ts/pollnet`, so a mod can change
|
|
24
|
+
only the import:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import Scheduler from "@noita-ts/base/async";
|
|
28
|
+
import { HttpClient } from "@noita-ts/wininet";
|
|
29
|
+
|
|
30
|
+
const http = new HttpClient(Scheduler.get());
|
|
31
|
+
|
|
32
|
+
const response = await http.get("https://example.com/", { Accept: "text/html" });
|
|
33
|
+
print(response.status, response.parseHeaders()["Content-Type"]);
|
|
34
|
+
|
|
35
|
+
await http.post("https://example.com/api", { "Content-Type": "application/json" }, "{}");
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `new HttpClient(scheduler, options?)`: `scheduler` is anything with
|
|
39
|
+
`spawn` (a `Scheduler` of `@noita-ts/base/async`). The options are
|
|
40
|
+
`userAgent` (default `noita-ts/wininet`) and `timeoutMs`, the timeout of a
|
|
41
|
+
connection, of a send and of a receive (default 30 s). A client keeps a
|
|
42
|
+
WinINet session until the Lua state is closed, so make one and keep it.
|
|
43
|
+
- `get(url, headers?)` and `post(url, headers?, body?)` resolve to an
|
|
44
|
+
`HttpResponse` with `status`, `headers` (`Key: value` lines separated by
|
|
45
|
+
`\n`), `body` and `parseHeaders()`. Headers are a map (an array value
|
|
46
|
+
sends the header more than once) or a string of `\n`-separated lines. Without
|
|
47
|
+
headers, a POST sends `content-type: application/x-www-form-urlencoded`, as
|
|
48
|
+
in pollnet.
|
|
49
|
+
- An error of WinINet rejects the promise with an `Error` that has the method,
|
|
50
|
+
the URL and the WinINet error, for example
|
|
51
|
+
`GET http://127.0.0.1:1/: cannot send the request: WinINet error 12029 (cannot connect)`.
|
|
52
|
+
- Redirects are followed, also between http and https.
|
|
53
|
+
|
|
54
|
+
## Limits
|
|
55
|
+
|
|
56
|
+
- Only `http` and `https` URLs with a host name or an IPv4 address. Use
|
|
57
|
+
`127.0.0.1`, not `localhost`: WinINet can take `localhost` as `::1`.
|
|
58
|
+
- The whole body is kept in memory, as one string.
|
|
59
|
+
- Each blocking call starts a new native thread; the Lua side looks at the
|
|
60
|
+
result once per frame, so every request takes at least a few frames.
|
|
61
|
+
- When the Lua state is closed (for example a new game starts), every client
|
|
62
|
+
closes its WinINet handles and frees its memory. A request that waits at
|
|
63
|
+
that moment cannot be stopped in Wine: its thread runs until the call
|
|
64
|
+
returns or times out, and then frees the memory of the call itself. Windows
|
|
65
|
+
cancels such a call when its handle is closed.
|
|
66
|
+
- Wine gives a timeout as WinINet error 12030 (connection aborted), not 12002.
|
package/README.md
DELETED