Skip to content

Instantly share code, notes, and snippets.

@LSPDFR-PeterU
Last active April 2, 2017 19:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LSPDFR-PeterU/952de66a4f17b838d3870bdaabb6c43b to your computer and use it in GitHub Desktop.
Save LSPDFR-PeterU/952de66a4f17b838d3870bdaabb6c43b to your computer and use it in GitHub Desktop.
`AsyncUpdateChecker` for LSPDFR plugins
/**
Copyright 2017 PeterU
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
/**
* * * * * * *
Use it as follows:
* Change YourNameSpace below!
* Change the endpoints to point to your plugin's file ID ("/files/file/[fileid]" in the LCPDFR.com URL) and any other update notification servers you might have in case LCPDFR.com is down.
* Change references to my `CommonUtils.Log` methods to however you prefer to log to the RagePluginHook.log file, or remove them entirely.
* In your OnOnDutyChanged event handler:
var checker = new AsyncUpdateChecker();
GameFiber.StartNew(checker.Check, Assembly.GetExecutingAssembly().GetName().Name + "UpdateChecker");
* * * * * * *
**/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net;
using System.Reflection;
using Rage;
using System.IO;
namespace YourNameSpace
{
/// <summary>
/// Check for updates without pausing the game while the HTTP request goes through, by running the actual
/// network requests through a non-GameFiber Thread.
/// </summary>
internal class AsyncUpdateChecker
{
/// <summary>
/// Any HTTP/HTTPS endpoints we should use to check for updates, in order of preference. The assumption is they return
/// nothing but a string which can be parsed into a Version.
/// </summary>
private static readonly string[] endpoints =
{
"https://change-me-www.lcpdfr.com/applications/downloadsng/interface/api.php?do=checkForUpdates&fileId=SOME-FILE-ID&textOnly=1",
"https://somewhere-else.example.com/somewhere"
};
/// <summary>
/// The actual thread that will make the HTTP requests.
/// </summary>
private Thread networkThread = null;
/// <summary>
/// The retrieved Version that is considered most up-to-date.
/// </summary>
private Version retrievedVersion = null;
/// <summary>
/// The user agent string to send with HTTP requests.
/// </summary>
private static readonly string userAgent = Assembly.GetExecutingAssembly().GetName().Name + "/" + Assembly.GetExecutingAssembly().GetName().Version.ToString();
/// <summary>
/// The currently executing version of the plugin.
/// </summary>
private static readonly Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
/// <summary>
/// The initialization file that contains the last update check DateTime.
/// </summary>
private string baseIniFilename = Directory.GetCurrentDirectory() + @"\Plugins\LSPDFR\" + Assembly.GetExecutingAssembly().GetName().Name + ".ini";
/// <summary>
/// The name of this plugin.
/// </summary>
private static readonly string pluginName = Assembly.GetExecutingAssembly().GetName().Name;
/// <summary>
/// Intended to be run from a new GameFiber, this launches the async check in yet another thread
/// and blocks the GameFiber until a result is retrieved. NEVER call from the default GameFiber!
/// </summary>
internal void Check()
{
// determine if it is high time to check for updates
DateTime lastUpdateCheck = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); // start with a safe value -- i.e. avoid hitting the servers if something does go wrong
InitializationFile baseIni = null;
try
{
baseIni = new InitializationFile(baseIniFilename);
string dateLastCheckedForUpdates = baseIni.ReadString(pluginName, "LastUpdateCheck", "1970-01-01");
bool shouldCheckForUpdates = baseIni.ReadBoolean(pluginName, "CheckForUpdates", true);
if (!shouldCheckForUpdates)
{
CommonUtils.DebugLog($"Will not check for updates as user requests in .ini");
return;
}
DateTime.TryParse(dateLastCheckedForUpdates, out lastUpdateCheck); // don't really care about return value because if it failed, we have a safe lastUpdateCheck above
if (lastUpdateCheck.AddDays(1) > DateTime.Now)
{
CommonUtils.DebugLog($"Will not check for updates until at least {lastUpdateCheck.AddDays(1)}");
return;
}
}
catch (Exception e)
{
CommonUtils.LogException(e, "Unable to get last update check date/time.");
// let's play it safe and not hit servers unless we know we should!
return;
}
int waitTime = Main.Rnd.Next(7500, 45000);
CommonUtils.DebugLog($"Waiting {waitTime} before update check"); // avoid the barrage of notifications on ForceDuty
GameFiber.Sleep(waitTime);
networkThread = new Thread(MostRecentVersion);
networkThread.Start();
// wait for network thread to complete and fill retrievedVersion
while (retrievedVersion == null && networkThread.IsAlive)
{
GameFiber.Sleep(1000);
}
// now, either the thread died without getting a version, or retrievedVersion is populated
if (retrievedVersion == null)
{
CommonUtils.DebugLog($"Unable to check for updates. Sad!");
return;
}
if (retrievedVersion > currentVersion)
{
Game.DisplayNotification($"An ~g~update~w~ to ~b~{pluginName}~w~ is available (v~g~{retrievedVersion}~w~). Please visit LCPDFR.com to download it.");
Game.DisplayNotification($"You can turn off automatic update checking in ~y~{pluginName}.ini~w~ in ~y~plugins/LSPDFR~w~.");
CommonUtils.Log($"###### An update to {pluginName} is available ({retrievedVersion}). Please visit LCPDFR.com to download it.");
}
else
{
CommonUtils.Log($"{pluginName} is up-to-date. We will check again on the next real-world calendar day in which you go on duty.");
}
// write last update check time
baseIni.Write(pluginName, "LastUpdateCheck", DateTime.Now.ToString("yyyy-MM-dd"));
}
/// <summary>
/// Look up the most recent version of the plugin using the HTTP(S) endpoints. Since this will run in a non-GameFiber
/// thread so as not to block the game, DO NOT call anything that interacts with the game world from here. It seems that
/// Game.LogTrivial() etc are OK. Dumps its result into retrievedVersion so we can poll for that value from the GameFiber.
/// </summary>
private void MostRecentVersion()
{
WebClient updateChecker = new WebClient();
updateChecker.Headers.Add("User-Agent", userAgent);
foreach (string endpoint in endpoints)
{
string updateReply = "";
try
{
updateReply = updateChecker.DownloadString(endpoint);
}
catch (WebException we)
{
CommonUtils.LogException(we, $"Unable to get most recent version from endpoint {endpoint}");
}
catch (ThreadAbortException) { }
catch (Exception e)
{
CommonUtils.LogException(e, $"A non-WebException occurred downloading the string from {endpoint}");
}
if (updateReply.Length < 1)
{
continue;
}
// try to parse a version
if (Version.TryParse(updateReply, out retrievedVersion))
{
CommonUtils.DebugLog($"Most recent version is {retrievedVersion} (retrieved from {endpoint}).");
return;
}
else
{
CommonUtils.Log($"Failed to parse update reply from {endpoint} as a Version");
CommonUtils.DebugLog($"Retrieved string was {updateReply}");
}
}
CommonUtils.Log($"Failed to retrieve a Version from any update endpoint.");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment