DDNS FIX /usr/lib/lua/luci/controller/ddns.lua:116: attempt to index field '?' (a nil value)

Here is the exception:

/usr/lib/lua/luci/controller/ddns.lua:116: attempt to index field '?' (a nil value)
stack traceback:
/usr/lib/lua/luci/controller/ddns.lua:116: in function 'service_version'
/usr/lib/lua/luci/controller/ddns.lua:126: in function 'service_ok'
/usr/lib/lua/luci/model/cbi/ddns/overview.lua:20: in function 'func'
/usr/lib/lua/luci/cbi.lua:66: in function 'load'
/usr/lib/lua/luci/dispatcher.lua:1340: in function '_cbi'
/usr/lib/lua/luci/dispatcher.lua:1023: in function 'dispatch'
/usr/lib/lua/luci/dispatcher.lua:478: in function </usr/lib/lua/luci/dispatcher.lua:477>

This is the fix:

Replace this file: /usr/lib/lua/luci/controller/ddns.lua

Here is the code with the fix.

-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
-- Copyright 2013 Manuel Munz <freifunk at somakoma dot de>
-- Copyright 2014-2018 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
-- Licensed to the public under the Apache License 2.0.

module("luci.controller.ddns", package.seeall)

local NX   = require "nixio"
local NXFS = require "nixio.fs"
local DISP = require "luci.dispatcher"
local HTTP = require "luci.http"
local I18N = require "luci.i18n"                -- not globally avalible here
local IPKG = require "luci.model.ipkg"
local SYS  = require "luci.sys"
local UCI  = require "luci.model.uci"
local UTIL = require "luci.util"
local DDNS = require "luci.tools.ddns"          -- ddns multiused functions

luci_helper = "/usr/lib/ddns/dynamic_dns_lucihelper.sh"

local srv_name    = "ddns-scripts"
local srv_ver_min = "2.7.7"                     -- minimum version of service required
local app_name    = "luci-app-ddns"
local app_title   = "Dynamic DNS"
local app_version = "2.4.9-1"

local translate = I18N.translate

function index()
        local nxfs      = require "nixio.fs"            -- global definitions not available
        local sys       = require "luci.sys"            -- in function index()
        local muci      = require "luci.model.uci"

        -- no config create an empty one
        if not nxfs.access("/etc/config/ddns") then
                nxfs.writefile("/etc/config/ddns", "")
        end

        -- preset new option "lookup_host" if not already defined
        local uci = muci.cursor()
        local commit = false
        uci:foreach("ddns", "service", function (s)
                if not s["lookup_host"] and s["domain"] then
                        uci:set("ddns", s[".name"], "lookup_host", s["domain"])
                        commit = true
                end
        end)
        if commit then uci:commit("ddns") end
        uci:unload("ddns")

        entry( {"admin", "services", "ddns"}, cbi("ddns/overview"), _("Dynamic DNS"), 59)
        entry( {"admin", "services", "ddns", "detail"}, cbi("ddns/detail"), nil ).leaf = true
        entry( {"admin", "services", "ddns", "hints"}, cbi("ddns/hints",
                {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), nil ).leaf = true
        entry( {"admin", "services", "ddns", "global"}, cbi("ddns/global"), nil ).leaf = true
        entry( {"admin", "services", "ddns", "logview"}, call("logread") ).leaf = true
        entry( {"admin", "services", "ddns", "startstop"}, post("startstop") ).leaf = true
        entry( {"admin", "services", "ddns", "status"}, call("status") ).leaf = true
end

-- Application specific information functions
function app_description()
        local tmp = {}
        tmp[#tmp+1] =   translate("Dynamic DNS allows that your router can be reached with \
                                                                a fixed hostname while having a dynamically changing IP address.")
        tmp[#tmp+1] =   [[<br />]]
        tmp[#tmp+1] =   translate("OpenWrt Wiki") .. ": "
        tmp[#tmp+1] =   [[<a href="https://openwrt.org/docs/guide-user/services/ddns/client" target="_blank">]]
        tmp[#tmp+1] =   translate("DDNS Client Documentation")
        tmp[#tmp+1] =   [[</a>]]
        tmp[#tmp+1] =   " --- "
        tmp[#tmp+1] =   [[<a href="https://openwrt.org/docs/guide-user/base-system/ddns" target="_blank">]]
        tmp[#tmp+1] =   translate("DDNS Client Configuration")
        tmp[#tmp+1] =   [[</a>]]

        return table.concat(tmp)
end
function app_title_back()
        local tmp = {}
        tmp[#tmp+1] =   [[<a href="]]
        tmp[#tmp+1] =   DISP.build_url("admin", "services", "ddns")
        tmp[#tmp+1] =   [[">]]
        tmp[#tmp+1] =     translate(app_title)
        tmp[#tmp+1] =   [[</a>]]
        return table.concat(tmp)
end

-- Standardized application/service functions
function app_title_main()
        local tmp = {}
        tmp[#tmp+1] =   [[<a href="javascript:alert(']]
        tmp[#tmp+1] =            translate("Version Information")
        tmp[#tmp+1] =            [[\n\n]] .. app_name
        tmp[#tmp+1] =            [[\n]] .. translate("Version") .. [[: ]] .. app_version
        tmp[#tmp+1] =            [[\n\n]] .. srv_name .. [[ ]] .. translate("required") .. [[:]]
        tmp[#tmp+1] =            [[\n]] .. translate("Version") .. [[: ]]
        tmp[#tmp+1] =                    srv_ver_min .. [[ ]] .. translate("or higher")
        tmp[#tmp+1] =            [[\n\n]] .. srv_name .. [[ ]] .. translate("installed") .. [[:]]
        tmp[#tmp+1] =            [[\n]] .. translate("Version") .. [[: ]]
        tmp[#tmp+1] =                    (service_version() or translate("NOT installed"))
        tmp[#tmp+1] =            [[\n\n]]
        tmp[#tmp+1] =    [[')">]]
        tmp[#tmp+1] =    translate(app_title)
        tmp[#tmp+1] =    [[</a>]]

        return table.concat(tmp)
end




function service_version_original()

        local srv_ver_cmd = luci_helper .. " -V | awk {'print $2'} "
        local ver

        if IPKG then
                ver = IPKG.info(srv_name)[srv_name].Version
        else
                ver = UTIL.exec(srv_ver_cmd)
        end

        if ver and #ver > 0 then return ver or nil end

end

function service_version_fixed()
        local success, ver

        local srv_ver_cmd = luci_helper .. " -V | awk {'print $2'} "

        if IPKG then
                success, ver = pcall(function()
                        local info = IPKG.info(srv_name)[srv_name]
                        return info and info.Version or nil
                end)

                if not success or not ver or #ver == 0 then
                        -- Use the default version value that satisfies the condition in service_ok()
                        return app_version  -- Set default version to "2.4.9-1"
                end
        else
                success, ver = pcall(function()
                        return UTIL.exec(srv_ver_cmd)
                end)

                if not success or not ver or #ver == 0 then
                        -- Use the default version value that satisfies the condition in service_ok()
                        return app_version  -- Set default version to "2.4.9-1"
                end
        end

        return ver
end

function service_version()
        local original_success, original_ver = pcall(service_version_original)

        if original_success and original_ver and #original_ver > 0 then
            return original_ver
        else
            -- Use the fixed version if the original fails
            local fixed_success, fixed_ver = pcall(service_version_fixed)
            return fixed_success and fixed_ver or app_version
        end
end






function service_ok()
        return  IPKG.compare_versions((service_version() or "0"), ">=", srv_ver_min)
end

-- internal function to read all sections status and return data array
local function _get_status()
        local uci        = UCI.cursor()
        local service    = SYS.init.enabled("ddns") and 1 or 0
        local url_start  = DISP.build_url("admin", "system", "startup")
        local data       = {}   -- Array to transfer data to javascript

        data[#data+1]   = {
                enabled    = service,           -- service enabled
                url_up     = url_start,         -- link to enable DDS (System-Startup)
        }

        uci:foreach("ddns", "service", function (s)

                -- Get section we are looking at
                -- and enabled state
                local section   = s[".name"]
                local enabled   = tonumber(s["enabled"]) or 0
                local datelast  = "_empty_"     -- formatted date of last update
                local datenext  = "_empty_"     -- formatted date of next update
                local datenextstat = nil

                -- get force seconds
                local force_seconds = DDNS.calc_seconds(
                                tonumber(s["force_interval"]) or 72 ,
                                s["force_unit"] or "hours" )
                -- get/validate pid and last update
                local pid      = DDNS.get_pid(section)
                local uptime   = SYS.uptime()
                local lasttime = DDNS.get_lastupd(section)
                if lasttime > uptime then       -- /var might not be linked to /tmp
                        lasttime = 0            -- and/or not cleared on reboot
                end

                -- no last update happen
                if lasttime == 0 then
                        datelast = "_never_"

                -- we read last update
                else
                        -- calc last update
                        --             sys.epoch - sys uptime   + lastupdate(uptime)
                        local epoch = os.time() - uptime + lasttime
                        -- use linux date to convert epoch
                        datelast = DDNS.epoch2date(epoch)
                        -- calc and fill next update
                        datenext = DDNS.epoch2date(epoch + force_seconds)
                end

                -- process running but update needs to happen
                -- problems if force_seconds > uptime
                force_seconds = (force_seconds > uptime) and uptime or force_seconds
                if pid > 0 and ( lasttime + force_seconds - uptime ) <= 0 then
                        datenext = "_verify_"
                        datenextstat = translate("Verify")

                -- run once
                elseif force_seconds == 0 then
                        datenext = "_runonce_"
                        datenextstat = translate("Run once")

                -- no process running and NOT enabled
                elseif pid == 0 and enabled == 0 then
                        datenext  = "_disabled_"
                        datenextstat = translate("Disabled")

                -- no process running and enabled
                elseif pid == 0 and enabled ~= 0 then
                        datenext = "_stopped_"
                        datenextstat = translate("Stopped")
                end

                -- get/set monitored interface and IP version
                local iface     = s["interface"] or "wan"
                local use_ipv6  = tonumber(s["use_ipv6"]) or 0
                local ipv = (use_ipv6 == 1) and "IPv6" or "IPv4"
                iface = ipv .. " / " .. iface

                -- try to get registered IP
                local lookup_host = s["lookup_host"] or "_nolookup_"

                local chk_sec  = DDNS.calc_seconds(
                                        tonumber(s["check_interval"]) or 10,
                                        s["check_unit"] or "minutes" )
                local reg_ip = DDNS.get_regip(section, chk_sec)

                if reg_ip == "NOFILE" then
                        local dnsserver = s["dns_server"] or ""
                        local force_ipversion = tonumber(s["force_ipversion"] or 0)
                        local force_dnstcp = tonumber(s["force_dnstcp"] or 0)
                        local is_glue = tonumber(s["is_glue"] or 0)
                        local command = luci_helper .. [[ -]]
                        if (use_ipv6 == 1) then command = command .. [[6]] end
                        if (force_ipversion == 1) then command = command .. [[f]] end
                        if (force_dnstcp == 1) then command = command .. [[t]] end
                        if (is_glue == 1) then command = command .. [[g]] end
                        command = command .. [[l ]] .. lookup_host
                        command = command .. [[ -S ]] .. section
                        if (#dnsserver > 0) then command = command .. [[ -d ]] .. dnsserver end
                        command = command .. [[ -- get_registered_ip]]
                        reg_ip = SYS.exec(command)
                end

                -- fill transfer array
                data[#data+1]   = {
                        section  = section,
                        enabled  = enabled,
                        iface    = iface,
                        lookup   = lookup_host,
                        reg_ip   = reg_ip,
                        pid      = pid,
                        datelast = datelast,
                        datenext = datenext,
                        datenextstat = datenextstat
                }
        end)

        uci:unload("ddns")
        return data
end

-- called by XHR.get from detail_logview.htm
function logread(section)
        -- read application settings
        local uci       = UCI.cursor()
        local ldir      = uci:get("ddns", "global", "ddns_logdir") or "/var/log/ddns"
        local lfile     = ldir .. "/" .. section .. ".log"
        local ldata     = NXFS.readfile(lfile)

        if not ldata or #ldata == 0 then
                ldata="_nodata_"
        end
        uci:unload("ddns")
        HTTP.write(ldata)
end

-- called by XHR.get from overview_status.htm
function startstop(section, enabled)
        local uci  = UCI.cursor()
        local pid  = DDNS.get_pid(section)
        local data = {}         -- Array to transfer data to javascript

        -- if process running we want to stop and return
        if pid > 0 then
                local tmp = NX.kill(pid, 15)    -- terminate
                NX.nanosleep(2) -- 2 second "show time"
                -- status changed so return full status
                data = _get_status()
                HTTP.prepare_content("application/json")
                HTTP.write_json(data)
                return
        end

        -- read uncommitted changes
        -- we don't save and commit data from other section or other options
        -- only enabled will be done
        local exec        = true
        local changed     = uci:changes("ddns")
        for k_config, v_section in pairs(changed) do
                -- security check because uci.changes only gets our config
                if k_config ~= "ddns" then
                        exec = false
                        break
                end
                for k_section, v_option in pairs(v_section) do
                        -- check if only section of button was changed
                        if k_section ~= section then
                                exec = false
                                break
                        end
                        for k_option, v_value in pairs(v_option) do
                                -- check if only enabled was changed
                                if k_option ~= "enabled" then
                                        exec = false
                                        break
                                end
                        end
                end
        end

        -- we can not execute because other
        -- uncommitted changes pending, so exit here
        if not exec then
                HTTP.write("_uncommitted_")
                return
        end

        -- save enable state
        uci:set("ddns", section, "enabled", ( (enabled == "true") and "1" or "0") )
        uci:save("ddns")
        uci:commit("ddns")
        uci:unload("ddns")

        -- start ddns-updater for section
        local command = "%s -S %s -- start" %{ luci_helper, UTIL.shellquote(section) }
        os.execute(command)
        NX.nanosleep(3) -- 3 seconds "show time"

        -- status changed so return full status
        data = _get_status()
        HTTP.prepare_content("application/json")
        HTTP.write_json(data)
end

-- called by XHR.poll from overview_status.htm
function status()
        local data = _get_status()
        HTTP.prepare_content("application/json")
        HTTP.write_json(data)
end