First hack at cleaning up the specs

These are 90% full of lies
This commit is contained in:
Paul Betts
2014-09-28 20:31:22 -07:00
parent 3f430aef88
commit 45d4a14025
3 changed files with 40 additions and 109 deletions

View File

@@ -1,20 +1,12 @@
# Client-side Library
To be able to meet the specifications of the "updates" section of the README
(especially the bits about 'No Reboots', 'Updates should be applied while the
app is running'), we have to be a bit more clever than "Stuff everything in a
folder, hit go".
To be able to meet the specifications of the "updates" section of the README (especially the bits about 'No Reboots', 'Updates should be applied while the app is running'), we have to be a bit more clever than "Stuff everything in a folder, hit go".
### How can you replace DLLs while they're loaded? Impossible!
You can't. So, how can you do it? The basic trick that ClickOnce uses is, you
have a folder of EXEs and DLLs, and an Application Shortcut. When ClickOnce
goes to update its stuff, it builds a completely *new* folder of binaries,
then the last thing it does is rewrite the app shortcut to point to the new
folder.
You can't. So, how can you do it? The basic trick that ClickOnce uses is, you have a folder of EXEs and DLLs, and an Application Shortcut. When ClickOnce goes to update its stuff, it builds a completely *new* folder of binaries, then the last thing it does is rewrite the app shortcut to point to the new folder.
So, to that end, the installation root really only needs to consist of two
folders:
So, to that end, the installation root really only needs to consist of two folders:
```
\packages
@@ -24,90 +16,65 @@ folders:
\app-[version]
```
Packages is effectively immutable, it simply consists of the packages we've
downloaded. This means however, that we need write-access to our own install
directory - this is fine for per-user installs, but if the user has installed
to Program Files, we'll need to come up with another solution. And that
solution is, "Only support per-user installs".
Packages is effectively immutable, it simply consists of the packages we've downloaded. This means however, that we need write-access to our own install directory - this is fine for per-user installs, but if the user has installed to Program Files, we'll need to come up with another solution. And that solution is, "Only support per-user installs".
## The Update process, from start to finish
### Syncing the packages directory
The first thing that the Squirrel client will do to start the updates process, is
download the remote version of "Releases". Comparing this file to the Releases
file on disk will tell us whether an update is available.
The first thing that the Squirrel client will do to start the updates process, is download the remote version of "Releases". Comparing this file to the Releases file on disk will tell us whether an update is available.
Determining whether to use the delta packages or not will depend on the
download size - the updater will take the smaller of "latest full package" vs.
"Sum of all delta packages between current and latest". The updater makes a
choice, then fetches down all the files and checks them against the SHA1s in
the Releases file.
Determining whether to use the delta packages or not will depend on the download size - the updater will take the smaller of "latest full package" vs. "Sum of all delta packages between current and latest". The updater makes a choice, then fetches down all the files and checks them against the SHA1s in the Releases file.
If the installer decided to do a Delta update, it will then use the Delta
updates against the existing Full package to build a new Full package.
If the installer decided to do a Delta update, it will then use the Delta updates against the existing Full package to build a new Full package.
### Installing a full update
Since we've done the prep work to create a new NuGet package from the deltas,
the actual update process only has to deal with full NuGet packages. This is
as simple as:
Since we've done the prep work to create a new NuGet package from the deltas, the actual update process only has to deal with full NuGet packages. This is as simple as:
1. Extract the NuGet package to a temp dir
1. Move lib\net40 to \app-[newversion]
1. Move lib\net45 to \app-[newversion]
1. Rewrite the shortcut to point to \app-[newversion]
On next startup, we blow away \app-[version] since it's now the previous
version of the code.
On next startup, we blow away \app-[version] since it's now the previous version of the code.
### What do we do on Setup? (Bootstrapping)
Since the WiX setup application is too dumb to setup our default directory, in
order to simplify trying to bootstrap our app directory, we'll just recreate
it. This is some wasted bandwidth, but oh well. If the packages or app root
doesn't actually exist, we'll download the latest full release and set up the
app.
TODO
### Client-side API
Referencing Squirrel.Client.dll, `UpdateManager` is all the app dev needs to use.
Referencing Squirrel.dll, `UpdateManager` is all the app dev needs to use.
UpdateManager
UpdateInfo CheckForUpdates()
UpdateInfo DownloadUpdate()
List<string> ApplyUpdates()
`UpdateInfo` contains information about pending updates if there is
any, and is null if there isn't.
`UpdateInfo` contains information about pending updates if there is any, and is null if there isn't.
```
UpdateInfo
ReleaseEntry CurrentlyInstalledVersion
ReleaseEntry FutureReleaseEntry
IEnumerable<ReleaseEntry> ReleasesToApply
```
And `ReleaseEntry` contains the specifics of each release:
```
ReleaseEntry
string SHA1
string Filename
long Filesize
bool IsDelta
```
## Applying Updates
#### A note about Reactive Extensions
Squirrel uses Reactive Extensions (Rx) heavily as the process necessary to
retrieve, download and apply updates is best done asynchronously. If you
are using the `Microsoft.Bcl.Async` package (which Squirrel also uses) you
can combine the Rx APIs with the TPL async/await keywords, for maximum
simplicity.
### Check yourself
First, check the location where your application updates are hosted:
```
```cs
var updateManager = new UpdateManager(@"C:\Users\brendanforster\Desktop\TestApp",
"TestApp",
FrameworkVersion.Net40);
@@ -132,22 +99,21 @@ Depending on the result you get from this operation, you might:
### Fetch all the Updates
The result from `CheckForUpdates` will contain a list of releases to apply to
your current application.
The result from `CheckForUpdates` will contain a list of releases to apply to your current application.
That result becomes the input to `DownloadReleases`:
```
```cs
var releases = updateInfo.ReleasesToApply;
await updateManager.DownloadReleases(releases);
```
### Apply dem Updates
### Apply The Updates
And lastly, once those updates have been downloaded, tell Squirrel to apply them:
```
```cs
var results = await updateManager.ApplyReleases(downloadedUpdateInfo);
updateManager.Dispose(); // don't forget to tidy up after yourself
```

View File

@@ -2,30 +2,18 @@
## Major Pieces
We need:
- A client library, which includes the core update logic
- An executable / PowerShell script to implement `New-Release`
- The actual Setup.exe that Create-Release hacks up, as well as any related
implementation (WiX stuff?) that we need.
TODO
## Production / "Server Side"
### The tricky part
Ironically, the difficulty of using NuGet packages as a distribution container
for your app, is *if your app uses NuGet*. This is because NuGet (with good
reason!) packages the *list* of dependencies, not the actual binaries. So, if
we were to try to use the NuGet package of the App directly, we'd be missing a
bunch of DLLs.
Ironically, the difficulty of using NuGet packages as a distribution container for your app, is *if your app uses NuGet*. This is because NuGet (with good reason!) packages the *list* of dependencies, not the actual binaries. So, if we were to try to use the NuGet package of the App directly, we'd be missing a bunch of DLLs.
So, we need an application that can *flatten* a NuGet dependency tree and
repack the package with all the DLLs. While this is a lot of steps, it's
actually pretty straightforward:
So, we need an application that can *flatten* a NuGet dependency tree and repack the package with all the DLLs. While this is a lot of steps, it's actually pretty straightforward:
1. Extract the App's NuGet package to a temp directory.
1. Walk the list of dependencies. For each dependency, extract it on top of
the temp directory (i.e. so that its `lib/*` ends up in the App's dir)
1. Walk the list of dependencies. For each dependency, extract it on top of the temp directory (i.e. so that its `lib/*` ends up in the App's dir)
1. Recursively do the same thing (i.e. recurse down the dependency tree)
1. Edit the root NuGet package XML and remove all its explicit dependencies.
@@ -33,54 +21,34 @@ This is kind of the moral equivalent of the Rails Gem "vendor freeze" I guess.
### Delta Packages
Now, once we've got a full package, we need to generate a Delta package. To do
this, we'll replace all the DLL/EXEs in the NuGet packages with bsdiff files.
[bspatch/bsdiff](http://code.logos.com/blog/2010/12/binary_patching_with_bsdiff.html)
is a mostly efficient algorithm for calculating diffs between binary files
(especially Native binaries, but it works well for .NET ones too), and a way
to apply them.
Now, once we've got a full package, we need to generate a Delta package. To do this, we'll replace all the DLL/EXEs in the NuGet packages with bsdiff files. [bspatch/bsdiff](http://code.logos.com/blog/2010/12/binary_patching_with_bsdiff.html) is a mostly efficient algorithm for calculating diffs between binary files (especially Native binaries, but it works well for .NET ones too), and a way to apply them.
So, this is pretty easy:
1. Extract the previous NuGet package
1. Extract the current NuGet package
1. Replace every EXE/DLL with the bsdiff. So, `lib\net40\MyCoolApp.exe`
becomes `lib\net40\MyCoolApp.exe.diff`. Create a file that contains a SHA1
of the expected resulting file and its filesize called
`lib\net40\MyCoolApp.exe.shasum`
1. Replace every EXE/DLL with the bsdiff. So, `lib\net40\MyCoolApp.exe` becomes `lib\net40\MyCoolApp.exe.diff`. Create a file that contains a SHA1 of the expected resulting file and its filesize called `lib\net40\MyCoolApp.exe.shasum`
1. New DLLs in current get put in verbatim
1. Zip it back up
The .shasum file has the same format as the Releases file described in the
"'Latest' Pointer" section, except that it will only have one entry.
The .shasum file has the same format as the Releases file described in the "'Latest' Pointer" section, except that it will only have one entry.
So now we've got all of the *metadata* of the original package, just none of
its *contents*. To get the final package, we do the following:
So now we've got all of the *metadata* of the original package, just none of its *contents*. To get the final package, we do the following:
1. Take the previous version, expand it out
1. Take the delta version, do the same
1. For each DLL in the previous package, we bspatch it, then check the shasum
file to ensure we created the correct resulting file
1. For each DLL in the previous package, we bspatch it, then check the shasum file to ensure we created the correct resulting file
1. If we find a DLL in the new package, just copy it over
1. If we can't find a bspatch for a file, nuke it (it doesn't exist in the new
rev)
1. If we can't find a bspatch for a file, nuke it (it doesn't exist in the new rev)
1. Zip it back up
### ChangeLogs / Release Notes
To write release notes for each release, we're going to reuse the
`<ReleaseNotes>` NuSpec element. However, we're going to standard that you
can write Markdown in this element, and as part of generating a flattened
package, we will render this Markdown as HTML.
To write release notes for each release, we're going to reuse the `<ReleaseNotes>` NuSpec element. However, we're going to standard that you can write Markdown in this element, and as part of generating a flattened package, we will render this Markdown as HTML.
### "Latest" Pointer
One of the last things we do before finishing `Create-Release` is that we
write out a simple "Releases" file alongside the flattened and Delta NuGet
packages. This is a text file that has the name of all of the release package
filenames in the folder in release order (i.e. oldest at top, newest at
bottom), along with the SHA1 hashes of their contents and their file sizes.
So, something like:
One of the last things we do before finishing `Create-Release` is that we write out a simple "Releases" file alongside the flattened and Delta NuGet packages. This is a text file that has the name of all of the release package filenames in the folder in release order (i.e. oldest at top, newest at bottom), along with the SHA1 hashes of their contents and their file sizes. So, something like:
```
94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502
@@ -88,8 +56,6 @@ So, something like:
14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1-delta.nupkg 80396
```
This format has a number of advantages - it's dead simple, yet enables us to
check for package corruption, as well as makes it efficient to determine what
to do if a user gets multiple versions behind (i.e. whether it's worth it to
download all of the delta packages to catch them up, or to just download the
latest full package)
TODO about URL shit
This format has a number of advantages - it's dead simple, yet enables us to check for package corruption, as well as makes it efficient to determine what to do if a user gets multiple versions behind (i.e. whether it's worth it to download all of the delta packages to catch them up, or to just download the latest full package)

View File

@@ -5,16 +5,15 @@ At the end of the day, here's how a developer will use Squirrel:
1. Add the **Squirrel** package to your application
1. As part of the install for Squirrel, NuGet Package Build is enabled in the csproj file
1. The user edits the generated `.nuspec` to specify some details about their app
1. From the NuGet package console, run `New-Release` - this builds the world, and you end up with a `$SolutionDir/Releases/ASSEMBLYNAME` folder that has both a Squirrel release package as well as a `Setup.exe`
1. From the NuGet package console, run `Squirrel --releasify` - this builds the world, and you end up with a `$SolutionDir/Releases` folder that has both a Squirrel release package as well as a `Setup.exe`
## How does this work:
As part of adding Squirrel to your application, a `targets` file gets added to your csproj file. This targets file dumps all of the references in your application to the output directory in a simple text file, as well as a list of files marked as content.
Calling `New-Release` results in this process being kicked off:
Calling `Squirrel --releasify` results in this process being kicked off:
1. Call `$DTE` to build the current project, including the NuGet packages
1. For the current project, run `CreateReleasePackage.exe` to build release and delta packages.
1. For the current project, build release and delta packages.
1. Create a Zip file consisting of `update.exe` and the latest full release from `Releases`.
1. Using Win32 API Abuse™, put that into `setup.exe`, a C++ bootstrapper application whose sole goal is to download .NET 4.5, install it, then run update.exe
1. Copy that to the Releases folder.