diff options
| author | ruki <[email protected]> | 2019-11-04 22:20:02 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2019-11-04 22:20:02 +0800 |
| commit | b1eee1ec65dd0f9d3a26af52596d86c331160587 (patch) | |
| tree | 6811a354f07c146ba4417e649d3a31d0c9aabd45 /xmake/core/base | |
| parent | 20b40c57bbe9d28f822d5feac304a5b892b02790 (diff) | |
| parent | f00316a42943a9a75303658f8d646d35f9e8d1b9 (diff) | |
Merge pull request #593 from xmake-io/socket
Add io.socket support
Diffstat (limited to 'xmake/core/base')
| -rw-r--r-- | xmake/core/base/bytes.lua | 441 | ||||
| -rw-r--r-- | xmake/core/base/io.lua | 180 | ||||
| -rw-r--r-- | xmake/core/base/socket.lua | 581 |
3 files changed, 1153 insertions, 49 deletions
diff --git a/xmake/core/base/bytes.lua b/xmake/core/base/bytes.lua new file mode 100644 index 000000000..6c62697aa --- /dev/null +++ b/xmake/core/base/bytes.lua @@ -0,0 +1,441 @@ +--!A cross-platform build utility based on Lua +-- +-- 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. +-- +-- Copyright (C) 2015 - 2019, TBOOX Open Source Group. +-- +-- @author ruki +-- @file bytes.lua +-- + +-- define module: bytes +local bytes = bytes or {} +local _instance = _instance or {} + +-- load modules +local bit = require('bit') +local ffi = require('ffi') +local os = require("base/os") +local utils = require("base/utils") + +-- define ffi interfaces +ffi.cdef[[ + void* malloc(size_t size); + void free(void* data); +]] + +-- new a bytes instance +-- +-- bytes(size): allocates a buffer of given size +-- bytes(size, ptr [, manage]): mounts buffer on existing storage (manage memory or not) +-- bytes(str): mounts a buffer from the given string +-- bytes(bytes, start, last): mounts a buffer from another one, with start/last limits +-- bytes(bytes1, bytes2, bytes3, ...): allocates and concat buffer from list of byte buffers +-- bytes(bytes): allocates a buffer from another one (strict replica, sharing memory) +-- bytes({bytes1, bytes2, ...}): allocates and concat buffer from a list of byte buffers (table) +-- +function _instance.new(...) + local args = {...} + local arg1, arg2, arg3 = unpack(args) + local instance = table.inherit(_instance) + if type(arg1) == "number" then + local size = arg1 + local ptr = arg2 + if ptr then + -- bytes(size, ptr [, manage]): mounts buffer on existing storage (manage memory or not) + local manage = arg3 + if manage then + instance._CDATA = ffi.gc(ffi.cast("unsigned char*", ptr), ffi.C.free) + instance._MANAGED = true + else + instance._CDATA = ffi.cast("unsigned char*", ptr) + instance._MANAGED = false + end + else + -- bytes(size): allocates a buffer of given size + ptr = ffi.C.malloc(size) + instance._CDATA = ffi.gc(ffi.cast("unsigned char*", ptr), ffi.C.free) + instance._MANAGED = true + end + instance._SIZE = size + instance._READONLY = false + elseif type(arg1) == "string" then + -- bytes(str): mounts a buffer from the given string + local str = arg1 + instance._SIZE = #str + instance._CDATA = ffi.cast("unsigned char*", str) + instance._REF = str -- keep ref for GC + instance._MANAGED = false + instance._READONLY = true + elseif type(arg1) == "table" then + if type(arg2) == 'number' then + -- bytes(bytes, start, last): mounts a buffer from another one, with start/last limits: + local b = arg1 + local start = arg2 or 1 + local last = arg3 or b:size() + if start < 1 or last > b:size() then + os.raise("incorrect bounds(%d-%d) for bytes(...)!", start, last) + end + instance._SIZE = last - start + 1 + instance._CDATA = b:cdata() - 1 + start + instance._REF = b -- keep lua ref for GC + instance._MANAGED = false + instance._READONLY = b:readonly() + elseif type(arg2) == "table" then + -- bytes(bytes1, bytes2, bytes3, ...): allocates and concat buffer from list of byte buffers + instance._SIZE = 0 + for _, b in ipairs(args) do + instance._SIZE = instance._SIZE + b:size() + end + instance._CDATA = ffi.gc(ffi.cast("unsigned char*", ffi.C.malloc(instance._SIZE)), ffi.C.free) + local offset = 0 + for _, b in ipairs(args) do + ffi.copy(instance._CDATA + offset, b:cdata(), b:size()) + offset = offset + b:size() + end + instance._MANAGED = true + instance._READONLY = false + elseif not arg2 and arg1[1] and type(arg1[1]) == 'table' then + -- bytes({bytes1, bytes2, ...}): allocates and concat buffer from a list of byte buffers (table) + args = arg1 + instance._SIZE = 0 + for _, b in ipairs(args) do + instance._SIZE = instance._SIZE + b:size() + end + instance._CDATA = ffi.gc(ffi.cast("unsigned char*", ffi.C.malloc(instance._SIZE)), ffi.C.free) + local offset = 0 + for _, b in ipairs(args) do + ffi.copy(instance._CDATA + offset, b._CDATA, b:size()) + offset = offset + b:size() + end + instance._MANAGED = true + instance._READONLY = false + elseif not arg2 and arg1:size() then + -- bytes(bytes): allocates a buffer from another one (strict replica, sharing memory) + local b = arg1 + local start = 1 + local last = arg3 or b:size() + if start < 1 or last > b:size() then + os.raise("incorrect bounds(%d-%d)!", start, last) + end + instance._SIZE = last - start + 1 + instance._CDATA = b:cdata() - 1 + start + instance._REF = b -- keep lua ref for GC + instance._MANAGED = false + instance._READONLY = b:readonly() + end + end + if instance:cdata() == nil then + os.raise("invalid arguments for bytes(...)!") + end + setmetatable(instance, _instance) + return instance +end + +-- get bytes size +function _instance:size() + return self._SIZE +end + +-- get bytes data +function _instance:cdata() + return self._CDATA +end + +-- get data address +function _instance:caddr() + return tonumber(ffi.cast('long', self:cdata())) +end + +-- readonly? +function _instance:readonly() + return self._READONLY +end + +-- bytes:ipairs() +function _instance:ipairs() + local index = 0 + return function (...) + if index < self:size() then + index = index + 1 + return index, self[index] + end + end +end + +-- get a slice of bytes +function _instance:slice(start, last) + return bytes(self, start, last) +end + +-- copy bytes +function _instance:copy(src) + if self:readonly() then + os.raise("%s: cannot be modified!", self) + end + if type(src) == 'string' then + src = bytes(src) + end + if src:size() ~= self:size() then + os.raise("%s: cannot copy bytes, src and dst must have same size(%d->%d)!", self, src:size(), self:size()) + end + ffi.copy(self:cdata(), src:cdata(), self:size()) + return self +end + +-- clone a new bytes buffer +function _instance:clone() + local new = bytes(self:size()) + new:copy(self) + return new +end + +-- dump whole bytes data +function _instance:dump() + + local i = 0 + local n = 147 + local p = 0 + local e = self:size() + local line = nil + while p < e do + line = "" + if p + 0x20 <= e then + + -- dump offset + line = line .. string.format("${yellow}%08X ${green}", p) + + -- dump data + for i = 0, 0x20 - 1 do + if (i % 4) == 0 then + line = line .. " " + end + line = line .. string.format(" %02X", self[p + i + 1]) + end + + -- dump spaces + line = line .. " " + + -- dump characters + line = line .. "${magenta}" + for i = 0, 0x20 - 1 do + local v = self[p + i + 1] + if v > 0x1f and v < 0x7f then + line = line .. string.format("%c", v) + else + line = line .. '.' + end + end + line = line .. "${clear}" + + -- dump line + utils.cprint(line) + + -- next line + p = p + 0x20 + + elseif p < e then + + -- init padding + local padding = n - 0x20 + + -- dump offset + line = line .. string.format("${yellow}%08X ${green}", p) + if padding >= 9 then + padding = padding - 9 + end + + -- dump data + local left = e - p + for i = 0, left - 1 do + if (i % 4) == 0 then + line = line .. " " + if padding then + padding = padding - 1 + end + end + line = line .. string.format(" %02X", self[p + i + 1]) + if padding >= 3 then + padding = padding - 3 + end + end + + -- dump spaces + while padding > 0 do + line = line .. " " + padding = padding - 1 + end + + -- dump characters + line = line .. "${magenta}" + for i = 0, left - 1 do + local v = self[p + i + 1] + if v > 0x1f and v < 0x7f then + line = line .. string.format("%c", v) + else + line = line .. '.' + end + end + line = line .. "${clear}" + + + -- dump line + utils.cprint(line) + + -- next line + p = p + left + + else + break + end + end +end + +-- convert bytes to string +function _instance:str(i, j) + local offset = i and i - 1 or 0 + return ffi.string(self:cdata() + offset, (j or self:size()) - offset) +end + +-- get uint8 value +function _instance:u8(offset) + return self[offset] +end + +-- get sint8 value +function _instance:s8(offset) + local value = self[offset] + return value < 0x80 and value or -0x100 + value +end + +-- get uint16 little-endian value +function _instance:u16le(offset) + return bit.lshift(self[offset + 1], 8) + self[offset] +end + +-- get uint16 big-endian value +function _instance:u16be(offset) + return bit.lshift(self[offset], 8) + self[offset + 1] +end + +-- get sint16 little-endian value +function _instance:s16le(offset) + local value = self:u16le(offset) + return value < 0x8000 and value or -0x10000 + value +end + +-- get sint16 big-endian value +function _instance:s16be(offset) + local value = self:u16be(offset) + return value < 0x8000 and value or -0x10000 + value +end + +-- get uint32 little-endian value +function _instance:u32le(offset) + return self[offset + 3] * 0x1000000 + bit.lshift(self[offset + 2], 16) + bit.lshift(self[offset + 1], 8) + self[offset] +end + +-- get uint32 big-endian value +function _instance:u32be(offset) + return self[offset] * 0x1000000 + bit.lshift(self[offset + 1], 16) + bit.lshift(self[offset + 2], 8) + self[offset + 3] +end + +-- get sint32 little-endian value +function _instance:s32le(offset) + return bit.tobit(self:u32le(offset)) +end + +-- get sint32 big-endian value +function _instance:s32be(offset) + return bit.tobit(self:u32be(offset)) +end + +-- get byte or bytes slice at the given index position +-- +-- bytes[1] +-- bytes[{1, 2}] +-- +function _instance:__index(key) + if type(key) == "number" then + if key < 1 or key > self:size() then + os.raise("%s: index(%d/%d) out of bounds!", self, key, self:size()) + end + return self._CDATA[key - 1] + elseif type(key) == "table" then + local start, last = key[1], key[2] + return self:slice(start, last) + end + return rawget(self, key) +end + +-- set byte or bytes slice at the given index position +-- +-- bytes[1] = 0x1 +-- bytes[{1, 2}] = bytes(2) +-- +function _instance:__newindex(key, value) + if self:readonly() then + os.raise("%s: cannot modify value at index[%s]!", self, key) + end + if type(key) == "number" then + if key < 1 or key > self:size() then + os.raise("%s: index(%d/%d) out of bounds!", self, key, self:size()) + end + self._CDATA[key - 1] = value + return + elseif type(key) == "table" then + local start, last = key[1], key[2] + self:slice(start,last):copy(value) + return + end + rawset(self, key, value) +end + +-- concat two bytes buffer +function _instance:__concat(other) + local new = bytes(self:size() + other:size()) + new:slice(1, self:size()):copy(self) + new:slice(self:size() + 1, new:size()):copy(other) + return new +end + +-- tostring(bytes) +function _instance:__tostring() + local parts = {} + local size = self:size() + if size > 8 then + size = 8 + end + for i = 1, size do + parts[i] = "0x" .. bit.tohex(self[i], 2) + end + return "<bytes(" .. self:size() .. "): " .. table.concat(parts, " ") .. (self:size() > 8 and "..>" or ">") +end + +-- new an bytes instance +function bytes.new(...) + return _instance.new(...) +end + +-- register call function +setmetatable(bytes, { + __call = function (_, ...) + return bytes.new(...) + end, + __tostring = function() + return "<bytes>" + end +}) + +-- return module: bytes +return bytes diff --git a/xmake/core/base/io.lua b/xmake/core/base/io.lua index f6c931970..6dc13ca30 100644 --- a/xmake/core/base/io.lua +++ b/xmake/core/base/io.lua @@ -55,10 +55,15 @@ end -- close file function _file:close() - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end - local ok, errors = io.file_close(self._FILE) + + -- close file + ok, errors = io.file_close(self._FILE) if ok then self._FILE = nil end @@ -67,7 +72,11 @@ end -- tostring(file) function _file:__tostring() - return "file: " .. self:name() + local str = self:path() + if #str > 16 then + str = ".." .. str:sub(#str - 16, #str) + end + return "<file: " .. str .. ">" end -- gc(file) @@ -84,101 +93,142 @@ end -- get file rawfd function _file:rawfd() - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return nil, errors end + + -- get file rawfd local result, errors = io.file_rawfd(self._FILE) if not result and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return result, errors end -- get file size function _file:size() - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return nil, errors end + + -- get file size local result, errors = io.file_size(self._FILE) if not result and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return result, errors end -- read data from file function _file:read(fmt, opt) - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return nil, errors end + + -- read file opt = opt or {} local result, errors = io.file_read(self._FILE, fmt, opt.continuation) if errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return result, errors end -- write data to file function _file:write(...) - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end - local ok, errors = io.file_write(self._FILE, ...) + + -- write file + ok, errors = io.file_write(self._FILE, ...) if not ok and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return ok, errors end -- seek offset at file function _file:seek(whence, offset) - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end + + -- seek file local result, errors = io.file_seek(self._FILE, whence, offset) if not result and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return result, errors end -- flush data to file function _file:flush() - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end - local ok, errors = io.file_flush(self._FILE) + + -- flush file + ok, errors = io.file_flush(self._FILE) if not ok and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return ok, errors end -- this file is a tty? function _file:isatty() - if not self._FILE then - return false, string.format("file(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return nil, errors end - local ok, errors = io.file_isatty(self._FILE) + + -- is a tty? + ok, errors = io.file_isatty(self._FILE) if ok == nil and errors then - errors = string.format("file(%s): %s", self:name(), errors) + errors = string.format("%s: %s", self, errors) end return ok, errors end --- iterator of lines -function _file._lines_iter(data) - local l = data.file:read("l", data.opt) - if not l and data.opt.close_on_finished then - data.file:close() +-- ensure the file is opened +function _file:_ensure_opened() + if not self._FILE then + return false, string.format("%s: has been closed!", self) end - return l + return true end --- read all lines from a file +-- read all lines from file function _file:lines(opt) - return _file._lines_iter, { file = assert(self), opt = opt or {} } + opt = opt or {} + return function() + local l = self:read("l", opt) + if not l and opt.close_on_finished then + self:close() + end + return l + end end -- print file @@ -245,14 +295,19 @@ end -- @return ok, errors -- function _filelock:lock(opt) - if not self._LOCK then - return false, string.format("filelock(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end + + -- lock it if self._LOCKED_NUM > 0 or io.filelock_lock(self._LOCK, opt) then self._LOCKED_NUM = self._LOCKED_NUM + 1 return true else - return false, string.format("filelock(%s): lock %s failed!", self:name(), self:path()) + return false, string.format("%s: lock failed!", self) end end @@ -263,22 +318,32 @@ end -- @return ok, errors -- function _filelock:trylock(opt) - if not self._LOCK then - return false, string.format("filelock(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end + + -- try lock it if self._LOCKED_NUM > 0 or io.filelock_trylock(self._LOCK, opt) then self._LOCKED_NUM = self._LOCKED_NUM + 1 return true else - return false, string.format("filelock(%s): trylock %s failed!", self:name(), self:path()) + return false, string.format("%s: trylock failed!", self) end end -- unlock file function _filelock:unlock(opt) - if not self._LOCK then - return false, string.format("filelock(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end + + -- unlock it if self._LOCKED_NUM > 1 or (self._LOCKED_NUM > 0 and io.filelock_unlock(self._LOCK)) then if self._LOCKED_NUM > 0 then self._LOCKED_NUM = self._LOCKED_NUM - 1 @@ -287,16 +352,21 @@ function _filelock:unlock(opt) end return true else - return false, string.format("filelock(%s): unlock %s failed!", self:name(), self:path()) + return false, string.format("%s: unlock failed!", self) end end -- close filelock function _filelock:close() - if not self._LOCK then - return false, string.format("filelock(%s) has been closed!", self:name()) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors end - local ok = io.filelock_close(self._LOCK) + + -- close it + ok = io.filelock_close(self._LOCK) if ok then self._LOCK = nil self._LOCKED_NUM = 0 @@ -304,9 +374,21 @@ function _filelock:close() return ok end +-- ensure the file is opened +function _filelock:_ensure_opened() + if not self._LOCK then + return false, string.format("%s: has been closed!", self) + end + return true +end + -- tostring(filelock) function _filelock:__tostring() - return "filelock: " .. self:name() + local str = self:path() + if #str > 16 then + str = ".." .. str:sub(#str - 16, #str) + end + return "<filelock: " .. str .. ">" end -- gc(filelock) diff --git a/xmake/core/base/socket.lua b/xmake/core/base/socket.lua new file mode 100644 index 000000000..4a2dfdcdc --- /dev/null +++ b/xmake/core/base/socket.lua @@ -0,0 +1,581 @@ +--!A cross-platform build utility based on Lua +-- +-- 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. +-- +-- Copyright (C) 2015 - 2019, TBOOX Open Source Group. +-- +-- @author ruki +-- @file socket.lua +-- + +-- define module +local socket = socket or {} +local _instance = _instance or {} + +-- load modules +local io = require("base/io") +local bytes = require("base/bytes") +local table = require("base/table") +local string = require("base/string") + +-- the socket types +socket.TCP = 1 +socket.UDP = 2 +socket.ICMP = 3 + +-- the socket families +socket.IPV4 = 1 +socket.IPV6 = 2 + +-- the socket events +socket.EV_RECV = 1 +socket.EV_SEND = 2 +socket.EV_CONN = socket.EV_SEND +socket.EV_ACPT = socket.EV_RECV + +-- new a socket +function _instance.new(socktype, family, sock) + local instance = table.inherit(_instance) + instance._SOCK = sock + instance._TYPE = socktype + instance._FAMILY = family + setmetatable(instance, _instance) + return instance +end + +-- get socket type +function _instance:type() + return self._TYPE +end + +-- get socket family +function _instance:family() + return self._FAMILY +end + +-- get socket rawfd +function _instance:rawfd() + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return nil, errors + end + + -- get rawfd + local result, errors = io.socket_rawfd(self._SOCK) + if not result and errors then + errors = string.format("%s: %s", self, errors) + end + return result, errors +end + +-- bind socket +function _instance:bind(addr, port) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- bind it + local ok, errors = io.socket_bind(self._SOCK, addr, port, self:family()) + if not ok and errors then + errors = string.format("%s: %s", self, errors) + end + return ok, errors +end + +-- listen socket +function _instance:listen(backlog) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- listen it + local ok, errors = io.socket_listen(self._SOCK, backlog or 10) + if not ok and errors then + errors = string.format("%s: %s", self, errors) + end + return ok, errors +end + +-- accept socket +function _instance:accept(opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- accept it + local sock, errors = io.socket_accept(self._SOCK) + if not sock and not errors then + opt = opt or {} + local events, waiterrs = self:wait(socket.EV_ACPT, opt.timeout or -1) + if events == socket.EV_ACPT then + sock, errors = io.socket_accept(self._SOCK) + else + errors = waiterrs + end + end + if not sock and errors then + errors = string.format("%s: %s", self, errors) + end + if sock then + sock = _instance.new(self:type(), self:family(), sock) + end + return sock, errors +end + +-- connect socket +function _instance:connect(addr, port, opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- connect it + local ok, errors = io.socket_connect(self._SOCK, addr, port, self:family()) + if ok == 0 then + opt = opt or {} + local events, waiterrs = self:wait(socket.EV_CONN, opt.timeout or -1) + if events == socket.EV_CONN then + ok, errors = io.socket_connect(self._SOCK, addr, port, self:family()) + else + errors = waiterrs + end + end + if ok < 0 and errors then + errors = string.format("%s: %s", self, errors) + end + return ok, errors +end + +-- send data to socket +function _instance:send(data, opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- data is bytes? unpack the raw address + local datasize = #data + if type(data) == "table" and data.caddr then + datasize = data:size() + data = {data = data:caddr(), size = data:size()} + end + + -- init start and last + opt = opt or {} + local start = opt.start or 1 + local last = opt.last or datasize + + -- check start and last + if start > last or start < 1 then + return -1, string.format("%s: invalid start(%d) and last(%d)!", self, start, last) + end + + -- send it + local send = 0 + local real = 0 + local wait = false + local errors = nil + if opt.block then + local size = last + 1 - start + while start <= last do + real, errors = io.socket_send(self._SOCK, data, start, last) + if real > 0 then + send = send + real + start = start + real + wait = false + elseif real == 0 and not wait then + local events, waiterrs = self:wait(socket.EV_SEND, opt.timeout or -1) + if events == socket.EV_SEND then + wait = true + else + errors = waiterrs + break + end + else + break + end + end + if send ~= size then + send = -1 + end + else + send, errors = io.socket_send(self._SOCK, data, start, last) + if send < 0 and errors then + errors = string.format("%s: %s", self, errors) + end + end + return send, errors +end + +-- send file to socket +function _instance:sendfile(file, opt) + + -- ensure the socket opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- ensure the file opened + local ok, errors = file:_ensure_opened() + if not ok then + return -1, errors + end + + -- init start and last + opt = opt or {} + local start = opt.start or 1 + local last = opt.last or file:size() + + -- check start and last + if start > last or start < 1 then + return -1, string.format("%s: invalid start(%d) and last(%d)!", self, start, last) + end + + -- send it + local send = 0 + local real = 0 + local wait = false + local errors = nil + if opt.block then + local size = last + 1 - start + while start <= last do + real, errors = io.socket_sendfile(self._SOCK, file._FILE, start, last) + if real > 0 then + send = send + real + start = start + real + wait = false + elseif real == 0 and not wait then + local events, waiterrs = self:wait(socket.EV_SEND, opt.timeout or -1) + if events == socket.EV_SEND then + wait = true + else + errors = waiterrs + break + end + else + break + end + end + if send ~= size then + send = -1 + end + else + send, errors = io.socket_sendfile(self._SOCK, file._FILE, start, last) + if send < 0 and errors then + errors = string.format("%s: %s", self, errors) + end + end + return send, errors +end + +-- recv data from socket +function _instance:recv(size, opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- check size + if size == 0 then + return 0 + elseif size == nil or size < 0 then + return -1, string.format("%s: invalid size(%d)!", self, size) + end + + -- recv it + opt = opt or {} + local recv = 0 + local real = 0 + local wait = false + local data_or_errors = nil + if opt.block then + local results = {} + while recv < size do + real, data_or_errors = io.socket_recv(self._SOCK, size - recv) + if real > 0 then + recv = recv + real + wait = false + table.insert(results, bytes(data_or_errors)) + elseif real == 0 and not wait then + local events, waiterrs = self:wait(socket.EV_RECV, opt.timeout or -1) + if events == socket.EV_RECV then + wait = true + else + data_or_errors = waiterrs + break + end + else + break + end + end + if recv == size then + data_or_errors = bytes(results) + else + recv = -1 + end + else + recv, data_or_errors = io.socket_recv(self._SOCK, size) + if recv > 0 then + data_or_errors = bytes(data_or_errors) + end + end + if recv < 0 and data_or_errors then + data_or_errors = string.format("%s: %s", self, data_or_errors) + end + return recv, data_or_errors +end + +-- send udp data to peer +function _instance:sendto(data, addr, port, opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- only for udp + if self:type() ~= socket.UDP then + return -1, string.format("%s: sendto() only for udp socket!", self) + end + + -- check address + if not addr or not port then + return -1, string.format("%s: sendto empty address!", self) + end + + -- data is bytes? unpack the raw address + if type(data) == "table" and data.caddr then + data = {data = data:caddr(), size = data:size()} + end + + -- send it + opt = opt or {} + local send = 0 + local wait = false + local errors = nil + if opt.block then + while true do + send, errors = io.socket_sendto(self._SOCK, data, addr, port, self:family()) + if send == 0 and not wait then + local events, waiterrs = self:wait(socket.EV_SEND, opt.timeout or -1) + if events == socket.EV_SEND then + wait = true + else + errors = waiterrs + break + end + else + break + end + end + else + send, errors = io.socket_sendto(self._SOCK, data, addr, port, self:family()) + if send < 0 and errors then + errors = string.format("%s: %s", self, errors) + end + end + return send, errors +end + +-- recv udp data from peer +function _instance:recvfrom(size, opt) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- only for udp + if self:type() ~= socket.UDP then + return -1, string.format("%s: sendto() only for udp socket!", self) + end + + -- check size + if size == 0 then + return 0 + elseif size == nil or size < 0 then + return -1, string.format("%s: invalid size(%d)!", self, size) + end + + -- recv it + opt = opt or {} + local recv = 0 + local wait = false + local data_or_errors = nil + if opt.block then + while true do + recv, data_or_errors, addr, port = io.socket_recvfrom(self._SOCK, size) + if recv > 0 then + data_or_errors = bytes(data_or_errors) + break + elseif recv == 0 and not wait then + local events, waiterrs = self:wait(socket.EV_RECV, opt.timeout or -1) + if events == socket.EV_RECV then + wait = true + else + recv = -1 + data_or_errors = waiterrs + break + end + else + break + end + end + else + recv, data_or_errors, addr, port = io.socket_recvfrom(self._SOCK, size) + if recv > 0 then + data_or_errors = bytes(data_or_errors) + end + end + if recv < 0 and data_or_errors then + data_or_errors = string.format("%s: %s", self, data_or_errors) + end + return recv, data_or_errors, addr, port +end + +-- wait socket events +function _instance:wait(events, timeout) + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return -1, errors + end + + -- wait it + local events, errors = io.socket_wait(self._SOCK, events, timeout or -1) + if events < 0 and errors then + errors = string.format("%s: %s", self, errors) + end + return events, errors +end + +-- close socket +function _instance:close() + + -- ensure opened + local ok, errors = self:_ensure_opened() + if not ok then + return false, errors + end + + -- close it + ok = io.socket_close(self._SOCK) + if ok then + self._SOCK = nil + end + return ok +end + +-- ensure the socket is opened +function _instance:_ensure_opened() + if not self._SOCK then + return false, string.format("%s: has been closed!", self) + end + return true +end + +-- tostring(socket) +function _instance:__tostring() + local rawfd = self:rawfd() or "closed" + local types = {"tcp", "udp", "icmp"} + return string.format("<socket: %s%s/%s>", types[self:type()], self:family() == socket.IPV6 and "6" or "4", rawfd) +end + +-- gc(socket) +function _instance:__gc() + if self._SOCK and io.socket_close(self._SOCK) then + self._SOCK = nil + end +end + +-- open a socket +-- +-- @param socktype the socket type, e.g. tcp, udp, icmp +-- @param family the address family, e.g. ipv4, ipv6 +-- +-- @return the socket instance +-- +function socket.open(socktype, family) + socktype = socktype or socket.TCP + family = family or socket.IPV4 + local sock, errors = io.socket_open(socktype, family) + if sock then + return _instance.new(socktype, family, sock) + else + return nil, errors or string.format("failed to open socket(%s/%s)!", socktype, family) + end +end + +-- open tcp socket +function socket.tcp(opt) + opt = opt or {} + return socket.open(socket.TCP, opt.family or socket.IPV4) +end + +-- open udp socket +function socket.udp(opt) + opt = opt or {} + return socket.open(socket.UDP, opt.family or socket.IPV4) +end + +-- open and bind tcp socket +function socket.bind(addr, port, opt) + local sock, errors = socket.tcp(opt) + if not sock then + return nil, errors + end + local ok, errors = sock:bind(addr, port) + if not ok then + sock:close() + return nil, string.format("bind %s:%s failed, errors: %s!", addr, port, errors or "") + end + return sock +end + +-- open and connect tcp socket +function socket.connect(addr, port, opt) + local sock, errors = socket.tcp(opt) + if not sock then + return nil, errors + end + local ok, errors = sock:connect(addr, port, opt) + if ok <= 0 then + sock:close() + return nil, string.format("connect %s:%s failed, errors: %s!", addr, port, errors or "") + end + return sock +end + +-- return module +return socket |
