Forráskód Böngészése

Add co_commands coroutine - executes server commands in real-time

- co_commands(): Listens for commands from server
- execute_command(): uci_set, shell, reboot, wifi_restart, status
- Commands execute ASYNCHRONOUSLY with monitoring
- Results sent back to server
- All three coroutines run concurrently
Luis Rosales 2 hónapja
szülő
commit
fd4aabd236
1 módosított fájl, 168 hozzáadás és 93 törlés
  1. 168 93
      package/src/client2server-unified.lua

+ 168 - 93
package/src/client2server-unified.lua

@@ -2,7 +2,7 @@
     client2server-unified.lua - Bidirectional event forwarder for OpenWrt
     Copyright (c) 2026 Luis Rosales - MIT License
     
-    Uses coroutines for concurrent event monitoring
+    Uses coroutines for concurrent event monitoring + command execution
 ]]
 
 -- ============================================================================
@@ -29,7 +29,6 @@ pcall(function()
     cfg.router_id = uci:get("client2server", "router", "id") or cfg.router_id
     cfg.wan_interface = uci:get("client2server", "wan", "interface") or cfg.wan_interface
     cfg.wan_device = uci:get("client2server", "wan", "device") or cfg.wan_device
-    cfg.check_interval = tonumber(uci:get("client2server", "general", "check_interval")) or cfg.check_interval
 end)
 
 if cfg.router_id == "" then
@@ -69,6 +68,13 @@ local function json_encode(t)
     return "{" .. table.concat(parts, ",") .. "}"
 end
 
+local function json_decode(str)
+    local result = {}
+    for k, v in str:gmatch('"([^"]+)":%s*"([^"]*)"') do result[k] = v end
+    for k, v in str:gmatch('"([^"]+)":%s*(%d+)') do result[k] = tonumber(v) end
+    return result
+end
+
 -- ============================================================================
 -- BUFFER
 -- ============================================================================
@@ -91,32 +97,25 @@ function buffer.save()
     if not buffer.dirty then return end
     local f = io.open(cfg.buffer_file, "w")
     if not f then return end
-    for _, ev in ipairs(buffer.events) do
-        f:write(ev .. "\n")
-    end
+    for _, ev in ipairs(buffer.events) do f:write(ev .. "\n") end
     f:close()
     buffer.dirty = false
 end
 
 function buffer.add(json_event)
     table.insert(buffer.events, json_event)
-    if #buffer.events > cfg.max_buffer then
-        table.remove(buffer.events, 1)
-    end
+    if #buffer.events > cfg.max_buffer then table.remove(buffer.events, 1) end
     buffer.dirty = true
 end
 
 function buffer.flush(send_fn)
     if #buffer.events == 0 then return end
-    log_info("Flushing " .. #buffer.events .. " buffered events...")
     local i = 1
     while i <= #buffer.events do
         if send_fn(buffer.events[i]) then
             table.remove(buffer.events, i)
             buffer.dirty = true
-        else
-            i = i + 1
-        end
+        else i = i + 1 end
     end
     buffer.save()
 end
@@ -135,26 +134,39 @@ function ws.connect(url)
     local sock = require("socket").tcp()
     sock:settimeout(10)
     
-    local ok = pcall(sock.connect, sock, host, tonumber(port:sub(2)) or 443)
-    if not ok then return nil end
-    
+    if not pcall(sock.connect, sock, host, tonumber(port:sub(2)) then return nil end
     ws.sock = sock
     ws.connected = true
     return sock
 end
 
 function ws.send(data)
-    if not ws.connected then
+    if not ws.connected then buffer.add(data); return false end
+    local frame = string.format("\x81\x80%s", data)
+    if not pcall(function() ws.sock:send(frame) end) then
+        ws.connected = false
         buffer.add(data)
         return false
     end
-    local frame = string.format("\x81\x80%s", data)
-    if pcall(function() ws.sock:send(frame) end) then
-        return true
+    return true
+end
+
+-- Read from websocket (non-blocking-ish)
+function ws.recv()
+    if not ws.connected then return nil end
+    
+    -- Set non-blocking
+    ws.sock:settimeout(0.1)
+    
+    local data, err = ws.sock:receive("*l")
+    ws.sock:settimeout(10)
+    
+    if err and err ~= "timeout" then
+        ws.connected = false
+        return nil
     end
-    ws.connected = false
-    buffer.add(data)
-    return false
+    
+    return data
 end
 
 function ws.close()
@@ -163,11 +175,75 @@ function ws.close()
     ws.connected = false
 end
 
+-- ============================================================================
+-- COMMAND EXECUTOR
+-- ============================================================================
+
+function execute_command(cmd_obj)
+    local cmd = cmd_obj.command or ""
+    local args = cmd_obj.args or {}
+    
+    log_info("Executing: " .. cmd)
+    
+    local result = { success = false, output = "", error = "" }
+    
+    if cmd == "uci_set" then
+        local config = args.config
+        local section = args.section
+        local option = args.option
+        local value = args.value
+        
+        if config and section and option and value then
+            local c = string.format("uci set %s.%s.%s='%s'", config, section, option, value)
+            local f = io.popen(c)
+            result.output = f and f:read("*a") or ""
+            if f then f:close() end
+            os.execute("uci commit " .. config)
+            result.success = true
+            log_info("UCI set: " .. config .. "." .. section .. "." .. option .. " = " .. value)
+        else
+            result.error = "Missing params"
+        end
+        
+    elseif cmd == "shell" then
+        local shell_cmd = args.command
+        if shell_cmd then
+            local f = io.popen(shell_cmd)
+            result.output = f and f:read("*a") or ""
+            if f then f:close() end
+            result.success = true
+        else
+            result.error = "No command"
+        end
+        
+    elseif cmd == "reboot" then
+        os.execute("sync && reboot &")
+        result.success = true
+        result.output = "Reboot scheduled"
+        
+    elseif cmd == "wifi_restart" then
+        os.execute("/etc/init.d/network restart")
+        os.execute("/etc/init.d/wireless restart")
+        result.success = true
+        
+    elseif cmd == "status" then
+        local f = io.popen("ubus call network getStatus")
+        result.output = f and f:read("*a") or "{}"
+        if f then f:close() end
+        result.success = true
+        
+    else
+        result.error = "Unknown: " .. cmd
+    end
+    
+    return result
+end
+
 -- ============================================================================
 -- COROUTINES
 -- ============================================================================
 
--- Coroutine: Monitor DHCP leases
+-- Monitor DHCP
 local function co_dhcp()
     local leases = {}
     
@@ -178,13 +254,10 @@ local function co_dhcp()
             for line in f:lines() do
                 local ts, mac, ip, name = line:match("(%d+)%s+(%S+)%s+(%S+)%s+(%S+)")
                 if mac then
-                    current[mac] = { ip = ip, hostname = name, time = ts }
-                    
+                    current[mac] = { ip = ip, hostname = name }
                     if not leases[mac] then
-                        -- New lease
                         local ev = json_encode({
-                            router_id = cfg.router_id,
-                            hostname = cfg.router_id,
+                            router_id = cfg.router_id, hostname = cfg.router_id,
                             event_type = "dhcp_lease_new",
                             payload = { mac = mac, ip = ip, hostname = name }
                         })
@@ -195,51 +268,40 @@ local function co_dhcp()
             end
             f:close()
             
-            -- Check for expired
             for mac in pairs(leases) do
                 if not current[mac] then
-                    local exp = leases[mac]
                     local ev = json_encode({
-                        router_id = cfg.router_id,
-                        hostname = cfg.router_id,
+                        router_id = cfg.router_id, hostname = cfg.router_id,
                         event_type = "dhcp_lease_expire",
-                        payload = { mac = mac, old_ip = exp.ip }
+                        payload = { mac = mac, old_ip = leases[mac].ip }
                     })
                     ws.send(ev)
-                    log_info("DHCP expired: " .. mac)
+                    log_info("DHCP expire: " .. mac)
                 end
             end
-            
             leases = current
         end
-        
         coroutine.yield(cfg.check_interval)
     end
 end
 
--- Coroutine: Monitor WAN state
+-- Monitor WAN
 local function co_wan()
-    local link_last = nil
-    local ip_last = nil
+    local link_last, ip_last = nil, nil
     
-    -- Initial link state
     local f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
-    if f then
-        link_last = (f:read("*a") or ""):find("^1") == 1
-        f:close()
-    end
+    if f then link_last = (f:read("*a") or "":find("^1") == 1; f:close() end
     
     while true do
-        -- Check physical link
+        -- Link
         f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
-        local link_now = f and ((f:read("*a") or ""):find("^1") == 1 or false) or false
+        local link_now = f and ((f:read("*a") or ""):find("^1") == 1) or false
         if f then f:close() end
         
         if link_now ~= link_last then
             link_last = link_now
             local ev = json_encode({
-                router_id = cfg.router_id,
-                hostname = cfg.router_id,
+                router_id = cfg.router_id, hostname = cfg.router_id,
                 event_type = link_now and "wan_link_up" or "wan_link_down",
                 payload = { device = cfg.wan_device }
             })
@@ -247,97 +309,111 @@ local function co_wan()
             log_info("WAN link: " .. (link_now and "up" or "down"))
         end
         
-        -- Check DHCP IP
+        -- IP
         f = io.popen("ubus call network.interface." .. cfg.wan_interface .. " status 2>/dev/null")
         local ip_now = nil
         if f then
-            local status = f:read("*a")
+            local st = f:read("*a")
             f:close()
-            ip_now = status and status:match('"address"%s*:%s*"([^"]+)"')
+            ip_now = st and st:match('"address"%s*:%s*"([^"]+)"')
         end
         
         if ip_now and ip_now ~= ip_last then
-            local old_ip = ip_last
-            ip_last = ip_now
-            
             local ev = json_encode({
-                router_id = cfg.router_id,
-                hostname = cfg.router_id,
-                event_type = old_ip and "wan_dhcp_changed" or "wan_dhcp_new",
-                payload = { old_ip = old_ip, new_ip = ip_now }
+                router_id = cfg.router_id, hostname = cfg.router_id,
+                event_type = ip_last and "wan_dhcp_changed" or "wan_dhcp_new",
+                payload = { old_ip = ip_last, new_ip = ip_now }
             })
             ws.send(ev)
-            log_info("WAN IP: " .. (old_ip or "none") .. " -> " .. ip_now)
+            log_info("WAN IP: " .. (ip_last or "none") .. " -> " .. ip_now)
+            ip_last = ip_now
         end
         
         coroutine.yield(cfg.check_interval)
     end
 end
 
--- Coroutine: Monitor commands from server
+-- 🔥 COROUTINE: Command Listener (handles server → router commands)
 local function co_commands()
     while true do
-        -- This would typically be event-driven from WebSocket
-        -- For now, simple sleep
-        coroutine.yield(cfg.check_interval)
+        if ws.connected then
+            -- Check for incoming data
+            local data = ws.recv()
+            
+            if data and data:match("^{") then
+                log_info("Received command: " .. data:sub(1, 100))
+                
+                -- Parse JSON command
+                local cmd_obj = json_decode(data)
+                
+                if cmd_obj.command then
+                    -- Execute command
+                    local result = execute_command(cmd_obj)
+                    
+                    -- Send result back
+                    local resp = json_encode({
+                        router_id = cfg.router_id,
+                        event_type = "command_result",
+                        payload = {
+                            command = cmd_obj.command,
+                            success = result.success,
+                            output = result.output,
+                            error = result.error
+                        }
+                    })
+                    ws.send(resp)
+                    
+                    log_info("Command done: " .. cmd_obj.command .. " = " .. (result.success and "OK" or result.error))
+                end
+            end
+        end
+        
+        coroutine.yield(1)  -- Check every second for commands
     end
 end
 
 -- ============================================================================
--- MAIN (Coroutine Scheduler)
+-- MAIN
 -- ============================================================================
 
 local function scheduler()
-    -- Create coroutines
-    local co_list = {
-        coroutine.create(co_dhcp),
-        coroutine.create(co_wan),
-        -- co_commands,  -- Uncomment when command receiver implemented
+    local cos = {
+        co_dhcp = coroutine.create(co_dhcp),
+        co_wan = coroutine.create(co_wan),
+        co_commands = coroutine.create(co_commands),  -- 🔥 Commands run concurrently!
     }
     
-    local function dispatch()
-        for _, co in ipairs(co_list) do
+    while true do
+        -- Resume all coroutines
+        for name, co in pairs(cos) do
             if coroutine.status(co) == "suspended" then
                 local _, delay = coroutine.resume(co)
-                delay = delay or cfg.check_interval
+                -- Small stagger to avoid spikes
+                if delay then os.execute("sleep 0.1") end
             end
         end
-    end
-    
-    -- Simple scheduler: iterate and resume each
-    while true do
-        dispatch()
         
-        -- Also flush buffer periodically
+        -- Flush buffer when connected
         if ws.connected then
             buffer.flush(function(d) return ws.send(d) end)
         end
         
-        -- Sleep before next round
-        os.execute("sleep " .. cfg.check_interval)
+        os.execute("sleep 1")
     end
 end
 
--- ============================================================================
--- INITIALIZATION
--- ============================================================================
-
 local function main()
     log_info("Starting client2server...")
     log_info("Router: " .. cfg.router_id)
     log_info("Server: " .. cfg.server_url)
-    log_info("Check interval: " .. cfg.check_interval .. "s")
     
     buffer.init()
     
     local pf = io.open(cfg.pid_file, "w")
-    if pf then
-        pf:write(tostring(os.getpid()))
-        pf:close()
-    end
+    if pf then pf:write(tostring(os.getpid())); pf:close() end
     
-    -- Connect in main
-    log_info("Connecting to " .. cfg.server_url .. "...")
+    -- Connect
+    log_info("Connecting...")
     local sock = ws.connect(cfg.server_url)
     
     if sock then
@@ -345,10 +421,9 @@ local function main()
         log_info("Connected!")
         buffer.flush(function(d) return ws.send(d) end)
     else
-        log_err("Connection failed, will retry in loop")
+        log_err("Connection failed")
     end
     
-    -- Start scheduler (runs coroutines)
     scheduler()
 end