mirror of
https://github.com/zhigang1992/react-native-code-push.git
synced 2026-05-14 02:14:52 +08:00
add windows support
This commit is contained in:
252
windows/.gitignore
vendored
Normal file
252
windows/.gitignore
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
644
windows/CodePush.cs
Normal file
644
windows/CodePush.cs
Normal file
@@ -0,0 +1,644 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ReactNative.Bridge;
|
||||
using ReactNative.Modules.Core;
|
||||
using ReactNative.UIManager;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using System.Xml;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Storage;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using Windows.Web.Http;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Windows.Storage.FileProperties;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
public class CodePush : IReactPackage
|
||||
{
|
||||
private static bool needToReportRollback = false;
|
||||
private static bool isRunningBinaryVersion = false;
|
||||
private static bool testConfigurationFlag = false;
|
||||
|
||||
private bool didUpdate = false;
|
||||
|
||||
private string assetsBundleFileName;
|
||||
|
||||
private static readonly string ASSETS_BUNDLE_PREFIX = "ms-appx:///ReactAssets/";
|
||||
private static readonly string BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime";
|
||||
private readonly string CODE_PUSH_PREFERENCES = "CodePush";
|
||||
private static readonly string DEFAULT_JS_BUNDLE_NAME = "index.windows.bundle";
|
||||
private readonly string DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress";
|
||||
private readonly string FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
|
||||
private static readonly string FILE_BUNDLE_PREFIX = "ms-appdata:///local";
|
||||
private readonly string PACKAGE_HASH_KEY = "packageHash";
|
||||
private readonly string PENDING_UPDATE_HASH_KEY = "hash";
|
||||
private readonly string PENDING_UPDATE_IS_LOADING_KEY = "isLoading";
|
||||
private readonly string PENDING_UPDATE_KEY = "CODE_PUSH_PENDING_UPDATE";
|
||||
|
||||
// This needs to be kept in sync with https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java#L78
|
||||
private readonly string REACT_DEV_BUNDLE_CACHE_FILE_NAME = "ReactNativeDevBundle.js";
|
||||
|
||||
// Helper classes.
|
||||
private CodePushNativeModule codePushNativeModule;
|
||||
private CodePushPackage codePushPackage;
|
||||
|
||||
// Config properties.
|
||||
private string appVersion;
|
||||
private string deploymentKey;
|
||||
private readonly string serverUrl = "https://codepush.azurewebsites.net/";
|
||||
|
||||
private ReactPage mainPage;
|
||||
|
||||
private static CodePush currentInstance;
|
||||
|
||||
public CodePush(string deploymentKey, ReactPage mainPage)
|
||||
{
|
||||
codePushPackage = new CodePushPackage();
|
||||
// TODO implement telemetryManager
|
||||
// this.codePushTelemetryManager = new CodePushTelemetryManager(this.applicationContext, CODE_PUSH_PREFERENCES);
|
||||
this.deploymentKey = deploymentKey;
|
||||
this.mainPage = mainPage;
|
||||
appVersion = Package.Current.Id.Version.Major + "." + Package.Current.Id.Version.Minor + "." + Package.Current.Id.Version.Build;
|
||||
InitializeUpdateAfterRestart();
|
||||
if (currentInstance != null)
|
||||
{
|
||||
CodePushUtils.log("More than one CodePush instance has been initialized. Please use the instance method codePush.getBundleUrlInternal() to get the correct bundleURL for a particular instance.");
|
||||
}
|
||||
|
||||
currentInstance = this;
|
||||
}
|
||||
|
||||
private async Task ClearReactDevBundleCache()
|
||||
{
|
||||
StorageFile devBundleCacheFile = null;
|
||||
try
|
||||
{
|
||||
devBundleCacheFile = await ApplicationData.Current.LocalFolder.GetFileAsync(REACT_DEV_BUNDLE_CACHE_FILE_NAME);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
if (devBundleCacheFile != null)
|
||||
{
|
||||
await devBundleCacheFile.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> GetBinaryResourcesModifiedTime()
|
||||
{
|
||||
StorageFile assetJSBundleFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(ASSETS_BUNDLE_PREFIX + assetsBundleFileName));
|
||||
BasicProperties fileProperties = await assetJSBundleFile.GetBasicPropertiesAsync();
|
||||
return fileProperties.DateModified.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
public static string GetBundleUrl()
|
||||
{
|
||||
return GetBundleUrl(DEFAULT_JS_BUNDLE_NAME);
|
||||
}
|
||||
|
||||
public static string GetBundleUrl(string assetsBundleFileName)
|
||||
{
|
||||
if (currentInstance == null)
|
||||
{
|
||||
throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?");
|
||||
}
|
||||
|
||||
return currentInstance.GetBundleUrlInternal(assetsBundleFileName).Result;
|
||||
}
|
||||
|
||||
public async Task<string> GetBundleUrlInternal(string assetsBundleFileName)
|
||||
{
|
||||
this.assetsBundleFileName = assetsBundleFileName;
|
||||
string binaryJsBundleUrl = ASSETS_BUNDLE_PREFIX + assetsBundleFileName;
|
||||
long binaryResourcesModifiedTime = await GetBinaryResourcesModifiedTime();
|
||||
StorageFile packageFile = await codePushPackage.GetCurrentPackageBundle(this.assetsBundleFileName);
|
||||
if (packageFile == null)
|
||||
{
|
||||
// There has not been any downloaded updates.
|
||||
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
|
||||
isRunningBinaryVersion = true;
|
||||
return binaryJsBundleUrl;
|
||||
}
|
||||
|
||||
JObject packageMetadata = await codePushPackage.GetCurrentPackage();
|
||||
long? binaryModifiedDateDuringPackageInstall = null;
|
||||
string binaryModifiedDateDuringPackageInstallString = (string)packageMetadata[BINARY_MODIFIED_TIME_KEY];
|
||||
if (binaryModifiedDateDuringPackageInstallString != null)
|
||||
{
|
||||
binaryModifiedDateDuringPackageInstall = long.Parse(binaryModifiedDateDuringPackageInstallString);
|
||||
}
|
||||
|
||||
string packageAppVersion = (string)packageMetadata["appVersion"];
|
||||
|
||||
// TODO: test configuration
|
||||
if (binaryModifiedDateDuringPackageInstall != null &&
|
||||
binaryModifiedDateDuringPackageInstall == binaryResourcesModifiedTime &&
|
||||
(IsUsingTestConfiguration() || appVersion.Equals(packageAppVersion)))
|
||||
{
|
||||
CodePushUtils.logBundleUrl(packageFile.Path);
|
||||
isRunningBinaryVersion = false;
|
||||
return FILE_BUNDLE_PREFIX + packageFile.Path.Replace(ApplicationData.Current.LocalFolder.Path, "").Replace("\\", "/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// The binary version is newer.
|
||||
didUpdate = false;
|
||||
if (!mainPage.UseDeveloperSupport || !appVersion.Equals(packageAppVersion))
|
||||
{
|
||||
await ClearUpdates();
|
||||
}
|
||||
|
||||
CodePushUtils.logBundleUrl(binaryJsBundleUrl);
|
||||
isRunningBinaryVersion = true;
|
||||
return binaryJsBundleUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationDataContainer GetCodePushSettings()
|
||||
{
|
||||
return ApplicationData.Current.LocalSettings.CreateContainer(CODE_PUSH_PREFERENCES, ApplicationDataCreateDisposition.Always);
|
||||
}
|
||||
|
||||
private JArray GetFailedUpdates()
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
string failedUpdatesString = (string)settings.Values[FAILED_UPDATES_KEY];
|
||||
if (failedUpdatesString == null)
|
||||
{
|
||||
return new JArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JArray.Parse(failedUpdatesString);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
JArray emptyArray = new JArray();
|
||||
settings.Values[FAILED_UPDATES_KEY] = JsonConvert.SerializeObject(emptyArray);
|
||||
return emptyArray;
|
||||
}
|
||||
}
|
||||
|
||||
private JObject GetPendingUpdate()
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
string pendingUpdateString = (string)settings.Values[PENDING_UPDATE_KEY];
|
||||
if (pendingUpdateString == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JObject.Parse(pendingUpdateString);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Should not happen.
|
||||
CodePushUtils.log("Unable to parse pending update metadata " + pendingUpdateString +
|
||||
" stored in SharedPreferences");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeUpdateAfterRestart()
|
||||
{
|
||||
JObject pendingUpdate = GetPendingUpdate();
|
||||
if (pendingUpdate != null)
|
||||
{
|
||||
didUpdate = true;
|
||||
bool updateIsLoading = (bool)pendingUpdate[PENDING_UPDATE_IS_LOADING_KEY];
|
||||
if (updateIsLoading)
|
||||
{
|
||||
// Pending update was initialized, but notifyApplicationReady was not called.
|
||||
// Therefore, deduce that it is a broken update and rollback.
|
||||
CodePushUtils.log("Update did not finish loading the last time, rolling back to a previous version.");
|
||||
needToReportRollback = true;
|
||||
RollbackPackage().Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear the React dev bundle cache so that new updates can be loaded.
|
||||
if (mainPage.UseDeveloperSupport)
|
||||
{
|
||||
ClearReactDevBundleCache().Wait();
|
||||
}
|
||||
// Mark that we tried to initialize the new update, so that if it crashes,
|
||||
// we will know that we need to rollback when the app next starts.
|
||||
SavePendingUpdate((string)pendingUpdate[PENDING_UPDATE_HASH_KEY], /* isLoading */true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsFailedHash(string packageHash)
|
||||
{
|
||||
JArray failedUpdates = GetFailedUpdates();
|
||||
if (packageHash != null)
|
||||
{
|
||||
foreach (JObject failedPackage in failedUpdates)
|
||||
{
|
||||
string failedPackageHash = (string)failedPackage[PACKAGE_HASH_KEY];
|
||||
if (packageHash.Equals(failedPackageHash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsPendingUpdate(string packageHash)
|
||||
{
|
||||
JObject pendingUpdate = GetPendingUpdate();
|
||||
return pendingUpdate != null &&
|
||||
!(bool)pendingUpdate[PENDING_UPDATE_IS_LOADING_KEY] &&
|
||||
(packageHash == null || ((string)pendingUpdate[PENDING_UPDATE_HASH_KEY]).Equals(packageHash));
|
||||
}
|
||||
|
||||
private void RemoveFailedUpdates()
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
settings.Values.Remove(FAILED_UPDATES_KEY);
|
||||
}
|
||||
|
||||
private void RemovePendingUpdate()
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
settings.Values.Remove(PENDING_UPDATE_KEY);
|
||||
}
|
||||
|
||||
private async Task RollbackPackage()
|
||||
{
|
||||
JObject failedPackage = await codePushPackage.GetCurrentPackage();
|
||||
SaveFailedUpdate(failedPackage);
|
||||
await codePushPackage.RollbackPackage();
|
||||
RemovePendingUpdate();
|
||||
}
|
||||
|
||||
private void SaveFailedUpdate(JObject failedPackage)
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
string failedUpdatesString = (string)settings.Values[FAILED_UPDATES_KEY];
|
||||
JArray failedUpdates;
|
||||
if (failedUpdatesString == null)
|
||||
{
|
||||
failedUpdates = new JArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
failedUpdates = JArray.Parse(failedUpdatesString);
|
||||
}
|
||||
|
||||
failedUpdates.Add(failedPackage);
|
||||
settings.Values[FAILED_UPDATES_KEY] = JsonConvert.SerializeObject(failedUpdates);
|
||||
}
|
||||
|
||||
private void SavePendingUpdate(string packageHash, bool isLoading)
|
||||
{
|
||||
ApplicationDataContainer settings = GetCodePushSettings();
|
||||
JObject pendingUpdate = new JObject();
|
||||
pendingUpdate[PENDING_UPDATE_HASH_KEY] = packageHash;
|
||||
pendingUpdate[PENDING_UPDATE_IS_LOADING_KEY] = isLoading;
|
||||
settings.Values[PENDING_UPDATE_KEY] = JsonConvert.SerializeObject(pendingUpdate);
|
||||
}
|
||||
|
||||
public static bool IsUsingTestConfiguration()
|
||||
{
|
||||
return testConfigurationFlag;
|
||||
}
|
||||
|
||||
public static void SetUsingTestConfiguration(bool shouldUseTestConfiguration)
|
||||
{
|
||||
testConfigurationFlag = shouldUseTestConfiguration;
|
||||
}
|
||||
|
||||
public async Task ClearUpdates()
|
||||
{
|
||||
await codePushPackage.ClearUpdates();
|
||||
RemovePendingUpdate();
|
||||
RemoveFailedUpdates();
|
||||
}
|
||||
|
||||
private class CodePushNativeModule : ReactContextNativeModuleBase
|
||||
{
|
||||
private ILifecycleEventListener lifecycleEventListener = null;
|
||||
private int minimumBackgroundDuration = 0;
|
||||
private ReactContext reactContext;
|
||||
|
||||
private class CodePushResumeListener : ILifecycleEventListener
|
||||
{
|
||||
private DateTime? lastPausedDate = null;
|
||||
private CodePushNativeModule codePushNativeModule;
|
||||
|
||||
public CodePushResumeListener(CodePushNativeModule codePushNativeModule)
|
||||
{
|
||||
this.codePushNativeModule = codePushNativeModule;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
if (lastPausedDate != null)
|
||||
{
|
||||
// Determine how long the app was in the background and ensure
|
||||
// that it meets the minimum duration amount of time.
|
||||
double durationInBackground = (new DateTime() - (DateTime)lastPausedDate).TotalSeconds;
|
||||
if (durationInBackground >= codePushNativeModule.minimumBackgroundDuration)
|
||||
{
|
||||
Action loadBundleAction = async () =>
|
||||
{
|
||||
await codePushNativeModule.LoadBundle();
|
||||
};
|
||||
|
||||
codePushNativeModule.Context.RunOnNativeModulesQueueThread(loadBundleAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSuspend()
|
||||
{
|
||||
// Save the current time so that when the app is later
|
||||
// resumed, we can detect how long it was in the background.
|
||||
lastPausedDate = new DateTime();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO get rid of this
|
||||
private CodePush codePush;
|
||||
|
||||
public CodePushNativeModule(ReactContext reactContext, CodePush codePush) : base(reactContext)
|
||||
{
|
||||
this.reactContext = reactContext;
|
||||
this.codePush = codePush;
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "CodePush";
|
||||
}
|
||||
}
|
||||
|
||||
public override IReadOnlyDictionary<string, object> Constants
|
||||
{
|
||||
get
|
||||
{
|
||||
Dictionary<string, object> constants = new Dictionary<string, object>();
|
||||
constants["codePushInstallModeImmediate"] = CodePushInstallMode.IMMEDIATE;
|
||||
constants["codePushInstallModeOnNextRestart"] = CodePushInstallMode.ON_NEXT_RESTART;
|
||||
constants["codePushInstallModeOnNextResume"] = CodePushInstallMode.ON_NEXT_RESUME;
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
codePush.InitializeUpdateAfterRestart();
|
||||
}
|
||||
|
||||
private async Task LoadBundle()
|
||||
{
|
||||
// #1) Get the private ReactInstanceManager, which is what includes
|
||||
// the logic to reload the current React context.
|
||||
FieldInfo info = typeof(ReactPage)
|
||||
.GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
ReactInstanceManager reactInstanceManager = (ReactInstanceManager)typeof(ReactPage)
|
||||
.GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.GetValue(codePush.mainPage);
|
||||
|
||||
// #2) Update the locally stored JS bundle file path
|
||||
Type reactInstanceManagerType = typeof(ReactInstanceManager);
|
||||
string latestJSBundleFile = await codePush.GetBundleUrlInternal(codePush.assetsBundleFileName);
|
||||
reactInstanceManagerType
|
||||
.GetField("_jsBundleFile", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.SetValue(reactInstanceManager, latestJSBundleFile);
|
||||
|
||||
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
|
||||
Action recreateReactContextAction = () =>
|
||||
{
|
||||
reactInstanceManager.RecreateReactContextInBackground();
|
||||
};
|
||||
Context.RunOnDispatcherQueueThread(recreateReactContextAction);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void downloadUpdate(JObject updatePackage, IPromise promise)
|
||||
{
|
||||
Action downloadAction = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
updatePackage[BINARY_MODIFIED_TIME_KEY] = "" + await codePush.GetBinaryResourcesModifiedTime();
|
||||
await codePush.codePushPackage.DownloadPackage(
|
||||
updatePackage,
|
||||
codePush.assetsBundleFileName,
|
||||
new Progress<HttpProgress>(
|
||||
(HttpProgress progress) =>
|
||||
{
|
||||
JObject downloadProgress = new JObject();
|
||||
downloadProgress["totalBytes"] = progress.TotalBytesToReceive;
|
||||
downloadProgress["receivedBytes"] = progress.BytesReceived;
|
||||
reactContext
|
||||
.GetJavaScriptModule<RCTDeviceEventEmitter>()
|
||||
.emit(codePush.DOWNLOAD_PROGRESS_EVENT_NAME, downloadProgress);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
JObject newPackage = await codePush.codePushPackage.GetPackage((string)updatePackage[codePush.PACKAGE_HASH_KEY]);
|
||||
promise.Resolve(newPackage);
|
||||
}
|
||||
catch (CodePushInvalidUpdateException e)
|
||||
{
|
||||
CodePushUtils.log(e.ToString());
|
||||
codePush.SaveFailedUpdate(updatePackage);
|
||||
promise.Reject(e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CodePushUtils.log(e.ToString());
|
||||
promise.Reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(downloadAction);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void getConfiguration(IPromise promise)
|
||||
{
|
||||
JObject config = new JObject();
|
||||
config["appVersion"] = codePush.appVersion;
|
||||
config["deploymentKey"] = codePush.deploymentKey;
|
||||
config["serverUrl"] = codePush.serverUrl;
|
||||
config["clientUniqueId"] = CodePushUtils.GetDeviceId();
|
||||
// TODO generate binary hash
|
||||
// string binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode);
|
||||
/*if (binaryHash != null)
|
||||
{
|
||||
// binaryHash will be null if the React Native assets were not bundled into the APK
|
||||
// (e.g. in Debug builds)
|
||||
configMap.putString(PACKAGE_HASH_KEY, binaryHash);
|
||||
}*/
|
||||
|
||||
promise.Resolve(config);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void getCurrentPackage(IPromise promise)
|
||||
{
|
||||
Action getCurrentPackageAction = async () =>
|
||||
{
|
||||
JObject currentPackage = await codePush.codePushPackage.GetCurrentPackage();
|
||||
if (currentPackage == null)
|
||||
{
|
||||
promise.Resolve("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunningBinaryVersion)
|
||||
{
|
||||
currentPackage["_isDebugOnly"] = true;
|
||||
}
|
||||
|
||||
bool isPendingUpdate = false;
|
||||
string currentHash = (string)currentPackage[codePush.PACKAGE_HASH_KEY];
|
||||
if (currentHash != null)
|
||||
{
|
||||
isPendingUpdate = codePush.IsPendingUpdate(currentHash);
|
||||
}
|
||||
|
||||
currentPackage["isPending"] = isPendingUpdate;
|
||||
promise.Resolve(currentPackage);
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(getCurrentPackageAction);
|
||||
}
|
||||
|
||||
|
||||
[ReactMethod]
|
||||
public void getNewStatusReport(IPromise promise)
|
||||
{
|
||||
// TODO implement this
|
||||
promise.Resolve("");
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void installUpdate(JObject updatePackage, int installMode, int minimumBackgroundDuration, IPromise promise)
|
||||
{
|
||||
Action installUpdateAction = async () =>
|
||||
{
|
||||
await codePush.codePushPackage.InstallPackage(updatePackage, codePush.IsPendingUpdate(null));
|
||||
string pendingHash = (string)updatePackage[codePush.PACKAGE_HASH_KEY];
|
||||
codePush.SavePendingUpdate(pendingHash, /* isLoading */false);
|
||||
if (installMode == (int)CodePushInstallMode.ON_NEXT_RESUME)
|
||||
{
|
||||
// Store the minimum duration on the native module as an instance
|
||||
// variable instead of relying on a closure below, so that any
|
||||
// subsequent resume-based installs could override it.
|
||||
codePush.codePushNativeModule.minimumBackgroundDuration = minimumBackgroundDuration;
|
||||
|
||||
if (lifecycleEventListener == null)
|
||||
{
|
||||
// Ensure we do not add the listener twice.
|
||||
lifecycleEventListener = new CodePushResumeListener(this);
|
||||
reactContext.AddLifecycleEventListener(lifecycleEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
promise.Resolve("");
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(installUpdateAction);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void isFailedUpdate(string packageHash, IPromise promise)
|
||||
{
|
||||
promise.Resolve(codePush.IsFailedHash(packageHash));
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void isFirstRun(string packageHash, IPromise promise)
|
||||
{
|
||||
Action isFirstRunAction = async () =>
|
||||
{
|
||||
bool isFirstRun = codePush.didUpdate
|
||||
&& packageHash != null
|
||||
&& packageHash.Length > 0
|
||||
&& packageHash.Equals(await codePush.codePushPackage.GetCurrentPackageHash());
|
||||
promise.Resolve(isFirstRun);
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(isFirstRunAction);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void notifyApplicationReady(IPromise promise)
|
||||
{
|
||||
codePush.RemovePendingUpdate();
|
||||
promise.Resolve("");
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
public void restartApp(bool onlyIfUpdateIsPending)
|
||||
{
|
||||
Action restartAppAction = async () =>
|
||||
{
|
||||
// If this is an unconditional restart request, or there
|
||||
// is current pending update, then reload the app.
|
||||
if (!onlyIfUpdateIsPending || codePush.IsPendingUpdate(null))
|
||||
{
|
||||
await LoadBundle();
|
||||
}
|
||||
};
|
||||
|
||||
Context.RunOnNativeModulesQueueThread(restartAppAction);
|
||||
}
|
||||
|
||||
[ReactMethod]
|
||||
// Replaces the current bundle with the one downloaded from removeBundleUrl.
|
||||
// It is only to be used during tests. No-ops if the test configuration flag is not set.
|
||||
public void downloadAndReplaceCurrentBundle(String remoteBundleUrl)
|
||||
{
|
||||
// TODO implement this
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<Type> CreateJavaScriptModulesConfig()
|
||||
{
|
||||
return new List<Type>();
|
||||
}
|
||||
|
||||
public IReadOnlyList<INativeModule> CreateNativeModules(ReactContext reactContext)
|
||||
{
|
||||
List<INativeModule> nativeModules = new List<INativeModule>();
|
||||
codePushNativeModule = new CodePushNativeModule(reactContext, this);
|
||||
//CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext, mainActivity);
|
||||
|
||||
nativeModules.Add(codePushNativeModule);
|
||||
//nativeModules.add(dialogModule);
|
||||
|
||||
return nativeModules;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IViewManager> CreateViewManagers(ReactContext reactContext)
|
||||
{
|
||||
return new List<IViewManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
144
windows/CodePush.csproj
Normal file
144
windows/CodePush.csproj
Normal file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{446A85D9-55EB-4C7D-8B9D-448306C833D6}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>CodePush</RootNamespace>
|
||||
<AssemblyName>CodePush</AssemblyName>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||
<TargetPlatformVersion>10.0.10586.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
|
||||
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\ARM\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<OutputPath>bin\ARM\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>ARM</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<OutputPath>bin\x64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>;2008</NoWarn>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- A reference to the entire .Net Framework and Windows SDK are automatically included -->
|
||||
<None Include="project.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CodePush.cs" />
|
||||
<Compile Include="CodePushInstallMode.cs" />
|
||||
<Compile Include="CodePushInvalidUpdateException.cs" />
|
||||
<Compile Include="CodePushNotInitializedException.cs" />
|
||||
<Compile Include="CodePushPackage.cs" />
|
||||
<Compile Include="CodePushUnknownException.cs" />
|
||||
<Compile Include="CodePushUpdateUtils.cs" />
|
||||
<Compile Include="CodePushUtils.cs" />
|
||||
<Compile Include="FileUtils.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<EmbeddedResource Include="Properties\CodePush.rd.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<SDKReference Include="WindowsMobile, Version=10.0.10586.0">
|
||||
<Name>Windows Mobile Extensions for the UWP</Name>
|
||||
</SDKReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\react-native\ReactWindows\ReactNative\ReactNative.csproj">
|
||||
<Project>{c7673ad5-e3aa-468c-a5fd-fa38154e205c}</Project>
|
||||
<Name>ReactNative</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
6
windows/CodePushInstallMode.cs
Normal file
6
windows/CodePushInstallMode.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
enum CodePushInstallMode
|
||||
{
|
||||
IMMEDIATE = 0,
|
||||
ON_NEXT_RESTART = 1,
|
||||
ON_NEXT_RESUME = 2
|
||||
}
|
||||
12
windows/CodePushInvalidUpdateException.cs
Normal file
12
windows/CodePushInvalidUpdateException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class CodePushInvalidUpdateException : Exception
|
||||
{
|
||||
public CodePushInvalidUpdateException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
12
windows/CodePushNotInitializedException.cs
Normal file
12
windows/CodePushNotInitializedException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
public class CodePushNotInitializedException : Exception
|
||||
{
|
||||
public CodePushNotInitializedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
348
windows/CodePushPackage.cs
Normal file
348
windows/CodePushPackage.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.Web.Http;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class CodePushPackage
|
||||
{
|
||||
private readonly string CODE_PUSH_FOLDER_PREFIX = "CodePush";
|
||||
private readonly string CURRENT_PACKAGE_KEY = "currentPackage";
|
||||
private readonly string DIFF_MANIFEST_FILE_NAME = "hotcodepush.json";
|
||||
private readonly string DOWNLOAD_FILE_NAME = "download.zip";
|
||||
private readonly string DOWNLOAD_URL_KEY = "downloadUrl";
|
||||
private readonly string PACKAGE_FILE_NAME = "app.json";
|
||||
private readonly string PACKAGE_HASH_KEY = "packageHash";
|
||||
private readonly string PREVIOUS_PACKAGE_KEY = "previousPackage";
|
||||
private readonly string RELATIVE_BUNDLE_PATH_KEY = "bundlePath";
|
||||
private readonly string STATUS_FILE = "codepush.json";
|
||||
private readonly string UNZIPPED_FOLDER_NAME = "unzipped";
|
||||
|
||||
public CodePushPackage()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<StorageFile> GetDownloadFile()
|
||||
{
|
||||
StorageFolder codePushFolder = await GetCodePushFolder();
|
||||
return await codePushFolder.CreateFileAsync(DOWNLOAD_FILE_NAME, CreationCollisionOption.OpenIfExists);
|
||||
}
|
||||
|
||||
private async Task<StorageFolder> GetUnzippedFolder()
|
||||
{
|
||||
StorageFolder codePushFolder = await GetCodePushFolder();
|
||||
return await codePushFolder.CreateFolderAsync(UNZIPPED_FOLDER_NAME, CreationCollisionOption.OpenIfExists);
|
||||
}
|
||||
|
||||
private async Task<StorageFolder> GetCodePushFolder()
|
||||
{
|
||||
return await ApplicationData.Current.LocalFolder.CreateFolderAsync(CODE_PUSH_FOLDER_PREFIX, CreationCollisionOption.OpenIfExists);
|
||||
}
|
||||
|
||||
private async Task<StorageFile> GetStatusFile()
|
||||
{
|
||||
StorageFolder codePushFolder = await GetCodePushFolder();
|
||||
return await codePushFolder.CreateFileAsync(STATUS_FILE, CreationCollisionOption.OpenIfExists);
|
||||
}
|
||||
|
||||
public async Task<JObject> GetCurrentPackageInfo()
|
||||
{
|
||||
StorageFile statusFile = await GetStatusFile();
|
||||
try
|
||||
{
|
||||
return await CodePushUtils.GetJObjectFromFile(statusFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CodePushUnknownException("Error getting current package info", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateCurrentPackageInfo(JObject packageInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
await FileIO.WriteTextAsync(await GetStatusFile(), JsonConvert.SerializeObject(packageInfo));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new CodePushUnknownException("Error updating current package info", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StorageFolder> GetCurrentPackageFolder()
|
||||
{
|
||||
JObject info = await GetCurrentPackageInfo();
|
||||
string packageHash = (string)info[CURRENT_PACKAGE_KEY];
|
||||
if (packageHash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await GetPackageFolder(packageHash, false);
|
||||
}
|
||||
|
||||
|
||||
public async Task<StorageFile> GetCurrentPackageBundle(string bundleFileName)
|
||||
{
|
||||
StorageFolder packageFolder = await GetCurrentPackageFolder();
|
||||
if (packageFolder == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JObject currentPackage = await GetCurrentPackage();
|
||||
string relativeBundlePath = (string)currentPackage[RELATIVE_BUNDLE_PATH_KEY];
|
||||
if (relativeBundlePath == null)
|
||||
{
|
||||
return await packageFolder.GetFileAsync(bundleFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await packageFolder.GetFileAsync(relativeBundlePath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StorageFolder> GetPackageFolder(string packageHash, bool createIfNotExists)
|
||||
{
|
||||
StorageFolder codePushFolder = await GetCodePushFolder();
|
||||
try
|
||||
{
|
||||
packageHash = shortenPackageHash(packageHash);
|
||||
if (createIfNotExists)
|
||||
{
|
||||
return await codePushFolder.CreateFolderAsync(packageHash, CreationCollisionOption.OpenIfExists);
|
||||
}
|
||||
else
|
||||
{
|
||||
return await codePushFolder.GetFolderAsync(packageHash);
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetCurrentPackageHash()
|
||||
{
|
||||
JObject metadata = await GetCurrentPackage();
|
||||
return (string)metadata[PACKAGE_HASH_KEY];
|
||||
}
|
||||
|
||||
public async Task<string> GetPreviousPackageHash()
|
||||
{
|
||||
JObject info = await GetCurrentPackageInfo();
|
||||
string previousPackageShortHash = (string)info[PREVIOUS_PACKAGE_KEY];
|
||||
if (previousPackageShortHash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JObject previousPackageMetadata = await GetPackage(previousPackageShortHash);
|
||||
if (previousPackageMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (string)previousPackageMetadata[PACKAGE_HASH_KEY];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> GetCurrentPackage()
|
||||
{
|
||||
StorageFolder currentPackageFolder = await GetCurrentPackageFolder();
|
||||
if (currentPackageFolder == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StorageFile packageFile = await currentPackageFolder.GetFileAsync(PACKAGE_FILE_NAME);
|
||||
return await CodePushUtils.GetJObjectFromFile(packageFile);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Should not happen unless the update metadata was somehow deleted.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> GetPackage(string packageHash)
|
||||
{
|
||||
StorageFolder packageFolder = await GetPackageFolder(packageHash, false);
|
||||
if (packageFolder == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StorageFile packageFile = await packageFolder.GetFileAsync(PACKAGE_FILE_NAME);
|
||||
return await CodePushUtils.GetJObjectFromFile(packageFile);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string shortenPackageHash(string packageHash)
|
||||
{
|
||||
return packageHash.Substring(0, 8);
|
||||
}
|
||||
|
||||
public async Task DownloadPackage(JObject updatePackage, string expectedBundleFileName, Progress<HttpProgress> downloadProgress)
|
||||
{
|
||||
StorageFolder codePushFolder = await GetCodePushFolder();
|
||||
string newUpdateHash = (string)updatePackage[PACKAGE_HASH_KEY];
|
||||
StorageFolder newUpdateFolder = await GetPackageFolder(newUpdateHash, false);
|
||||
if (newUpdateFolder != null)
|
||||
{
|
||||
// This removes any stale data in newPackageFolderPath that could have been left
|
||||
// uncleared due to a crash or error during the download or install process.
|
||||
await newUpdateFolder.DeleteAsync();
|
||||
}
|
||||
|
||||
newUpdateFolder = await GetPackageFolder(newUpdateHash, true);
|
||||
StorageFile newUpdateMetadataFile = await newUpdateFolder.CreateFileAsync(PACKAGE_FILE_NAME);
|
||||
string downloadUrlString = (string)updatePackage[DOWNLOAD_URL_KEY];
|
||||
StorageFile downloadFile = await GetDownloadFile();
|
||||
Uri downloadUri = new Uri(downloadUrlString);
|
||||
|
||||
// Download the file and send progress event asynchronously
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUri);
|
||||
HttpClient client = new HttpClient();
|
||||
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
|
||||
HttpResponseMessage response = await client.SendRequestAsync(request).AsTask(cancellationTokenSource.Token, downloadProgress);
|
||||
IInputStream inputStream = await response.Content.ReadAsInputStreamAsync();
|
||||
Stream downloadFileStream = await downloadFile.OpenStreamForWriteAsync();
|
||||
await RandomAccessStream.CopyAndCloseAsync(inputStream, downloadFileStream.AsOutputStream());
|
||||
try
|
||||
{
|
||||
// Unzip the downloaded file and then delete the zip
|
||||
StorageFolder unzippedFolder = await GetUnzippedFolder();
|
||||
ZipFile.ExtractToDirectory(downloadFile.Path, unzippedFolder.Path);
|
||||
await downloadFile.DeleteAsync();
|
||||
|
||||
// Merge contents with current update based on the manifest
|
||||
StorageFile diffManifestFile = null;
|
||||
try
|
||||
{
|
||||
diffManifestFile = await unzippedFolder.GetFileAsync(DIFF_MANIFEST_FILE_NAME);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// There is no diff manifest, so this is not a diff update.
|
||||
}
|
||||
|
||||
if (diffManifestFile != null)
|
||||
{
|
||||
StorageFolder currentPackageFolder = await GetCurrentPackageFolder();
|
||||
if (currentPackageFolder == null)
|
||||
{
|
||||
throw new CodePushInvalidUpdateException("Received a diff update, but there is no current version to diff against.");
|
||||
}
|
||||
|
||||
await CodePushUpdateUtils.CopyNecessaryFilesFromCurrentPackage(diffManifestFile, currentPackageFolder, newUpdateFolder);
|
||||
await diffManifestFile.DeleteAsync();
|
||||
}
|
||||
|
||||
await FileUtils.MergeDirectories(unzippedFolder, newUpdateFolder);
|
||||
await unzippedFolder.DeleteAsync();
|
||||
|
||||
// For zip updates, we need to find the relative path to the jsBundle and save it in the
|
||||
// metadata so that we can find and run it easily the next time.
|
||||
string relativeBundlePath = await CodePushUpdateUtils.FindJSBundleInUpdateContents(newUpdateFolder, expectedBundleFileName);
|
||||
if (relativeBundlePath == null)
|
||||
{
|
||||
throw new CodePushInvalidUpdateException("Update is invalid - A JS bundle file named \"" + expectedBundleFileName + "\" could not be found within the downloaded contents. Please check that you are releasing your CodePush updates using the exact same JS bundle file name that was shipped with your app's binary.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (diffManifestFile != null)
|
||||
{
|
||||
// TODO verify hash for diff update
|
||||
// CodePushUpdateUtils.verifyHashForDiffUpdate(newUpdateFolderPath, newUpdateHash);
|
||||
}
|
||||
|
||||
updatePackage[RELATIVE_BUNDLE_PATH_KEY] = relativeBundlePath;
|
||||
}
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
// Downloaded file is not a zip, assume it is a jsbundle
|
||||
await downloadFile.RenameAsync(expectedBundleFileName);
|
||||
await downloadFile.MoveAsync(newUpdateFolder);
|
||||
}
|
||||
|
||||
// Save metadata to the folder
|
||||
await FileIO.WriteTextAsync(newUpdateMetadataFile, JsonConvert.SerializeObject(updatePackage));
|
||||
}
|
||||
|
||||
public async Task InstallPackage(JObject updatePackage, bool removePendingUpdate)
|
||||
{
|
||||
string packageHash = (string)updatePackage[PACKAGE_HASH_KEY];
|
||||
JObject info = await GetCurrentPackageInfo();
|
||||
if (removePendingUpdate)
|
||||
{
|
||||
StorageFolder currentPackageFolder = await GetCurrentPackageFolder();
|
||||
if (currentPackageFolder != null)
|
||||
{
|
||||
await currentPackageFolder.DeleteAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string previousPackageHash = await GetPreviousPackageHash();
|
||||
if (previousPackageHash != null && !previousPackageHash.Equals(packageHash))
|
||||
{
|
||||
StorageFolder previousPackageFolder = await GetPackageFolder(previousPackageHash, false);
|
||||
if (previousPackageFolder != null)
|
||||
{
|
||||
await previousPackageFolder.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
info[PREVIOUS_PACKAGE_KEY] = info[CURRENT_PACKAGE_KEY];
|
||||
}
|
||||
|
||||
info[CURRENT_PACKAGE_KEY] = packageHash;
|
||||
await UpdateCurrentPackageInfo(info);
|
||||
}
|
||||
|
||||
public async Task RollbackPackage()
|
||||
{
|
||||
JObject info = await GetCurrentPackageInfo();
|
||||
StorageFolder currentPackageFolder = await GetCurrentPackageFolder();
|
||||
if (currentPackageFolder != null)
|
||||
{
|
||||
await currentPackageFolder.DeleteAsync();
|
||||
}
|
||||
|
||||
info[CURRENT_PACKAGE_KEY] = info[PREVIOUS_PACKAGE_KEY];
|
||||
info[PREVIOUS_PACKAGE_KEY] = null;
|
||||
await UpdateCurrentPackageInfo(info);
|
||||
}
|
||||
|
||||
public void DownloadAndReplaceCurrentBundle(string remoteBundleUrl, string bundleFileName)
|
||||
{
|
||||
// TODO implement this method (only used in tests)
|
||||
}
|
||||
|
||||
public async Task ClearUpdates()
|
||||
{
|
||||
await (await GetStatusFile()).DeleteAsync();
|
||||
await (await GetCodePushFolder()).DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
windows/CodePushUnknownException.cs
Normal file
12
windows/CodePushUnknownException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class CodePushUnknownException : Exception
|
||||
{
|
||||
public CodePushUnknownException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
49
windows/CodePushUpdateUtils.cs
Normal file
49
windows/CodePushUpdateUtils.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class CodePushUpdateUtils
|
||||
{
|
||||
// TODO: Generate binary hash
|
||||
// private static readonly String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json";
|
||||
|
||||
public async static Task CopyNecessaryFilesFromCurrentPackage(StorageFile diffManifestFile, StorageFolder currentPackageFolder, StorageFolder newPackageFolder)
|
||||
{
|
||||
await FileUtils.MergeDirectories(currentPackageFolder, newPackageFolder);
|
||||
JObject diffManifest = await CodePushUtils.GetJObjectFromFile(diffManifestFile);
|
||||
JArray deletedFiles = (JArray)diffManifest["deletedFiles"];
|
||||
foreach (string fileNameToDelete in deletedFiles)
|
||||
{
|
||||
StorageFile fileToDelete = await newPackageFolder.GetFileAsync(fileNameToDelete);
|
||||
await fileToDelete.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async static Task<string> FindJSBundleInUpdateContents(StorageFolder updateFolder, string expectedFileName)
|
||||
{
|
||||
foreach (StorageFile file in await updateFolder.GetFilesAsync())
|
||||
{
|
||||
string fileName = file.Name;
|
||||
if (fileName.Equals(expectedFileName))
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (StorageFolder folder in await updateFolder.GetFoldersAsync())
|
||||
{
|
||||
string mainBundlePathInSubFolder = await FindJSBundleInUpdateContents(folder, expectedFileName);
|
||||
if (mainBundlePathInSubFolder != null)
|
||||
{
|
||||
return Path.Combine(folder.Name, mainBundlePathInSubFolder);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
windows/CodePushUtils.cs
Normal file
50
windows/CodePushUtils.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.System.Profile;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class CodePushUtils
|
||||
{
|
||||
public static readonly string REACT_NATIVE_LOG_CATEGORY = "ReactNative";
|
||||
|
||||
public async static Task<JObject> GetJObjectFromFile(StorageFile file)
|
||||
{
|
||||
string jsonString = await FileIO.ReadTextAsync(file);
|
||||
if (jsonString.Length == 0)
|
||||
{
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
return JObject.Parse(jsonString);
|
||||
}
|
||||
|
||||
public static void log(string message)
|
||||
{
|
||||
Debug.WriteLine("[CodePush] " + message, REACT_NATIVE_LOG_CATEGORY);
|
||||
}
|
||||
|
||||
public static void logBundleUrl(string path)
|
||||
{
|
||||
log("Loading JS bundle from \"" + path + "\"");
|
||||
}
|
||||
|
||||
public static string GetDeviceId()
|
||||
{
|
||||
HardwareToken token = Windows.System.Profile.HardwareIdentification.GetPackageSpecificToken(null);
|
||||
IBuffer hardwareId = token.Id;
|
||||
DataReader dataReader = DataReader.FromBuffer(hardwareId);
|
||||
|
||||
byte[] bytes = new byte[hardwareId.Length];
|
||||
dataReader.ReadBytes(bytes);
|
||||
|
||||
return BitConverter.ToString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
windows/FileUtils.cs
Normal file
27
windows/FileUtils.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace ReactNative.CodePush
|
||||
{
|
||||
class FileUtils
|
||||
{
|
||||
public async static Task MergeDirectories(StorageFolder source, StorageFolder target)
|
||||
{
|
||||
foreach (StorageFile sourceFile in await source.GetFilesAsync())
|
||||
{
|
||||
await sourceFile.CopyAndReplaceAsync(await target.CreateFileAsync(sourceFile.Name, CreationCollisionOption.OpenIfExists));
|
||||
}
|
||||
|
||||
foreach (StorageFolder sourceDirectory in await source.GetFoldersAsync())
|
||||
{
|
||||
StorageFolder nextTargetSubDir = await target.CreateFolderAsync(sourceDirectory.Name, CreationCollisionOption.OpenIfExists);
|
||||
await MergeDirectories(sourceDirectory, nextTargetSubDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
windows/Properties/AssemblyInfo.cs
Normal file
29
windows/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("CodePush")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("CodePush")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2016")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: ComVisible(false)]
|
||||
33
windows/Properties/CodePush.rd.xml
Normal file
33
windows/Properties/CodePush.rd.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This file contains Runtime Directives, specifications about types your application accesses
|
||||
through reflection and other dynamic code patterns. Runtime Directives are used to control the
|
||||
.NET Native optimizer and ensure that it does not remove code accessed by your library. If your
|
||||
library does not do any reflection, then you generally do not need to edit this file. However,
|
||||
if your library reflects over types, especially types passed to it or derived from its types,
|
||||
then you should write Runtime Directives.
|
||||
|
||||
The most common use of reflection in libraries is to discover information about types passed
|
||||
to the library. Runtime Directives have three ways to express requirements on types passed to
|
||||
your library.
|
||||
|
||||
1. Parameter, GenericParameter, TypeParameter, TypeEnumerableParameter
|
||||
Use these directives to reflect over types passed as a parameter.
|
||||
|
||||
2. SubTypes
|
||||
Use a SubTypes directive to reflect over types derived from another type.
|
||||
|
||||
3. AttributeImplies
|
||||
Use an AttributeImplies directive to indicate that your library needs to reflect over
|
||||
types or methods decorated with an attribute.
|
||||
|
||||
For more information on writing Runtime Directives for libraries, please visit
|
||||
http://go.microsoft.com/fwlink/?LinkID=391919
|
||||
-->
|
||||
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
|
||||
<Library Name="CodePush">
|
||||
|
||||
<!-- add directives for your library here -->
|
||||
|
||||
</Library>
|
||||
</Directives>
|
||||
17
windows/project.json
Normal file
17
windows/project.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0",
|
||||
"Newtonsoft.Json": "8.0.3"
|
||||
},
|
||||
"frameworks": {
|
||||
"uap10.0": {}
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-arm": {},
|
||||
"win10-arm-aot": {},
|
||||
"win10-x86": {},
|
||||
"win10-x86-aot": {},
|
||||
"win10-x64": {},
|
||||
"win10-x64-aot": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user