From 842f5dd7f890a9fed28d5e32480020ab327e07eb Mon Sep 17 00:00:00 2001 From: joelhulen Date: Sat, 5 Dec 2015 18:16:36 -0800 Subject: [PATCH] Initial commit --- AppvNext.Throttlebird.sln | 22 ++ .../AppvNext.Throttlebird.csproj | 70 +++++ AppvNext.Throttlebird/Extensions.cs | 53 ++++ .../Properties/AssemblyInfo.cs | 36 +++ AppvNext.Throttlebird/Throttlebird.nuspec | 25 ++ AppvNext.Throttlebird/Throttlebird.snk | Bin 0 -> 596 bytes .../Throttling/IThrottleStore.cs | 19 ++ .../Throttling/InMemoryThrottleStore.cs | 47 +++ .../Throttling/ThrottleEntry.cs | 22 ++ .../Throttling/ThrottlingHandler.cs | 100 ++++++ LICENSE.txt | 26 ++ build.bat | 6 + build.cake | 291 ++++++++++++++++++ build.ps1 | 132 ++++++++ 14 files changed, 849 insertions(+) create mode 100644 AppvNext.Throttlebird.sln create mode 100644 AppvNext.Throttlebird/AppvNext.Throttlebird.csproj create mode 100644 AppvNext.Throttlebird/Extensions.cs create mode 100644 AppvNext.Throttlebird/Properties/AssemblyInfo.cs create mode 100644 AppvNext.Throttlebird/Throttlebird.nuspec create mode 100644 AppvNext.Throttlebird/Throttlebird.snk create mode 100644 AppvNext.Throttlebird/Throttling/IThrottleStore.cs create mode 100644 AppvNext.Throttlebird/Throttling/InMemoryThrottleStore.cs create mode 100644 AppvNext.Throttlebird/Throttling/ThrottleEntry.cs create mode 100644 AppvNext.Throttlebird/Throttling/ThrottlingHandler.cs create mode 100644 LICENSE.txt create mode 100644 build.bat create mode 100644 build.cake create mode 100644 build.ps1 diff --git a/AppvNext.Throttlebird.sln b/AppvNext.Throttlebird.sln new file mode 100644 index 0000000..816b2e5 --- /dev/null +++ b/AppvNext.Throttlebird.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppvNext.Throttlebird", "AppvNext.Throttlebird\AppvNext.Throttlebird.csproj", "{1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/AppvNext.Throttlebird/AppvNext.Throttlebird.csproj b/AppvNext.Throttlebird/AppvNext.Throttlebird.csproj new file mode 100644 index 0000000..2438bb2 --- /dev/null +++ b/AppvNext.Throttlebird/AppvNext.Throttlebird.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01} + Library + Properties + AppvNext.Throttlebird + AppvNext.Throttlebird + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + Throttlebird.snk + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AppvNext.Throttlebird/Extensions.cs b/AppvNext.Throttlebird/Extensions.cs new file mode 100644 index 0000000..4fd6f87 --- /dev/null +++ b/AppvNext.Throttlebird/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Net.Http; + +namespace AppvNext.Throttlebird.Extensions +{ + public static class HttpRequestMessageExtensions + { + private const string HttpContext = "MS_HttpContext"; + private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; + private const string OwinContext = "MS_OwinContext"; + + public static bool IsLocal(this HttpRequestMessage request) + { + var localFlag = request.Properties["MS_IsLocal"] as Lazy; + return localFlag != null && localFlag.Value; + } + + public static string GetClientIpAddress(this HttpRequestMessage request) + { + //Web-hosting + if (request.Properties.ContainsKey(HttpContext)) + { + dynamic ctx = request.Properties[HttpContext]; + if (ctx != null) + { + return ctx.Request.UserHostAddress; + } + } + //Self-hosting + if (request.Properties.ContainsKey(RemoteEndpointMessage)) + { + dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; + if (remoteEndpoint != null) + { + return remoteEndpoint.Address; + } + } + //Owin-hosting + if (request.Properties.ContainsKey(OwinContext)) + { + dynamic ctx = request.Properties[OwinContext]; + if (ctx != null) + { + return ctx.Request.RemoteIpAddress; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/AppvNext.Throttlebird/Properties/AssemblyInfo.cs b/AppvNext.Throttlebird/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b0fcd66 --- /dev/null +++ b/AppvNext.Throttlebird/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +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("AppvNext.Throttlebird")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AppvNext.Throttlebird")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1e86b5c1-eea2-429c-82c7-7dbd5f978a01")] + +// 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")] diff --git a/AppvNext.Throttlebird/Throttlebird.nuspec b/AppvNext.Throttlebird/Throttlebird.nuspec new file mode 100644 index 0000000..8b71203 --- /dev/null +++ b/AppvNext.Throttlebird/Throttlebird.nuspec @@ -0,0 +1,25 @@ + + + + App vNext + Joel Hulen, App vNext + + Throttlebird is a simple Http request throttler to help limit the number of client requests within a given period of time. + + en-US + https://raw.github.com/App-vNext/Throttlebird/master/LICENSE.txt + https://github.com/App-vNext/Throttlebird + ASP.NET MVC HTTP Tools + Copyright � 2015, App vNext + + 1.0.0 + --------------------- + - Initial release + + + + + + + + \ No newline at end of file diff --git a/AppvNext.Throttlebird/Throttlebird.snk b/AppvNext.Throttlebird/Throttlebird.snk new file mode 100644 index 0000000000000000000000000000000000000000..e10230284a85c580c735291581807bedb0df179b GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096`7%CxakUM2k<7gjU&wFxfW&DT0YFfj@ z87FzqHSbGq$I0k{1Po>G<;qC6syr0rISbO@z2~Pp9i^$IPNd7)I6KIWIIMA}s31Gw`*;D0=GIY+qBivkhZfJok?snp4psXx zZp6ng`)t|1Vwr-ZKu-K6?@vz1tcu$y#nv8?06c8pgM*eN({>K9qM?y{2I5s^l4cX9 zp4~|K$35)P@{%FGP^7Y*`G15o0#a55we`)OVZJSFmU<~zbgH3$wd}qsuXi2a7DNf( z+2y5O>tkv@8xN>e6p&s~5ULleBA5+^HM + /// Interface for caching request throttling data. + /// + public interface IThrottleStore + { + bool TryGetValue(string key, out ThrottleEntry entry); + void IncrementRequests(string key); + void Rollover(string key); + void Clear(); + } +} diff --git a/AppvNext.Throttlebird/Throttling/InMemoryThrottleStore.cs b/AppvNext.Throttlebird/Throttling/InMemoryThrottleStore.cs new file mode 100644 index 0000000..12edf9e --- /dev/null +++ b/AppvNext.Throttlebird/Throttling/InMemoryThrottleStore.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace AppvNext.Throttlebird.Throttling +{ + /// + /// Creates an in-memory throttle cache to keep track of how many Http + /// requests within a timespan each client is making. + /// + public class InMemoryThrottleStore : IThrottleStore + { + private readonly ConcurrentDictionary _throttleStore = new ConcurrentDictionary(); + + public bool TryGetValue(string key, out ThrottleEntry entry) + { + return _throttleStore.TryGetValue(key, out entry); + } + + public void IncrementRequests(string key) + { + _throttleStore.AddOrUpdate(key, + k => + { + return new ThrottleEntry() { Requests = 1 }; + }, + (k, e) => + { + e.Requests++; + return e; + }); + } + + public void Rollover(string key) + { + ThrottleEntry dummy; + _throttleStore.TryRemove(key, out dummy); + } + + public void Clear() + { + _throttleStore.Clear(); + } + } +} \ No newline at end of file diff --git a/AppvNext.Throttlebird/Throttling/ThrottleEntry.cs b/AppvNext.Throttlebird/Throttling/ThrottleEntry.cs new file mode 100644 index 0000000..83f88e4 --- /dev/null +++ b/AppvNext.Throttlebird/Throttling/ThrottleEntry.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace AppvNext.Throttlebird.Throttling +{ + /// + /// Creates an entry in our throttle store. + /// + public class ThrottleEntry + { + public DateTime PeriodStart { get; set; } + public long Requests { get; set; } + + public ThrottleEntry() + { + PeriodStart = DateTime.UtcNow; + Requests = 0; + } + } +} \ No newline at end of file diff --git a/AppvNext.Throttlebird/Throttling/ThrottlingHandler.cs b/AppvNext.Throttlebird/Throttling/ThrottlingHandler.cs new file mode 100644 index 0000000..cd1b696 --- /dev/null +++ b/AppvNext.Throttlebird/Throttling/ThrottlingHandler.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using AppvNext.Throttlebird.Extensions; + +namespace AppvNext.Throttlebird.Throttling +{ + public class ThrottlingHandler + : DelegatingHandler + { + private readonly IThrottleStore _store; + private readonly Func _maxRequestsForUserIdentifier; + private readonly TimeSpan _period; + private readonly string _message; + + public ThrottlingHandler(IThrottleStore store, Func maxRequestsForUserIdentifier, TimeSpan period) + : this(store, maxRequestsForUserIdentifier, period, "The allowed number of requests has been exceeded.") + { + } + + public ThrottlingHandler(IThrottleStore store, Func maxRequestsForUserIdentifier, TimeSpan period, string message) + { + _store = store; + _maxRequestsForUserIdentifier = maxRequestsForUserIdentifier; + _period = period; + _message = message; + } + + protected virtual string GetUserIdentifier(HttpRequestMessage request) + { + return request.GetClientIpAddress(); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var identifier = GetUserIdentifier(request); + + if (string.IsNullOrEmpty(identifier)) + { + return CreateResponse(request, HttpStatusCode.Forbidden, "Could not identify client."); + } + + var maxRequests = _maxRequestsForUserIdentifier(identifier); + + ThrottleEntry entry = null; + if (_store.TryGetValue(identifier, out entry)) + { + if (entry.PeriodStart + _period < DateTime.UtcNow) + { + _store.Rollover(identifier); + } + } + _store.IncrementRequests(identifier); + if (!_store.TryGetValue(identifier, out entry)) + { + return CreateResponse(request, HttpStatusCode.Forbidden, "Could not identify client."); + } + + Task response = null; + if (entry.Requests > maxRequests) + { + response = CreateResponse(request, HttpStatusCode.ServiceUnavailable, _message); + } + else + { + response = base.SendAsync(request, cancellationToken); + } + + return response.ContinueWith(task => + { + var remaining = maxRequests - entry.Requests; + if (remaining < 0) + { + remaining = 0; + } + + var httpResponse = task.Result; + httpResponse.Headers.Add("RateLimit-Limit", maxRequests.ToString()); + httpResponse.Headers.Add("RateLimit-Remaining", remaining.ToString()); + + return httpResponse; + }); + } + + protected Task CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, string message) + { + var tsc = new TaskCompletionSource(); + var response = request.CreateResponse(statusCode); + response.ReasonPhrase = message; + response.Content = new StringContent(message); + tsc.SetResult(response); + return tsc.Task; + } + } +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..05e6fb6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ +New BSD License += +Copyright (c) 2015, App vNext +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of the App vNext nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 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. \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..8dbd671 --- /dev/null +++ b/build.bat @@ -0,0 +1,6 @@ +@ECHO OFF +PUSHD %~dp0 +PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "& './build.ps1'" + +IF %errorlevel% neq 0 PAUSE + diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..52362cb --- /dev/null +++ b/build.cake @@ -0,0 +1,291 @@ +/////////////////////////////////////////////////////////////////////////////// +// ARGUMENTS +/////////////////////////////////////////////////////////////////////////////// + +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); + +////////////////////////////////////////////////////////////////////// +// EXTERNAL NUGET TOOLS +////////////////////////////////////////////////////////////////////// + +#Tool "xunit.runner.console" +#Tool "GitVersion.CommandLine" +#Tool "Brutal.Dev.StrongNameSigner" +#Tool "NuSpec.ReferenceGenerator" + +////////////////////////////////////////////////////////////////////// +// EXTERNAL NUGET LIBRARIES +////////////////////////////////////////////////////////////////////// + +#addin "System.Text.Json" +using System.Text.Json; + +/////////////////////////////////////////////////////////////////////////////// +// GLOBAL VARIABLES +/////////////////////////////////////////////////////////////////////////////// + +var projectName = "Throttlebird"; +var keyName = "Throttlebird.snk"; + +var solutions = GetFiles("./**/*.sln"); +var solutionPaths = solutions.Select(solution => solution.GetDirectory()); + +var srcDir = Directory("./AppvNext.Throttlebird"); +var buildDir = Directory("./build"); +var artifactsDir = Directory("./artifacts"); +var testResultsDir = artifactsDir + Directory("test-results"); + +// NuGet +var nuspecFilename = projectName + ".nuspec"; +var nuspecSrcFile = srcDir + File(nuspecFilename); +var nuspecDestFile = buildDir + File(nuspecFilename); +var nupkgDestDir = artifactsDir + Directory("nuget-package"); +var snkFile = srcDir + File(keyName); + +var projectToNugetFolderMap = new Dictionary() { + { "Net45", new [] {"net45"} } +}; + +// Gitversion +var gitVersionPath = ToolsExePath("GitVersion.exe"); +Dictionary gitVersionOutput; + +// StrongNameSigner +var strongNameSignerPath = ToolsExePath("StrongNameSigner.Console.exe"); + +// NuSpec.ReferenceGenerator +var refGenPath = ToolsExePath("RefGen.exe"); + +/////////////////////////////////////////////////////////////////////////////// +// SETUP / TEARDOWN +/////////////////////////////////////////////////////////////////////////////// + +Setup(() => +{ + Information(""); + Information(" ████████╗██╗ ██╗██████╗ ██████╗ ████████╗████████╗██╗ ███████╗██████╗ ██╗██████╗ ██████╗ "); + Information(" ╚══██╔══╝██║ ██║██╔══██╗██╔═══██╗╚══██╔══╝╚══██╔══╝██║ ██╔════╝██╔══██╗██║██╔══██╗██╔══██╗"); + Information(" ██║ ███████║██████╔╝██║ ██║ ██║ ██║ ██║ █████╗ ██████╔╝██║██████╔╝██║ ██║"); + Information(" ██║ ██╔══██║██╔══██╗██║ ██║ ██║ ██║ ██║ ██╔══╝ ██╔══██╗██║██╔══██╗██║ ██║"); + Information(" ██║ ██║ ██║██║ ██║╚██████╔╝ ██║ ██║ ███████╗███████╗██████╔╝██║██║ ██║██████╔╝"); + Information(" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ "); + Information(""); +}); + +Teardown(() => +{ + Information("Finished running tasks."); +}); + +////////////////////////////////////////////////////////////////////// +// PRIVATE TASKS +////////////////////////////////////////////////////////////////////// + +Task("__Clean") + .Does(() => +{ + CleanDirectories(new DirectoryPath[] { + buildDir, + artifactsDir, + testResultsDir, + nupkgDestDir + }); + + foreach(var path in solutionPaths) + { + Information("Cleaning {0}", path); + CleanDirectories(path + "/**/bin/" + configuration); + CleanDirectories(path + "/**/obj/" + configuration); + } +}); + +Task("__RestoreNugetPackages") + .Does(() => +{ + foreach(var solution in solutions) + { + Information("Restoring NuGet Packages for {0}", solution); + NuGetRestore(solution); + } +}); + +Task("__UpdateAssemblyVersionInformation") + .Does(() => +{ + var gitVersionSettings = new ProcessSettings() + .SetRedirectStandardOutput(true); + + IEnumerable outputLines; + StartProcess(gitVersionPath, gitVersionSettings, out outputLines); + + var output = string.Join("\n", outputLines); + gitVersionOutput = new JsonParser().Parse>(output); + + Information("Updated GlobalAssemblyInfo"); + Information("AssemblyVersion -> {0}", gitVersionOutput["AssemblySemVer"]); + Information("AssemblyFileVersion -> {0}", gitVersionOutput["MajorMinorPatch"]); + Information("AssemblyInformationalVersion -> {0}", gitVersionOutput["InformationalVersion"]); +}); + +Task("__UpdateAppVeyorBuildNumber") + .WithCriteria(() => AppVeyor.IsRunningOnAppVeyor) + .Does(() => +{ + var fullSemVer = gitVersionOutput["FullSemVer"].ToString(); + AppVeyor.UpdateBuildVersion(fullSemVer); +}); + +Task("__BuildSolutions") + .Does(() => +{ + foreach(var solution in solutions) + { + Information("Building {0}", solution); + + MSBuild(solution, settings => + settings + .SetConfiguration(configuration) + .WithProperty("TreatWarningsAsErrors", "true") + .UseToolVersion(MSBuildToolVersion.NET46) + .SetVerbosity(Verbosity.Minimal) + .SetNodeReuse(false)); + } +}); + +Task("__RunTests") + .Does(() => +{ + XUnit2("./src/**/bin/" + configuration + "/*.Specs.dll", new XUnit2Settings { + OutputDirectory = testResultsDir, + XmlReportV1 = true + }); +}); + +Task("__CopyOutputToNugetFolder") + .Does(() => +{ + foreach(var project in projectToNugetFolderMap.Keys) { + var sourceDir = srcDir + Directory(projectName + "." + project) + Directory("bin") + Directory(configuration); + + foreach(var targetFolder in projectToNugetFolderMap[project]) { + var destDir = buildDir + Directory("lib") + Directory(targetFolder); + + Information("Copying {0} -> {1}.", sourceDir, destDir); + CopyDirectory(sourceDir, destDir); + } + } + + CopyFile(nuspecSrcFile, nuspecDestFile); +}); + +Task("__AddDotNetReferencesToNuspecFile") + .Does(() => +{ + // see: https://github.com/onovotny/ReferenceGenerator + var pclProjectName = projectName + ".Pcl"; + var pclDirectory = srcDir + Directory(pclProjectName); + var projectFile = pclDirectory + File(pclProjectName + ".csproj"); + var projectDll = pclDirectory + Directory("bin") + Directory(configuration) + File(projectName + ".dll"); + + var refGenSettings = new ProcessSettings() + .WithArguments(args => args + .AppendQuoted(".NETPortable,Version=v4.5,Profile=Profile259") + .AppendQuoted("dotnet") + .AppendQuoted(nuspecDestFile) + .AppendQuoted(projectFile) + .AppendQuoted(projectDll)); + + StartProcess(refGenPath, refGenSettings); +}); + +Task("__CreateNugetPackage") + .Does(() => +{ + var nugetVersion = gitVersionOutput["NuGetVersion"].ToString(); + var packageName = projectName; + + Information("Building {0}.{1}.nupkg", packageName, nugetVersion); + + var nuGetPackSettings = new NuGetPackSettings { + Id = packageName, + Title = packageName, + Version = nugetVersion, + OutputDirectory = nupkgDestDir + }; + + NuGetPack(nuspecDestFile, nuGetPackSettings); +}); + +Task("__StronglySignAssemblies") + .Does(() => +{ + //see: https://github.com/brutaldev/StrongNameSigner + var strongNameSignerSettings = new ProcessSettings() + .WithArguments(args => args + .Append("-in") + .AppendQuoted(buildDir) + .Append("-k") + .AppendQuoted(snkFile) + .Append("-l") + .AppendQuoted("Changes")); + + StartProcess(strongNameSignerPath, strongNameSignerSettings); +}); + +Task("__CreateSignedNugetPackage") + .Does(() => +{ + var nugetVersion = gitVersionOutput["NuGetVersion"].ToString(); + var packageName = projectName + "-Signed"; + + Information("Building {0}.{1}.nupkg", packageName, nugetVersion); + + var nuGetPackSettings = new NuGetPackSettings { + Id = packageName, + Title = packageName, + Version = nugetVersion, + OutputDirectory = nupkgDestDir + }; + + NuGetPack(nuspecDestFile, nuGetPackSettings); +}); + +////////////////////////////////////////////////////////////////////// +// BUILD TASKS +////////////////////////////////////////////////////////////////////// + +Task("Build") + .IsDependentOn("__Clean") + .IsDependentOn("__RestoreNugetPackages") + .IsDependentOn("__UpdateAssemblyVersionInformation") + .IsDependentOn("__UpdateAppVeyorBuildNumber") + .IsDependentOn("__BuildSolutions") + .IsDependentOn("__RunTests") + .IsDependentOn("__CopyOutputToNugetFolder") + .IsDependentOn("__AddDotNetReferencesToNuspecFile") + .IsDependentOn("__CreateNugetPackage") + .IsDependentOn("__StronglySignAssemblies") + .IsDependentOn("__CreateSignedNugetPackage"); + +/////////////////////////////////////////////////////////////////////////////// +// PRIMARY TARGETS +/////////////////////////////////////////////////////////////////////////////// + +Task("Default") + .IsDependentOn("Build"); + +/////////////////////////////////////////////////////////////////////////////// +// EXECUTION +/////////////////////////////////////////////////////////////////////////////// + +RunTarget(target); + +////////////////////////////////////////////////////////////////////// +// HELPER FUNCTIONS +////////////////////////////////////////////////////////////////////// + +string ToolsExePath(string exeFileName) { + var exePath = System.IO.Directory.GetFiles(@".\Tools", exeFileName, SearchOption.AllDirectories).FirstOrDefault(); + return exePath; +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..a66f0c5 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,132 @@ +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. + +.LINK +http://cakebuild.net +#> + +Param( + [string]$Script = "build.cake", + [string]$Target = "Default", + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [switch]$Verbose +) + +Write-Host "Preparing to run build script..." + +# Should we show verbose messages? +if($Verbose.IsPresent) +{ + $VerbosePreference = "continue" +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + Invoke-WebRequest -Uri http://nuget.org/nuget.exe -OutFile $NUGET_EXE +} + +# Make sure NuGet exists where we expect it. +if (!(Test-Path $NUGET_EXE)) { + Throw "Could not find NuGet.exe" +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) +{ + # Restore tools from NuGet. + Push-Location + Set-Location $TOOLS_DIR + + Write-Verbose -Message "Restoring tools from NuGet..." + + # Restore packages + if (Test-Path $PACKAGES_CONFIG) + { + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion" + Write-Verbose ($NuGetOutput | Out-String) + } + # Install just Cake if missing config + else + { + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install Cake -ExcludeVersion" + Write-Verbose ($NuGetOutput | Out-String) + } + Pop-Location + if ($LASTEXITCODE -ne 0) + { + exit $LASTEXITCODE + } +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "$CAKE_EXE `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental" +exit $LASTEXITCODE