--
-- LLRNet - network part of LLR
--
-- (C) 2004-2005 Vincent Penne
--
-- Released under GNU LIBRARY GENERAL PUBLIC LICENSE
-- (See file LICENSE that must be included with this software)
--
--

version = "0.9b1"

dofile(EXEDIR.."init.lua")

if not dolib("basic") then
   return
end
if not dolib("client") then
   return
end

if not dolib("sql") then
   return
end

print(format("LLR Server version %s started", version))
--print(EXEDIR)
--print(WIN32)

-- maximum number of concurrent connections
maxConnections = 4

-- port that the server listen to
port = 7000

-- maximum allowed time for a user to complete a job (in seconds)
-- (this is a ridiculous default value for testing only, it needs to
--  be properly redefined into llr-serverconfig.txt))
jobMaxTime = 10 

displayFormat = "%s*2^%s-1"

knPairsFile = "knpairs.txt"
jobListFile = "joblist.txt"
resultsFile = "results.txt"
rejectedFile = "rejected.txt"
knPairs = { }
jobList = { }

tosendFile = "tosend-proxy.txt"
password = "llrnet" -- for now, same password for everybody

stats = {
   connections = 0,
   results = 0,
   rejected = 0,
   errors = 0,
   primes = 0,
}


-- WIN32 specific
serviceName = "LLRnet-server"
serviceDisplayName = "LLRnet server, k*2^n-1 / k*2^n+1 networked prime tester"
------------------


function Seconds()
   if WIN32 then
      local y, d, h, m, s = date("%Y")-1970, date("%j")-1, date("%H"), date("%M"), date("%S")
      
      return (y*366+d)*24*60*60+h*60*60+m*60+s
   else
      return tonumber(date("%s"))
   end
end

function SqlDate()
   return date("%Y-%m-%d\ %H:%M:%S")
end

function DisplayDate()
   return date("%c")
end

if not OutputBoth then
   function OutputBoth(s)
      print(s)
   end
end

if not stopCheck then
   function stopCheck() end
end

function UpdateStatus()
   statusUpdated = 1
   if not trayIcon then return end
   local str = format("cnx : %d, rslts : %d, primes : %d, rejected : %d, errs : %d", 
		      stats.connections,
		      stats.results,
		      stats.primes,
		      stats.rejected,
		      stats.errors)
   TrayIconTip(trayIcon, "LLRserver : "..str)
end

function WriteResultToFile(job, filename)
   -- write result into lresults file
   local file = openfile(filename, "a")
   if file then
      write(file, format("user=%s\n", job.user))
      write(file, format("[%s]\n", job.resultdate))
      if job.result ~= "0" then
	 write(file, format(displayFormat.." is not prime.  Res64: %s  Time : %d.0 sec.\n", 
			    job.k, job.n, job.result, 
			    Seconds() - job.seconds))
      else
	 write(file, format(displayFormat.." is prime!  Time : %d.0 sec.\n", 
			    job.k, job.n, 
			    Seconds() - job.seconds))
      end
      closefile(file)
   end
end

function ProxyForwardResult(t, k, n, residue)
   if not proxy then
      return
   end

   tinsert(tosend, { t = t, k = k, n = n, 
	      result = -2, residue = residue } )
   WriteTosendfile()
end

function ProxyUpdate(twice)
   if not proxy then
      return
   end

   server = proxyMasterAddress
   port = proxyMasterPort

   if (not proxyTosendThreshold or 
       getn(tosend) >= proxyTosendThreshold) and SendAllResults() then
      Logout()
      return -1
   end

   -- if the proxy cache is not filled enough, ask for new 
   -- WU from the master server
   local i, v, n
   n = 0
   for i, v in jobList do
      n = n + 1
   end
   if getn(knPairs) - n < proxyCacheSize then
      local dirty
      local i

      if SendAllResults() then
	 Logout()
	 return -1
      end

      for i=1, proxyCacheSize, 1 do
	 local t, k, n
	 repeat
	    t, k, n = GetPair()
	 until not t or not k or not n or not knPairs.lookup[format("%s/%s", k, n)]

	 if not testType then
	    testType = t
	 end
	 if t == testType and k and n then
	    tinsert(knPairs, { k = k, n = n })
	    knPairs.lookup[format("%s/%s", k, n)] = getn(knPairs)
	    dirty = 1
	 else
	    break
	 end
      end
      
      if dirty then
	 -- we need to update the knpairs file and table
	 PrunePairs()
	 knPairs = ReadPairs(knPairsFile)

	 -- and the joblist also so that it does not grow too much
	 PruneJoblist()
	 WriteJobList(jobListFile)
      end
   end

   Logout()
end

function PrunePairs()
   print("Pruning knpairs.txt file")

   local file
   if no_shell then
      file = openfile(knPairsFile, "w")
   else
      file = openfile(knPairsFile..".tmp", "w")
   end
   if not file then
      return -1
   end

   local i, n

   write(file, testType.."\n")

   n = getn(knPairs)
   for i=1, n, 1 do
      local p = knPairs[i]
      local name = p.k.."/"..p.n
      local j = jobList[name]
      if not j or j.status ~= "solved" then
	 write(file, format("%s %s\n", p.k, p.n))
      else
	 print("Removing solved pair", p.k, p.n)
      end
   end

   closefile(file)

   if not no_shell then
      execute(format('mv "%s" "%s"', knPairsFile..".tmp", knPairsFile))
   end
end

-- parse a newpgen file
function ReadPairs(filename)
   local file = openfile(filename, "r")
   if not file then 
      return 
   end

   local pairs = { lookup = { } }

   -- first line contains type of test to perform
   testType = read(file, "*w")

   -- next lines contains k/n pairs
   while 1 do
      local k, n
      k = read(file, "*w")
      if not k then
	 break
      end
      n = read(file, "*w")
      if not n then
	 break
      end

      tinsert(pairs, { k = k, n = n })
      --print(k, n)

      pairs.lookup[format("%s/%s", k, n)] = getn(pairs)
   end

   lowestKnPair = 1

   closefile(file)

   return pairs
end

-- Remove all jobs that are solved and do not belong anymore to
-- the knpairs file, so that the jobList file will not grow infinitely
function PruneJoblist()
   local i, job

   print("pruning joblist ...")
   for i, job in jobList do
      if job.status == "solved" and
	 not knPairs.lookup[i] then
	 print("Removing solved job", i)
	 jobList[i] = nil
      end
   end
   print("finished ...")
end

function WriteJobList(filename)
   print("write '"..filename.."' ...")
   local file
   if no_shell then
      file = openfile(filename, "w")
   else
      file = openfile(filename..".tmp", "w")
   end
   if not file then
      return
   end

   write(file, "jobList = {\n")
   local i, v
   local sorted = { }
   for i, v in jobList do
      tinsert(sorted, { idx=i, job=v })
   end
   table.sort(sorted, 
	      function (a, b) 
		 return a.job.seconds < b.job.seconds 
	      end
	   )
   local k, n
   n = getn(sorted)
   for k=1,n,1 do
      i = sorted[k].idx
      v = sorted[k].job
      if v.status ~= "zombie" then
	 write(file, format('[%q] = ', i)..type_dump(v)..",\n")
      end
   end
   write(file, "}")
   closefile(file)

   if not no_shell then
      execute(format('mv "%s" "%s"', filename..".tmp", filename))
   end
   print("finished ...")
end

function UpdateJobList(filename, i, v)
   --print("update joblist ...")

   local file = openfile(filename, "a")
   if not file then
      print(" --> failed")
      return
   end

   write(file, format('\n-- update [%s]\njobList[%q] = ',DisplayDate() , i)..
	 type_dump(v).."\n")

   closefile(file)

   --print("finished ...")
end

-- All commands recognised by the server
commands = {
   Login = 
      function(client, socket)
	 local username = net_Recv(socket)
	 local password = net_Recv(socket)
	 --print(username, password)

	 local wanted_pass
	 SqlConnect(client)

	 if sqlUsed and not client.sqlConn then
	    net_Send(socket, "LOGIN FAILED")
	    return
	 end

	 if sqlUsed and sqlUsersTable then
	    -- get password from sql users table
	    local cur, err = 
	       client.sqlConn:execute("select "..sqlPasswordEntry.." from "..
				      sqlUsersTable.." where "..
					 sqlUsernameEntry.."='"..username.."'")
	    if cur then
	       local info = cur:fetch({}, "a")
	       wanted_pass = info and info[sqlPasswordEntry]
	       cur:close()
	    else
	       print(err)
	    end
	 else
	    -- check general password for now
	    wanted_pass = "llrnet"
	 end
	 if password == wanted_pass then
	    client.username = username
	    net_Send(socket, "LOGGED") -- ACK the client
	 else
	    net_Send(socket, "LOGIN FAILED")
	 end
      end,
   
   AskPair = 
      function(client, socket)
	 if sqlUsed and sqlPairsTable then
	    -- SQL mode
	    SqlAskPair(client, socket)
	    return
	 end

	 local i, n
	 local s = Seconds()
	 local pairs = knPairs
	 n = getn(pairs)
	 local newlow
	 for i=(lowestKnPair or 1), n, 1 do
	    local pair = pairs[i]

	    local ks = pair.k
	    local ns = pair.n

	    local job = jobList[ks.."/"..ns]

	    if not newlow and (not job or job.status ~= "solved") then
	       newlow = i
	    end

	    if not job or (job.status == "working" and 
			   s-job.seconds > jobMaxTime) or
	       job.status == "abandonned" then

	       if job then
		  print("Cancelling job taken by", job.user)
	       end

	       -- mark it as temporarily taken
	       jobList[ks.."/"..ns] = { status = "zombie", seconds = 0 }

	       net_Send(socket, "OK")
	       net_Send(socket, testType)
	       net_Send(socket, ks)
	       net_Send(socket, ns)
	       --tremove(knPairs, 1)

	       local k, n, t
	       t = net_Recv(socket)
	       k = net_Recv(socket)
	       n = net_Recv(socket)
	       if k and n and t == testType 
		  and k == pair.k and n == pair.n then
		  local name = k.."/"..n
		  jobList[name] = {
		     user = client.username,
		     date = DisplayDate(),
		     seconds = Seconds(),
		     k = k,
		     n = n,
		     status = "working"
		  }

		  print(format("Proposing pair %d/%d to %s", k, n, client.username))

		  --dump(jobList[k.."/"..n])

		  -- update the file containing all jobs
		  UpdateJobList(jobListFile, name, jobList[name])

		  -- ACK the client
		  net_Send(socket, "OK")

		  lowestKnPair = newlow

		  return
	       else
		  -- remove the zombie job and put back the original one
		  jobList[ks.."/"..ns] = job
		  break
	       end
	    end
	 end
	 net_Send(socket, "ERROR")
      end,

   GiveResult = 
      function(client, socket)
	 local t, k, n, result
	 t = net_Recv(socket)
	 k = net_Recv(socket)
	 n = net_Recv(socket)
	 result = net_Recv(socket)
	 --print(k, n, result)

	 if sqlUsed and sqlPairsTable and 
	    t == testType and k and n and result then

	    -- SQL
	    SqlGiveResult(client, socket, t, k, n, result)
	    return
	 end

	 if t == testType and k and n and result then
	    local name = k.."/"..n
	    local job = jobList[name]
	    local owned = job

	    if not job then
	       local i, j

	       job = {
		  user = client.username,
		  date = DisplayDate(),
		  seconds = Seconds(),
		  k = k,
		  n = n,
		  status = "working"
	       }

--	       j = getn(knPairs)
--	       for i=1, j, 1 do
--		  local pair = knPairs[i]
--		  if pair.k == k and pair.n == n then
--		     jobList[name] = job
--		     owned = job
--		     break
--		  end
--	       end
	       
	       if knPairs.lookup[name] then
		  jobList[name] = job
		  owned = job
	       end
	    end

	    if (noUserCheck or job.user == client.username) 
--	       and
--	       job.status == "working" 
	    then

	       if job.status == "solved" then
		  owned = nil
	       end
	       if result == "CANCEL" or result == "ERROR" then
		  job.status = "abandonned"
	       else
		  job.status = "solved"
	       end
	       job.result = result
	       job.resultdate = DisplayDate()

	       net_Send(socket, "OK")
	       
	       -- update the file containing all jobs
	       if owned then
		  UpdateJobList(jobListFile, name, job)
	       end

	       -- forward to the master proxy
	       ProxyForwardResult(t, k, n, result)

	       if result == "CANCEL" or result == "ERROR" then
		  return
	       end

	       if owned then
		  stats.results = stats.results + 1
		  WriteResultToFile(job, resultsFile)
	       else
		  stats.rejected = stats.rejected + 1
		  WriteResultToFile(job, rejectedFile)
	       end

	       if OnResult then
		  pcall(OnResult, t, k, n, result, job)
	       end
	       if result == "0" then
		  stats.primes = stats.primes + 1
		  if OnPrime then
		     pcall(OnPrime, t, k, n, job)
		  end
	       end

	       return
	    end
	 end
	 net_Send(socket, "ERROR")
      end
}

-- This function is called each time a client connects to the server
function OnConnect(socket)

   stats.connections = stats.connections + 1

   -- create a new client structure
   local client = { socket = socket }

   while 1 do
      -- Receive a command from the client
      local command = net_Recv(socket)
      --print(command)

      if not command or command == "Exit" then
	 if not command then
	    stats.errors = stats.errors + 1
	 end
	 break
      end

      -- check we are logged on for any command except the Login one
      if command ~= "Login" and not client.username then
	 stats.errors = stats.errors + 1
	 break
      end
      
      -- execute the command
      local f = commands[command]
      if f then
	 local res, error = pcall(f, client, socket)
	 if not res then
	    print("llrnet command lua error :", error)
	 end
      else
	 print("llrserver: Unknown command", command)
      end
   end

   ProxyUpdate()

   if (not sqlUsed or not sqlPairsTable) and not proxy then
      -- handle periodic pruning
      local s = Seconds()

      if prunePeriod and s - lastPruneDate > prunePeriod then
	 lastPruneDate = s

	 -- update the knpairs file and table
	 PrunePairs()
	 knPairs = ReadPairs(knPairsFile)

	 -- update joblist
	 PruneJoblist()
	 WriteJobList(jobListFile)
      end
   end

   -- WARNING : do not close the socket, it is closed automatically after
   -- OnConnect returns

   UpdateStatus()

   SqlDisconnect(client)
end

function Help()
   print([[
usage : llrserver.sh [options]
options :

 -h :
   print this message

 -d :
   detach server and run in background

 -s : 
   simplify joblist and knpairs files by removing solved pairs 
   from these files

 -sort-joblist : 
   sort the "joblist.txt" file and write it out as
   "sorted-joblist.txt" file

 -import-jobs :
   use this option when you upgrade your llrnet from non sql to sql.
   NOTE : this also prune knpairs and joblist and import the pairs.
   Be sure you have configured you sql options correctly into
   llr-serverconfig.txt first.

 -import-pairs :
   append pairs from knpairs.txt into the sql database. 

 -import-results :
   import results from results.txt into sql database.
   date must be in this type of format : 'Sun Feb  6 00:57:42 2005' or
   in mysql compatible date format (eg. '2005-2-4 23:23:12').

 -import-rejected :
   import rejected results from rejected.txt into sql database.
   (see above for supported date formats)

 -create-tables :
   create the sql tables necessary for llrnet.
]])
end

-- parse command line options
local i, n
n = getn(arg)
local starti = 2
if WIN32 then
   starti = 1
end
for i=starti,n,1 do
   if arg[i] == "-d" then
      detach()
   elseif arg[i] == "-s" then
      simplify = 1
   elseif arg[i] == "-sort-joblist" then
      sort_joblist = 1
   elseif arg[i] == "-h" then
      Help()
      return
   elseif arg[i] == "-import-pairs" then
      opt_importPairs = 1
   elseif arg[i] == "-import-jobs" then
      opt_importJobs = 1
      simplify = 1
   elseif arg[i] == "-create-tables" then
      opt_createTables = 1
   elseif arg[i] == "-import-results" then
      opt_importResults = 1
   elseif arg[i] == "-import-rejected" then
      opt_importRejected = 1
   elseif arg[i] == "-double-check" then
      opt_doubleCheck = 1
   else
      print(format("Unknown option '%s'", arg[i]))
      Help()
      return
   end
end

-- read configuration file
dofile("llr-serverconfig.txt")

-- read local config file
pcall(dofile, "llr-serverlocal.txt")

-- win32 specific : systray and service
dolib("win32")

-- import-results and import-rejected options
if opt_importResults or opt_importRejected then
   if not sqlPairsTable then
      Help()
      return
   end

   local client = { }
   SqlConnect(client)

   if opt_importResults then
      SqlImportResults(client, sqlResultsTable, resultsFile)
   end
   if opt_importRejected then
      SqlImportResults(client, sqlRejectedTable, rejectedFile)
   end

   SqlDisconnect(client)
   return
end

if opt_doubleCheck then
   -- fake client
   local client = { }
   SqlConnect(client)
   SqlDoubleCheck(client)
   SqlDisconnect(client)
   return
end

-- read all k/n pairs
knPairs = ReadPairs(knPairsFile)
if not knPairs then
   if not proxy then
      print("Could not read k/n pairs file", knPairsFile)
      return
   else
      knPairs = { lookup = { } }
   end
end

-- import-pairs option
if opt_importPairs then
   if not sqlPairsTable then
      Help()
      return
   end

   local client = { }
   SqlConnect(client)

   SqlImportPairs(client)

   SqlDisconnect(client)
   return
end

-- -create-tables option
if opt_createTables then
   if not sqlPairsTable then
      Help()
      return
   end

   local client = { }
   SqlConnect(client)

   SqlCreateTables(client)

   SqlDisconnect(client)

   return
end

-- sort joblist option
if sort_joblist then
   dofile(jobListFile)
   --PruneJoblist()
   WriteJobList("sorted-"..jobListFile)
   return
end

-- since ProxyUpdate set port to the master proxy port, we save 
-- this server's port into a local variable
local p = port

-- initialize proxy stuffs
if proxy then
   print("Set up for proxy server")
   if not proxyName or proxyName == "nobody" then
      print("Please enter the name of your proxy in llr-serverconfig.txt")
      return
   end
   username = proxyName
   ReadTosendfile()
end
ProxyUpdate()

-- read list of jobs
pcall(dofile, jobListFile)
PruneJoblist()
WriteJobList(jobListFile)

ProxyUpdate()

if simplify then
   PrunePairs()

   -- import-pairs option
   if opt_importJobs then
      if not sqlPairsTable then
	 Help()
	 return
      end
      
      knPairs = ReadPairs(knPairsFile)
      PruneJoblist()
      WriteJobList(jobListFile)

      local client = { }
      SqlConnect(client)
      
      SqlImportPairs(client)
      SqlImportJobs(client)
      
      SqlDisconnect(client)
   end

   return
end

lastPruneDate = Seconds()

stats.started = SqlDate()

function ServerThread()
   local res = net_Server(maxConnections, p)
   if res then
      print("Error while listening on port", p)
      print("Error code :", res)
   end
   serverRunning = nil
end

serverRunning = 1
create_thread(ServerThread, 128*1024)

statusUpdated = 1 -- force initial status refresh

while serverRunning do
   if statusUpdated and sqlUsed and sqlStatsTable then
      -- fake client
      local client = { }
      SqlConnect(client)
      SqlUpdateStatus(client)
      SqlDisconnect(client)
      statusUpdated = nil
   end
   SqlManageConnections()
   sleep(1)
end

-- close all SQL connections
SqlManageConnections(1)

if Win32Exit then
   Win32Exit()
end

print("llrserver : EXIT")
