/
CountZero
/
quik-cscalp-lua
Обзор
Документация
Войти
/
CountZero
/
quik-cscalp-lua
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
qsutils.lua
484 строки
13 KB
CountZero
Update
05 авг 2026, 20:59
05 авг 2026, 20:59
500bdaf
Код
Авторство
О чём код?
-- Local transport, JSON framing, time and bounded logging. local socket = require("socket") local json = require("dkjson") local Transport = require("qstransport") local qsutils = {} local RESPONSE_PORT = 34130 local CALLBACK_PORT = RESPONSE_PORT + 1 local ACCEPT_TIMEOUT_SEC = 0.2 local CLIENT_TIMEOUT_SEC = 0.2 local HALF_CONNECTION_TIMEOUT_MSEC = 5000 local SEND_DEADLINE_MSEC = 250 local MAX_REQUEST_BYTES = 1024 * 1024 local MAX_LOG_LINE_BYTES = 8192 local LOG_FLUSH_MSEC = 1000 local response_server = nil local callback_server = nil local response_client = nil local callback_client = nil local half_connection_deadline_msec = nil local response_reader = Transport.new_reader(MAX_REQUEST_BYTES) local disconnecting = false local log_buffer = {} local last_log_flush_msec = 0 local log_flushing = false is_connected = false is_debug = false local logfile = nil local function integer_msec(value) return math.floor((tonumber(value) or 0) + 0.5) end function delay(msec) local duration = math.max(0, tonumber(msec) or 0) if type(_G.sleep) == "function" then pcall(_G.sleep, duration) elseif type(socket.sleep) == "function" then pcall(socket.sleep, duration / 1000) end end function timemsec() local ok, value = pcall(socket.gettime) if ok and tonumber(value) then return integer_msec(tonumber(value) * 1000) end return integer_msec(os.time() * 1000) end local function normalize_level(level) if type(level) == "string" then local normalized = string.upper(level) if normalized == "DEBUG" or normalized == "INFO" or normalized == "WARN" or normalized == "ERROR" then return normalized end end if tonumber(level) == 3 then return "ERROR" elseif tonumber(level) == 2 then return "WARN" elseif tonumber(level) == 1 then return "INFO" elseif tonumber(level) == 0 then return "INFO" end return "INFO" end local function terminal_level(level) if level == "ERROR" then return 3 elseif level == "WARN" then return 2 end return 1 end local function truncate(value, max_bytes) local text = tostring(value or "") if #text <= max_bytes then return text end return text:sub(1, max_bytes) .. "...<truncated>" end local function mkdir(path) if package.config and package.config:sub(1, 1) == "\\" then pcall(os.execute, 'mkdir "' .. tostring(path) .. '" >NUL 2>NUL') else pcall(os.execute, 'mkdir -p "' .. tostring(path) .. '" >/dev/null 2>&1') end end local function join_path(base, name) local separator = package.config and package.config:sub(1, 1) or "\\" local text = tostring(base or "") if text == "" then return name end local last = text:sub(-1) if last == "\\" or last == "/" then return text .. name end return text .. separator .. name end local function log_directory() local base = rawget(_G, "script_path") or "." if type(_G.getWorkingFolder) == "function" then local ok, value = pcall(_G.getWorkingFolder) if ok and type(value) == "string" and value ~= "" then base = value end end local cscalp = join_path(base, "cscalp") local logs = join_path(cscalp, "logs") mkdir(cscalp) mkdir(logs) return logs end function openLog() local directory = log_directory() local path = join_path(directory, "QUIK#_" .. os.date("%Y%m%d") .. ".log") local file = io.open(path, "ab") if file then return file end local fallback = join_path(rawget(_G, "script_path") or ".", "QUIK#_" .. os.date("%Y%m%d") .. ".log") return io.open(fallback, "ab") end local function write_log_line(line) if not logfile then return end local ok = pcall(logfile.write, logfile, line) if not ok then pcall(logfile.close, logfile) logfile = nil end end function flushLog(force) if log_flushing then return end local now = timemsec() if not force and now - last_log_flush_msec < LOG_FLUSH_MSEC then return end local pending = log_buffer log_buffer = {} log_flushing = true if logfile then for i = 1, #pending do write_log_line(pending[i]) end pcall(logfile.flush, logfile) end log_flushing = false last_log_flush_msec = now end function closeLog() flushLog(true) if logfile then pcall(logfile.close, logfile) logfile = nil end end logfile = openLog() function log(message_text, level) local normalized_level = normalize_level(level) if normalized_level == "DEBUG" and not is_debug then return end local text = truncate(message_text, MAX_LOG_LINE_BYTES) local now = timemsec() local milliseconds = now % 1000 local line = os.date("%Y-%m-%d %H:%M:%S", math.floor(now / 1000)) .. "." .. string.format("%03d", milliseconds) .. " LOG " .. normalized_level .. ": " .. text .. "\n" if normalized_level == "WARN" or normalized_level == "ERROR" then flushLog(true) write_log_line(line) if logfile then pcall(logfile.flush, logfile) end if type(_G.message) == "function" then pcall(_G.message, text, terminal_level(normalized_level)) end else log_buffer[#log_buffer + 1] = line end end function split(input_string, separator) if type(input_string) ~= "string" then return {} end local sep = separator or "%s" local result = {} for item in input_string:gmatch("([^" .. sep .. "]+)") do result[#result + 1] = item end return result end function from_json(text) if type(text) ~= "string" then return nil, "JSON input must be a string" end local ok, value, _, decode_error = pcall(json.decode, text, 1, json.null) if not ok then return nil, tostring(value) end if decode_error then return nil, tostring(decode_error) end if type(value) ~= "table" then return nil, "JSON message root must be an object" end return value end function to_json(message_table) local ok, encoded = pcall(json.encode, message_table, { indent = false, }) if not ok then return nil, tostring(encoded) end return encoded end local function configure_server(server, name) if not server then return nil end local ok, timeout_error = pcall( server.settimeout, server, ACCEPT_TIMEOUT_SEC) if not ok then log(name .. " settimeout failed: " .. tostring(timeout_error), "error") return nil end return server end local function bind_server(port, name) local ok, server, bind_error = pcall(socket.bind, "127.0.0.1", port, 2) if not ok then log(name .. " bind raised an error: " .. tostring(server), "error") return nil end if not server then log(name .. " bind failed: " .. tostring(bind_error), "error") return nil end return configure_server(server, name) end response_server = bind_server(RESPONSE_PORT, "response server") callback_server = bind_server(CALLBACK_PORT, "callback server") local function configure_client(client, name) local ok, timeout_error = pcall( client.settimeout, client, CLIENT_TIMEOUT_SEC) if not ok then log(name .. " settimeout failed: " .. tostring(timeout_error), "warn") pcall(client.close, client) return nil end if type(client.setoption) == "function" then pcall(client.setoption, client, "tcp-nodelay", true) pcall(client.setoption, client, "keepalive", true) end return client end local function accept_one(server, name) if not server then return nil, name .. " is unavailable" end local ok, client, accept_error = pcall(server.accept, server) if not ok then return nil, tostring(client) end if not client then return nil, tostring(accept_error or "timeout") end return configure_client(client, name) end local function notify_local_disconnected(reason) if type(_G.OnQuikSharpDisconnected) == "function" then local ok, callback_error = pcall(_G.OnQuikSharpDisconnected, reason) if not ok then log("OnQuikSharpDisconnected failed: " .. tostring(callback_error), "error") end end end local function notify_local_connected() if type(_G.OnQuikSharpConnected) == "function" then local ok, callback_error = pcall(_G.OnQuikSharpConnected) if not ok then log("OnQuikSharpConnected failed: " .. tostring(callback_error), "error") end end end local function disconnected(reason) if disconnecting then return end disconnecting = true local was_connected = is_connected is_connected = false if response_client then pcall(response_client.close, response_client) response_client = nil end if callback_client then pcall(callback_client.close, callback_client) callback_client = nil end response_reader = Transport.new_reader(MAX_REQUEST_BYTES) half_connection_deadline_msec = nil if was_connected then log("QUIK# client disconnected; reason=" .. tostring(reason), "warn") notify_local_disconnected(reason) end disconnecting = false end function qsutils.disconnect(reason) disconnected(reason or "explicit disconnect") end function qsutils.connect() if is_connected then return true end if not response_server or not callback_server then return nil, "local servers are not available" end if not response_client then local client, accept_error = accept_one( response_server, "response client") if client then response_client = client half_connection_deadline_msec = timemsec() + HALF_CONNECTION_TIMEOUT_MSEC log("Response half of CScalp connection accepted", "info") elseif accept_error ~= "timeout" then log("Response accept failed: " .. tostring(accept_error), "warn") end return false end if not callback_client then local client, accept_error = accept_one( callback_server, "callback client") if client then callback_client = client elseif accept_error ~= "timeout" then log("Callback accept failed: " .. tostring(accept_error), "warn") end if not callback_client and half_connection_deadline_msec and timemsec() >= half_connection_deadline_msec then pcall(response_client.close, response_client) response_client = nil half_connection_deadline_msec = nil log("Incomplete CScalp connection timed out", "warn") end if not callback_client then return false end end is_connected = true half_connection_deadline_msec = nil response_reader = Transport.new_reader(MAX_REQUEST_BYTES) log("QUIK# client connected", "info") notify_local_connected() return true end function receiveRequest() if not is_connected then return nil, "not connected" end local line, receive_error, fatal = Transport.read_line(response_client, response_reader) if line then local message_table, decode_error = from_json(line) if not message_table then log("CScalp JSON request rejected: " .. tostring(decode_error), "error") return nil, decode_error end return message_table end if fatal then disconnected(receive_error) end return nil, receive_error end local function send_message(client, message_table, kind) local encoded, encode_error = to_json(message_table) if not encoded then log(kind .. " JSON encoding failed: " .. tostring(encode_error), "error") return nil, encode_error end local ok, send_error = Transport.send_all( client, encoded .. "\n", { now = timemsec, timeout_msec = SEND_DEADLINE_MSEC, pause = function() delay(1) end, }) if not ok then disconnected(kind .. " send failed: " .. tostring(send_error)) return nil, send_error end return true end function sendResponse(message_table) if not is_connected then return nil, "not connected" end return send_message(response_client, message_table, "response") end function sendCallback(message_table) if not is_connected then return nil, "not connected" end return send_message(callback_client, message_table, "callback") end function qsutils.housekeeping() flushLog(false) end function qsutils.close(reason) disconnected(reason or "transport close") if response_server then pcall(response_server.close, response_server) response_server = nil end if callback_server then pcall(callback_server.close, callback_server) callback_server = nil end closeLog() end return qsutils