Bridge.exe runs elevated and controls all Bridge commands

Bridge.exe now has an app manifest that allows it to run
as admin (UAC prompting if appropriate).

The Bridge.exe command line options have now been improved
to match  the names in TestProperties and BridgeConfiguration.
And it is possible to give Bridge.exe a .json file to initialize
multiple Bridge options.

The prior certificates used for https tests were replaced with
newer versions, and they are installed and uninstalled by the
Bridge as needed.

All firewall rule and certificate cleanup that used to happen
in other scripts is now handled by the Bridge itself. And those
prior scripts have been removed.

The BridgeController was added and a DELETE request to it will
shutdown the Bridge cleanly.
This commit is contained in:
Ron Cain 2015-08-25 06:22:41 -07:00
Родитель 9cf85ca8a1
Коммит cdeccaaa45
21 изменённых файлов: 686 добавлений и 209 удалений

Просмотреть файл

@ -9,60 +9,48 @@ service(s) on a machine other than the machine running the test
(example: cross-platform testing).
For this we have developed what we call the "Bridge." This Bridge is
a WebAPI application meant to run on a Windows OS using the full
.NET framework. It offers a REST API that the scenario tests can
invoke through normal HTTP requests to launch WCF services on other
machines.
a self-elevating .exe that hosts a WebAPI application capable of
starting WCF services on demand. Currently this exe must be run on a
Windows OS using the full .NET Framework.
The Bridge itself is agnostic of WCF services. Instead, it is aware
of types that implement IResource, and it uses reflection to analyze
a collection of assemblies to discover them. It then exposes each of these
as a "resource" to the Bridge REST API. When a test requests a particular
resource, the Bridge invokes the appropriate IResource. In this case, the
WCF tests provide IResources that know how to create and host WCF services.
When Outerloop tests are run, the Bridge is automatically started at
the beginning of the run and closed at the end.
When the tests start up, they inform the Bridge where to find these
resource assemblies. This folder is known as the "Bridge Resource Folder".
There are several Bridge-specific properties that are meaningful to
both the Bridge and the tests. They can be defined either as Environment
variables or passed into build.cmd as MSBuild properties. They are:
- **BuildHost**: the name of the machine on which the Bridge runs (default localhost)
- **BridgePort**: the port to use to communicate with the Bridge (default 44283)
- **BridgeResourceFolder**: the full path to the folder that contains the Bridge IResource assemblies (default bin\Wcf\Bridge\Resources)
- **BridgeAllowRemote**: indicates whether the Bridge will accept requests from machines other than itself (default: false)
Starting the Bridge Locally
Starting the Bridge manually
---------------------------
By default, the Bridge will self-start locally during OuterLoop tests and accept only
requests from the machine running the tests. It will shutdown when all the OuterLoop
tests have completed.
If you want to run the Bridge on a different machine, you must follow the steps below.
Starting the Bridge on a different machine
------------------------------------------
On the Bridge where the machine will run, do these things:
cd to the root folder of a local WCF Git repository and run this command:
However, the Bridge can be started manually. This is useful if you want to run it on a different machine or attach a debugger to it before the tests run. Executing this CMD from the repository root will start the Bridge locally:
```
startBridge.cmd {options}
startBridge.cmd {options}
```
The options can be any of these:
See below for a description of available options.
-portNumber NNN
-allowRemote true/false
-remoteAddresses comma-separated-list
If you start the Bridge manually this way you must also stop it manually.
This manual start sets an Environment variable 'BridgeKeepRunning' to true,
and this prevents the Bridge from being shutdown when OuterLoop tests are complete.
To shutdown the Bridge manually, type "exit" into the CMD window in which the Bridge is running.
As long as 'BridgeKeepRunning' is set to true, the OuterLoop tests will not stop the Bridge when complete, even if the tests caused the Bridge to start.
The 'portNumber' option allows you to choose a port other than
the default 44283.
The Bridge.exe program starts minimized. If you want to see its
output as it runs, restore its CMD window from the taskbar.
Security options to consider when starting the Bridge
-----------------------------------------------------
Because the Bridge offers the ability to exercise arbitrary
code, it is important that it not be exposed publically.
There are 2 options to 'startBridge' to limit its visibility:
-allowRemote
-remoteAddresses:comma-separated-list
The 'allowRemote' option tells the Bridge it is allowed to accept
requests from other machines. If unspecified, it will accept only
from localhost. It must be set to true if the Bridge is running
requests from other machines. If unspecified, it will accept only requests
from localhost. It must be set explicitly if the Bridge is running
on a machine other than where the tests run.
The 'remoteAddresses' is a comma-separated list of IP addresses,
@ -70,63 +58,133 @@ a range of IP's, or one of the supported predefined terms supported
for the Scope properties page in the Windows Firewall.
See https://technet.microsoft.com/en-us/library/dd759059.aspx .
If left unspecified, but 'allowRemote' is true, the 'remoteAddresses'
will default to "LocalSubnet". The value "*" allows remote
This option is used only if 'allowRemote' has also been specified.
Its default value is "LocalSubnet". This means setting 'allowRemote' will by default allow only requests on the same local subnet. The value "*" allows remote
access from all addresses, but for security reasons is not recommended.
The purpose of these options is to limit the scope applied to the
firewall rules the Bridge automatically generates while it is running.
It does this to open specific ports needed for the WCF test services.
The Bridge deletes these firewall rules when it exits.
After running 'startBridge.cmd', a PowerShell window will start Bridge.exe in elevated mode.
You will see a CMD window indicating what URL the Bridge is using and confirmation
whether it is enabled to receive remote requests. It will remain running until you manually close it or type "exit" in that CMD window.
Running OuterLoop tests when the Bridge is remote
------------------------------------------------
On the machine where you want the tests to run, follow these steps:
Start the OuterLoop tests like this:
* cd to the root folder
* run this CMD
```
build.cmd /p:WithCategories=OuterLoop /p:BridgeHost=bridge-host-name /p:BridgeResourceFolder=shared-folder-Bridge-can-access
On the machine where you want the Bridge to run, run this
command from the repository root:
```
startBridge.cmd -allowRemote
```
Alternatively, you could have set these as environment variables to skip passing them as MSBuild properties:
On the machine where you want the tests to run, follow these steps to execute the OuterLoop tests using the remote Bridge:
```
build.cmd /p:WithCategories=OuterLoop /p:BridgeHost={bridge host name} /p:BridgeResourceFolder={shared folder Bridge can access}
```
Alternatively, you could have set these as environment variables to skip passing them as MSBuild properties:
set BridgeHost=bridge-host-name
set BridgeResourceFolder=shared-folder-Bridge-can-access
[optional] set BridgePort=NNN
Another way to run the Bridge and specify multiple options at the same time is to create a file in json format and specify it using the BridgeConfig optionm like this:
BridgeHost must match the machine name where the Bridge is running.
```
build.cmd /p:WithCategories=OuterLoop /p:BridgeConfig={my config file}
```
BridgeHost and BridgePort must match the machine name where the Bridge is running.
BridgeResourceFolder must be the location of a folder that both the client
and Bridge machines can access. The WCF services built for the tests will
be written to that folder, and the Bridge will be asked to read from it.
BridgePort needs to be set only if you started the Bridge on a different port.
After you started 'build.cmd' you will see a PowerShell window execute elevated,
and it will ping the remotely running Bridge to verify it is available. All OuterLoop tests
will then run against WCF test services running on the Bridge machine.
If you watch the CMD window running on the Bridge machine, you will see it
write to the console the incoming configuration and resource requests.
After all tests have run, the Bridge will continue to run for several minutes.
The timeout interval can be specified on the client test machine by setting
the BridgeMaxIdleTimeSpan to some valid TimeSpan value (either as an environment
variable or on the build.cmd line). Default is "20:00" (twenty minutes). After
no activity for this amount of time, the Bridge will close. Alternatively, you
can type "exit" in the Bridge CMD window to close it manually.
You can control the amount of time the Bridge will remain alive when idle by setting the environment variable BridgeMaxIdleTimeSpan to any legal TimeSpan string (example: 'set BridgeMaxIdleTimeSpan=1.02:03:04' will allow it to run 1 day, 2 hours, 3 minutes, and 4 seconds). This value should be set on the machine where the tests run, because they will configure the Bridge machine when they start.
How the Bridge works
--------------------
The Bridge is a WebAPI application that can run on any Windows
machine using the full .NET framework. The Bridge can launch
WCF services requested by tests running on the same or a different
machine.
This allows the tests to be run in environments (such as NET Native
or CoreCLR) or on other operating systems (such as Linux or the Mac)
that are not able to host their own WCF services.
The Bridge is agnostic of WCF but instead supports the notion of
named "resources". A resource is any type that implements the IResource
interface and resides in the "Bridge Resource Folder" when the Bridge
is configured. The Bridge can be reconfigured on-the-fly without being
stopped and restarted.
Each resource has a name, which is just the simple class name of the
type implementing IResource. In this way, a remote application can
request the Bridge to invoke the Get or Put method of any named resource.
For WCF tests, there are a number of IResources that know how to start
and host specific WCF services. This allows a test running in a .NET
Core enviromnent to say "I need the URL to reach a running instance of
the XYZ WCF service" and have the Bridge automatically start that service and return its URL to the client.
The Bridge offers 3 endpoints:
- http://{host}:{port}/**Bridge**
- http://{host}:{port}/**Config**
- http://{host}:{port}/**Resource**
The 'Bridge' endpoint supports these Http requests:
- GET -- returns the current Bridge configuration as a set of name/value pairs
- DELETE -- Terminates the Bridge process cleanly
The 'Config' endpoint supports these Http requests:
- GET -- returns the current Bridge configuration as a set of name/value pairs
- POST -- reconfigures the Bridge configuration with a new set of name/value pairs
- DELETE -- releases all resources currently used by the Bridge but remains running
The 'Resource' endpoint supports these Http requests:
- GET -- return the result of the IResource.Get for the resource of the given name
- POST -- returns the result of the IResource.Put for the resource of the given name
Bridge.exe
-----------
The Bridge.exe is a self-elevating executable capable of starting and
stopping the Bridge WebAPI application. If started from a non-elevated
process, it will ask for elevation confirmation depending on UAC settings.
Usage is: Bridge.exe [/ping] [/stop] [/stopIfLocal] [/allowRemote] [/remoteAddresses:x,y,z] [/{BridgeProperty}:value]
- **ping** Pings the Bridge to check if it is running
- **stop** Stops the Bridge if it is running
- **stopIfLocal** Stops the Bridge if it is running locally
- **allowRemote** If starting the Bridge, allows access from other than localHost (default is localhost only)
- **remoteAddresses** If starting the Bridge, comma-separated list of addresses firewall rules will accept (default is 'LocalSubnet')
- **BridgeConfig:file** Treat file as json name/value pairs to initialize any or all other options
- **BridgeResourceFolder** The folder containing the Bridge 'resources'
- **BridgeHost** The machine on which the Bridge is running
- **BridgePort** The port on which the Bridge is listening
- **BridgeHttpPort** The port used for Http tests
- **BridgeHttpsPort** The port used for Https tests
- **BridgeTcpPort** The port used for TCP tests
- **BridgeWebSocketPort** The port used for web socket tests
- **BridgeCertificateAuthority** The name of the certificate file to serve as the certificate authorithy
- **BridgeHttpsCertificate** The name of the certificate file to import for Https tests
- **BridgeMaxIdleTimeSpan** The maximum TimeSpan the Bridge can stay idle before shutting down
Any of the options starting with 'Bridge' can also be specified by setting an Environment
variable with that name. If passed explicitly on the command line, the command line value takes precedence over the Environment variable.
Any of these options can also be specified by placing them into a json-formatted file and specifying the 'BridgeConfig' property. An example of such a file might be:
```
{
allowRemote : "",
BridgePort : "44289",
BridgeMaxIdleTimeSpan : "24:00:00"
}
```
Options stated explicitly on the command line take precedence over Environment variables or values read from the BridgeConfig file.
When the Outerloop tests are run, Bridge.exe is started automatically running on localhost. When the Outerloop tests complete 'Bridge /stopIfLocal' is invoked to close the Bridge.
While running, the Bridge will add firewall rules to allow access to specific ports.
It may also load certificates as they are needed by tests. When the Bridge closes,
it removes all those firewall rules or certificates.

Просмотреть файл

@ -18,11 +18,6 @@ if %errorlevel% equ 0 (
set outloop=true
)
if "%outloop%" equ "true" (
start /D %setupFilesFolder% /wait BuildWCFTestService.cmd
)
if not defined VisualStudioVersion (
if defined VS140COMNTOOLS (
call "%VS140COMNTOOLS%\VsDevCmd.bat"
@ -48,7 +43,9 @@ set _buildprefix=echo
set _buildpostfix=^> "%_buildlog%"
if "%outloop%" equ "true" (
start /D %setupFilesFolder% /wait RunElevated.vbs SetupWCFTestService.cmd
pushd %setupFilesFolder%
call SetupWCFTestService.cmd
popd
)
call :build %*
@ -72,7 +69,9 @@ findstr /ir /c:".*Warning(s)" /c:".*Error(s)" /c:"Time Elapsed.*" "%_buildlog%"
echo Build Exit Code = %BUILDERRORLEVEL%
if "%outloop%" equ "true" (
start /D %setupFilesFolder% /wait RunElevated.vbs CleanupWCFTestService.cmd
pushd %setupFilesFolder%
call CleanupWCFTestService.cmd
popd
)
exit /b %BUILDERRORLEVEL%

Просмотреть файл

@ -26,10 +26,6 @@ set _buildlog=%~dp0..\..\..\..\msbuildWCFTestService.log
set _buildprefix=echo
set _buildpostfix=^> "%_buildlog%"
:Clean Up Test Service
pushd %~dp0
start /wait RunElevated.vbs CleanupWCFTestService.cmd
popd
call :build %*
:: Build
@ -52,4 +48,4 @@ echo.
findstr /ir /c:".*Warning(s)" /c:".*Error(s)" /c:"Time Elapsed.*" "%_buildlog%"
echo Build Exit Code = %BUILDERRORLEVEL%
exit %BUILDERRORLEVEL%
exit /b %BUILDERRORLEVEL%

Просмотреть файл

@ -1,13 +1,14 @@
echo off
setlocal
pushd %~dp0
echo BridgeKeepRunning=%BridgeKeepRunning%
if '%BridgeKeepRunning%' neq 'true' (
echo Stopping the Bridge.exe task locally...
Taskkill /IM bridge.exe /F
echo Stopping the Bridge...
pushd %~dp0..\..\..\..\bin\wcf\tools\Bridge
call Bridge.exe -stopIfLocal %*
popd
) else (
echo Bridge is left running because BridgeKeepRunning is true
echo The Bridge was left running because BridgeKeepRunning is true
)
exit /b

Двоичный файл не отображается.

Просмотреть файл

@ -1,11 +0,0 @@
'Capture command line arguments to forward to program we start
strName = WScript.Arguments.Item(0)
cmdArgs = ""
If WScript.Arguments.Count > 1 Then
For i = 1 To WScript.Arguments.Count - 1
cmdArgs = cmdArgs & " " & WScript.Arguments.Item(i)
Next
End If
Set objShell = CreateObject("Shell.Application")
objShell.ShellExecute WScript.Arguments(0), cmdArgs,"", "runas", 1

Просмотреть файл

@ -2,20 +2,14 @@ echo off
setlocal
pushd %~dp0
echo Building the Bridge...
call BuildWCFTestService.cmd
popd
if '%BridgeHost%' neq '' (
set _bridgeHostArg=-hostName %BridgeHost%
)
echo Starting the Bridge with parameters %*
pushd %~dp0..\..\..\..\bin\wcf\tools\Bridge
start /MIN Bridge.exe %*
popd
if '%BridgePort%' neq '' (
set _bridgePortArg=-portNumber %BridgePort%
)
if '%BridgeAllowRemote%' neq '' (
set _bridgeAllowRemoteArg=-allowRemote %BridgeAllowRemote%
)
echo Executing: start powershell -ExecutionPolicy Bypass -File ..\test\Bridge\bin\ensureBridge.ps1 %_bridgeHostArg% %_bridgePortArg% %_bridgeAllowRemoteArg% %*
start powershell -ExecutionPolicy Bypass -File ..\test\Bridge\bin\ensureBridge.ps1 %_bridgeHostArg% %_bridgePortArg% %_bridgeAllowRemoteArg% %*
exit /b

Двоичный файл не отображается.

Просмотреть файл

@ -66,7 +66,7 @@ namespace Bridge
throw new ArgumentNullException("appDomainName");
}
lock (ConfigController.BridgeLock)
lock (ConfigController.ConfigLock)
{
AppDomain appDomain = null;
if (TypeCache.AppDomains.TryGetValue(appDomainName, out appDomain))

Просмотреть файл

@ -2,6 +2,7 @@
<Project ToolsVersion="12.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')" />
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), wcf.targets))\wcf.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), wcf.targets))' != '' " />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
@ -14,7 +15,7 @@
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<OutputPath>..\bin\</OutputPath>
<OutputPath>$(WcfToolsOutputPath)\Bridge\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@ -34,6 +35,10 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup />
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Owin, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>$(PackageOutputDir)\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll</HintPath>
@ -91,6 +96,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="AppDomainManager.cs" />
<Compile Include="BridgeController.cs" />
<Compile Include="BridgeState.cs" />
<Compile Include="ChangedEventArgs.cs" />
<Compile Include="ConfigController.cs" />
<Compile Include="IdleTimeoutManager.cs" />
@ -104,6 +111,7 @@
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="app.manifest" />
<None Include="ensureBridge.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -117,4 +125,7 @@
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
<Target Name="BeforeBuild">
<Message Text="$$$ OutputPath is $(OutputPath)" />
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
using WcfTestBridgeCommon;
namespace Bridge
{
public class BridgeController : ApiController
{
private static object BridgeLock { get; set; }
public static BridgeState BridgeState { get; private set; }
static BridgeController()
{
BridgeLock = new object();
BridgeState = BridgeState.Running;
}
public HttpResponseMessage Get(HttpRequestMessage request)
{
Dictionary<string, string> dictionary = ConfigController.BridgeConfiguration.ToDictionary();
string configResponse = JsonSerializer.SerializeDictionary(dictionary);
Trace.WriteLine(String.Format("{0:T} - GET bridge returning raw content:{1}{2}",
DateTime.Now, Environment.NewLine, configResponse),
typeof(BridgeController).Name);
// Bridge GET response is the current Bridge configuration
HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(configResponse);
response.Content.Headers.ContentType = new MediaTypeHeaderValue(JsonSerializer.JsonMediaType);
return response;
}
// The DELETE Http verb means stop the Bridge cleanly
public HttpResponseMessage Delete(HttpRequestMessage request)
{
Trace.WriteLine(String.Format("{0:T} - received DELETE request", DateTime.Now),
typeof(BridgeController).Name);
lock(BridgeLock)
{
if (BridgeState == BridgeState.Running)
{
// 'Stopping' is the Bridge's terminal state because
// the process itself will terminate during this response.
BridgeState = BridgeState.Stopping;
try
{
HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new ExitOnDisposeStringContent("\"The Bridge has closed.\"");
response.Content.Headers.ContentType = new MediaTypeHeaderValue(JsonSerializer.JsonMediaType);
return response;
}
catch (Exception ex)
{
var exceptionResponse = ex.Message;
Trace.WriteLine(String.Format("{0:T} - DELETE config exception:{1}{2}",
DateTime.Now, Environment.NewLine, ex),
typeof(BridgeController).Name);
return request.CreateResponse(HttpStatusCode.BadRequest, exceptionResponse);
}
}
else
{
// Multiple concurrent DELETE requests are blocked by the monitor.
// But in case the process has not yet terminated from the first request,
// send back BADREQUEST for any others.
return request.CreateResponse(HttpStatusCode.BadRequest, "Bridge is already stopping.");
}
}
}
public static void ReleaseAllResources()
{
CertificateManager.UninstallAllCertificates();
PortManager.RemoveAllBridgeFirewallRules();
}
public static void StopBridgeProcess(int exitCode)
{
ReleaseAllResources();
Environment.Exit(exitCode);
}
// This class exists to release all Bridge resources
// in this class's Dispose(). WebAPI guarantees the
// HttpResponseMessage and its content will be disposed
// only after the response has been sent, allowing the
// Bridge to provide a valid 200 response for the DELETE
// and then immediately terminate the process.
class ExitOnDisposeStringContent : StringContent
{
public ExitOnDisposeStringContent(string content) : base(content)
{
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
StopBridgeProcess(0);
}
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bridge
{
public enum BridgeState
{
Running,
Stopping
}
}

Просмотреть файл

@ -27,7 +27,7 @@ namespace Bridge
static ConfigController()
{
BridgeLock = new object();
ConfigLock = new object();
// Register to manage AppDomains in response to changes to the resource folder
ResourceFolderChanged += (object s, ChangedEventArgs<string> args) =>
@ -39,7 +39,7 @@ namespace Bridge
// We lock the Bridge when necessary to prevent configuration
// changes or resource instantiation concurrent execution.
public static object BridgeLock { get; private set; }
internal static object ConfigLock { get; private set; }
public static BridgeConfiguration BridgeConfiguration
{
@ -55,7 +55,7 @@ namespace Bridge
public HttpResponseMessage POST(HttpRequestMessage request)
{
// A configuration change can have wide impact, so we don't allow concurrent use
lock(BridgeLock)
lock(ConfigLock)
{
try
{
@ -106,7 +106,7 @@ namespace Bridge
typeof(ConfigController).Name);
// Directly return a json string to avoid use of MediaTypeFormatters
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(configResponse);
response.Content.Headers.ContentType = new MediaTypeHeaderValue(JsonSerializer.JsonMediaType);
return response;
@ -118,7 +118,7 @@ namespace Bridge
DateTime.Now, Environment.NewLine, ex),
typeof(ConfigController).Name);
return Request.CreateResponse(HttpStatusCode.BadRequest, exceptionResponse);
return request.CreateResponse(HttpStatusCode.BadRequest, exceptionResponse);
}
}
}
@ -134,7 +134,7 @@ namespace Bridge
typeof(ConfigController).Name);
// Directly return a json string to avoid use of MediaTypeFormatters
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(configResponse);
response.Content.Headers.ContentType = new MediaTypeHeaderValue(JsonSerializer.JsonMediaType);
return response;
@ -148,7 +148,7 @@ namespace Bridge
typeof(ConfigController).Name);
// A configuration change can have wide impact, so we don't allow concurrent use
lock (ConfigController.BridgeLock)
lock (ConfigController.ConfigLock)
{
try {
if (!String.IsNullOrEmpty(CurrentAppDomainName))
@ -161,7 +161,7 @@ namespace Bridge
ResourceFolderChanged(this, new ChangedEventArgs<string>(oldResourceFolder, null));
}
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
HttpResponseMessage response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent("\"Bridge configuration has been cleared.\"");
response.Content.Headers.ContentType = new MediaTypeHeaderValue(JsonSerializer.JsonMediaType);
return response;
@ -173,7 +173,7 @@ namespace Bridge
DateTime.Now, Environment.NewLine, ex),
typeof(ConfigController).Name);
return Request.CreateResponse(HttpStatusCode.BadRequest, exceptionResponse);
return request.CreateResponse(HttpStatusCode.BadRequest, exceptionResponse);
}
}
}

Просмотреть файл

@ -27,7 +27,7 @@ namespace Bridge
internal static Dictionary<string, string> DeserializeDictionary(string data)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
data = data.Replace("{", String.Empty)
.Replace("}", String.Empty)
.Trim();

Просмотреть файл

@ -2,8 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using WcfTestBridgeCommon;
namespace Bridge
@ -18,10 +21,186 @@ namespace Bridge
{
CommandLineArguments commandLineArgs = new CommandLineArguments(args);
Console.WriteLine("Specified BridgeConfiguration is:{0}{1}",
Environment.NewLine, commandLineArgs.BridgeConfiguration.ToString());
// If asked to ping (not the default), just ping and return an exit code indicating its state
if (commandLineArgs.Ping)
{
string errorMessage = null;
if (PingBridge(commandLineArgs.BridgeConfiguration.BridgeHost,
commandLineArgs.BridgeConfiguration.BridgePort,
out errorMessage))
{
Console.WriteLine("The Bridge is running.");
Environment.Exit(0);
}
else
{
Console.WriteLine("The Bridge is not running: {0}", errorMessage);
Environment.Exit(1);
}
}
else if (commandLineArgs.StopIfLocal)
{
StopBridgeIfLocal(commandLineArgs);
}
else if (commandLineArgs.Stop)
{
StopBridge(commandLineArgs);
}
else
{
// Default action is starting the Bridge
StartBridge(commandLineArgs);
}
}
// Issues a GET request to the Bridge to determine whether it is alive.
// A return of 'true' means the Bridge is healthy. A return of 'false'
// indicates the Bridge is not healthy, and 'errorMessage' describes the problem.
private static bool PingBridge(string host, int port, out string errorMessage)
{
errorMessage = null;
string bridgeUrl = String.Format("http://{0}:{1}/Bridge", host, port);
using (HttpClient httpClient = new HttpClient())
{
Console.WriteLine("Testing Bridge at {0}", bridgeUrl);
try
{
var response = httpClient.GetAsync(bridgeUrl).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
errorMessage = String.Format("{0}Bridge returned unexpected status code='{1}', reason='{2}'",
Environment.NewLine, response.StatusCode, response.ReasonPhrase);
if (response.Content != null)
{
string contentAsString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (contentAsString.Length > 1000)
{
contentAsString = contentAsString.Substring(0, 999) + "...";
}
errorMessage = String.Format("{0}, content:{1}{2}",
errorMessage, Environment.NewLine, contentAsString);
}
return false;
}
}
catch (Exception ex)
{
errorMessage = ex.Message;
return false;
}
}
return true;
}
private static void StopBridgeIfLocal(CommandLineArguments commandLineArgs)
{
if (IsBridgeHostLocal(commandLineArgs.BridgeConfiguration))
{
StopBridge(commandLineArgs);
}
else
{
Console.WriteLine("The Bridge on host {0} is not running locally and will not be stopped.",
commandLineArgs.BridgeConfiguration.BridgeHost);
Console.WriteLine("Use 'Bridge.exe /stop' to stop a Bridge on another machine.");
}
}
private static void StopBridge(CommandLineArguments commandLineArgs)
{
string errorMessage = null;
if (!PingBridge(commandLineArgs.BridgeConfiguration.BridgeHost,
commandLineArgs.BridgeConfiguration.BridgePort,
out errorMessage))
{
Console.WriteLine("The Bridge is not running: {0}", errorMessage);
Environment.Exit(0);
}
string bridgeUrl = String.Format("http://{0}:{1}/Bridge", commandLineArgs.BridgeConfiguration.BridgeHost, commandLineArgs.BridgeConfiguration.BridgePort);
string problem = null;
// We stop the Bridge using a DELETE request.
// If the Bridge is running on localhost, it will be running
// in a different process on this machine.
using (HttpClient httpClient = new HttpClient())
{
Console.WriteLine("Stopping Bridge at {0}", bridgeUrl);
try
{
var response = httpClient.DeleteAsync(bridgeUrl).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
problem = String.Format("{0}Bridge returned unexpected status code='{1}', reason='{2}'",
Environment.NewLine, response.StatusCode, response.ReasonPhrase);
if (response.Content != null)
{
string contentAsString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (contentAsString.Length > 1000)
{
contentAsString = contentAsString.Substring(0, 999) + "...";
}
problem = String.Format("{0}, content:{1}{2}",
problem, Environment.NewLine, contentAsString);
}
}
}
catch (Exception ex)
{
problem = ex.ToString();
}
}
if (problem != null)
{
Console.WriteLine("A problem was encountered stopping the Bridge:{0}{1}",
Environment.NewLine, problem);
Console.WriteLine("Forcing local resource cleanup...");
BridgeController.StopBridgeProcess(1);
}
// A successfull DELETE will have cleaned up all firewall rules,
// certificates, etc. So when using localhost, this cleanup will
// be redundant and harmless. When the Bridge is running remotely,
// this cleanup will remove all firewall rules and certificates we
// installed on the current machine to talk with that Bridge.
BridgeController.StopBridgeProcess(0);
}
// Starts the Bridge locally if it is not already running.
private static void StartBridge(CommandLineArguments commandLineArgs)
{
string errorMessage = null;
if (PingBridge(commandLineArgs.BridgeConfiguration.BridgeHost,
commandLineArgs.BridgeConfiguration.BridgePort,
out errorMessage))
{
Console.WriteLine("The Bridge is already running.");
Environment.Exit(0);
}
// The host is not local so we cannot start the Bridge
if (!IsBridgeHostLocal(commandLineArgs.BridgeConfiguration))
{
Console.WriteLine("The Bridge cannot be started from this machine on {0}",
commandLineArgs.BridgeConfiguration.BridgeHost);
Environment.Exit(1);
}
int port = commandLineArgs.BridgeConfiguration.BridgePort;
string hostFormatString = "http://{0}:{1}";
string owinAddress = String.Format(hostFormatString, commandLineArgs.AllowRemote ? "+" : "localhost", commandLineArgs.Port);
string owinAddress = String.Format(hostFormatString, commandLineArgs.AllowRemote ? "+" : "localhost", port);
string visibleHost = (commandLineArgs.AllowRemote) ? Environment.MachineName : "localhost";
string visibleAddress = String.Format(hostFormatString, visibleHost, commandLineArgs.Port);
string visibleAddress = String.Format(hostFormatString, visibleHost, port);
// Configure the remote addresses the firewall rules will accept.
// If remote access is not allowed, specifically disallow remote addresses
@ -29,25 +208,31 @@ namespace Bridge
// Initialize the BridgeConfiguration from command line.
// The first POST to the ConfigController will supply the rest.
ConfigController.BridgeConfiguration = commandLineArgs.BridgeConfiguration;
ConfigController.BridgeConfiguration.BridgeHost = visibleHost;
ConfigController.BridgeConfiguration.BridgePort = commandLineArgs.Port;
// Remove any pre-existing firewall rules the Bridge may have added
// in past runs. We normally cleanup on exit but could have been
// aborted.
PortManager.RemoveAllBridgeFirewallRules();
// Remove any pre-existing firewall rules or certificates the Bridge
// may have added in past runs. We normally clean them up on exit but
// it is possible a prior Bridge process was terminated prematurely.
BridgeController.ReleaseAllResources();
// Open the port used to communicate with the Bridge itself
PortManager.OpenPortInFirewall(commandLineArgs.Port);
PortManager.OpenPortInFirewall(port);
Console.WriteLine("Starting the Bridge at {0}", visibleAddress);
OwinSelfhostStartup.Startup(owinAddress);
Test(visibleHost, commandLineArgs.Port);
// Now test whether the Bridge is running. Failure cleans up
// all resources and terminates the process.
if (!PingBridge(visibleHost, port, out errorMessage))
{
Console.WriteLine("The Bridge failed to start or is not responding: {0}", errorMessage);
BridgeController.StopBridgeProcess(1);
}
while (true)
{
Console.WriteLine("The Bridge is listening at {0}", visibleAddress);
Console.WriteLine("The Bridge is running and listening at {0}", visibleAddress);
if (commandLineArgs.AllowRemote)
{
Console.WriteLine("Remote access is allowed from '{0}'", commandLineArgs.RemoteAddresses);
@ -57,8 +242,6 @@ namespace Bridge
Console.WriteLine("Remote access is disabled.");
}
Console.WriteLine("Current configuration is:{0}{1}", Environment.NewLine, ConfigController.BridgeConfiguration.ToString());
Console.WriteLine("Type \"exit\" to stop the Bridge.");
string answer = Console.ReadLine();
if (String.Equals(answer, "exit", StringComparison.OrdinalIgnoreCase))
@ -67,36 +250,33 @@ namespace Bridge
}
}
Environment.Exit(0);
BridgeController.StopBridgeProcess(0);
}
[Conditional("DEBUG")]
private static void Test(string hostName, int portNumber)
// Returns 'true' if the BridgeConfiguration describes a Bridge that
// would run locally.
private static bool IsBridgeHostLocal(BridgeConfiguration configuration)
{
Console.WriteLine("Self-testing the Bridge on http://{0}:{1} ...", hostName, portNumber);
string executionFolder = Path.GetDirectoryName(typeof(Program).Assembly.Location);
string ensureBridgePath = Path.Combine(executionFolder, "ensureBridge.ps1");
string commandLine = String.Format("-ExecutionPolicy Bypass -File {0} -portNumber {1} -hostName {2}",
ensureBridgePath, portNumber, hostName);
ProcessStartInfo procStartInfo = new ProcessStartInfo("powershell.exe", commandLine);
procStartInfo.RedirectStandardOutput = true;
procStartInfo.UseShellExecute = false;
procStartInfo.CreateNoWindow = true;
var proc = new Process();
proc.StartInfo = procStartInfo;
proc.Start();
proc.WaitForExit();
string result = proc.StandardOutput.ReadToEnd();
Console.WriteLine("Result from Test: " + result);
if (String.Equals("localhost", configuration.BridgeHost, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (String.Equals(Environment.MachineName, configuration.BridgeHost, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
class CommandLineArguments
{
public CommandLineArguments(string[] args)
{
Port = DefaultPortNumber;
AllowRemote = DefaultAllowRemote;
RemoteAddresses = DefaultRemoteAddresses;
Ping = false;
bool success = Parse(args);
if (!success)
@ -106,52 +286,138 @@ namespace Bridge
}
}
public int Port { get; private set; }
public BridgeConfiguration BridgeConfiguration { get; private set; }
public bool AllowRemote { get; private set; }
public string RemoteAddresses { get; private set; }
public bool Ping { get; private set; }
public bool Stop { get; private set; }
public bool StopIfLocal { get; private set; }
private bool Parse(string[] args)
{
// Build a dictionary of all command line arguments.
// This allows us to initialize BridgeConfiguration from it.
// Precedence of values in the BridgeConfiguration is this:
// - Lowest precedence is the BridgeConfiguration ctor defaults
// - Next precedence is any value found in a specified configuration file
// - Next precedence is environment variables
// - Highest precedence is a BridgeConfiguration value explicitly set on the command line
Dictionary<string, string> argumentDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (string arg in args)
{
if (!arg.StartsWith("/") && !arg.StartsWith("-"))
{
return false;
}
string[] argAndValue = arg.Substring(1).Split(':');
if (argAndValue.Length == 0)
// Cannot use split because some argument values could contain colons
int index = arg.IndexOf(':');
string argName = (index < 0) ? arg.Substring(1) : arg.Substring(1, index - 1);
string argValue = (index < 0) ? String.Empty : arg.Substring(index+1);
if (String.Equals(argName, "?", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (String.Equals(argAndValue[0], "port", StringComparison.OrdinalIgnoreCase))
{
if (argAndValue.Length < 2)
{
return false;
}
int port = 0;
if (!int.TryParse(argAndValue[1], out port))
{
return false;
}
Port = port;
}
else if (String.Equals(argAndValue[0], "allowRemote", StringComparison.OrdinalIgnoreCase))
{
AllowRemote = true;
}
else if (String.Equals(argAndValue[0], "remoteAddresses", StringComparison.OrdinalIgnoreCase))
{
if (argAndValue.Length < 2)
{
return false;
}
RemoteAddresses = argAndValue[1];
}
else
argumentDictionary[argName] = argValue;
}
BridgeConfiguration = new BridgeConfiguration();
BridgeConfiguration.BridgePort = DefaultPortNumber;
BridgeConfiguration.BridgeHost = "localhost";
BridgeConfiguration.BridgeMaxIdleTimeSpan = IdleTimeoutHandler.Default_MaxIdleTimeSpan;
// If the user specified a configuration file, deserialize it as json
// and treat each name-value pair as if it had been on the command line.
// But options explicitly on the command line take precedence over these file options.
string argumentValue;
if (argumentDictionary.TryGetValue("bridgeConfig", out argumentValue))
{
if (!File.Exists(argumentValue))
{
Console.WriteLine("The configuration file '{0}' does not exist.");
return false;
}
// Read the configuration file as json and deserialize it
string configurationAsJson = File.ReadAllText(argumentValue);
Dictionary<string, string> deserializedConfig = null;
try
{
deserializedConfig = JsonSerializer.DeserializeDictionary(configurationAsJson);
}
catch (Exception ex)
{
// Catch all exceptions because any will cause
// this application to terminate.
Console.WriteLine("Error deserializing {0} : {1}",
argumentValue, ex.Message);
return false;
}
// Every name/value pair in the config file not explicitly set on the command line
// is treated as if it had been on the command line.
foreach (var pair in deserializedConfig)
{
if (!argumentDictionary.ContainsKey(pair.Key))
{
argumentDictionary[pair.Key] = pair.Value;
}
}
}
// For every property in the BridgeConfiguration that has not been explicitly
// specified on the command line or via the config file, check if there is an
// Environment variable set for it. If so, use it as if it had been on the command line.
foreach (string key in BridgeConfiguration.ToDictionary().Keys)
{
// If the property is explicitly on the command line, it has highest precedence
if (!argumentDictionary.ContainsKey(key))
{
// But if it is not explicitly on the command line but
// an environment variable exists for it, it has higher precedence
// than defaults or the config file.
string environmentVariable = Environment.GetEnvironmentVariable(key);
if (!String.IsNullOrWhiteSpace(environmentVariable))
{
argumentDictionary[key] = environmentVariable;
}
}
}
// Finally, apply all our command line arguments to the BridgeConfiguration,
// overwriting any values that were the default or came from the optional config file
BridgeConfiguration = new BridgeConfiguration(BridgeConfiguration, argumentDictionary);
// Finish parsing the command line arguments that are not part of BridgeConfiguration
if (argumentDictionary.ContainsKey("allowRemote"))
{
AllowRemote = true;
}
if (argumentDictionary.ContainsKey("ping"))
{
Ping = true;
}
if (argumentDictionary.ContainsKey("stop"))
{
Stop = true;
}
if (argumentDictionary.ContainsKey("stopiflocal"))
{
StopIfLocal = true;
}
string remoteAddresses;
if (argumentDictionary.TryGetValue("remoteAddresses", out remoteAddresses))
{
RemoteAddresses = remoteAddresses;
}
return true;
@ -159,11 +425,18 @@ namespace Bridge
private void ShowUsage()
{
Console.WriteLine("Starts a new instance of the Bridge. Usage is:");
Console.WriteLine("Bridge.exe [/port:nnn] [/allowRemote] [/remoteAddresses:x,y,z");
Console.WriteLine(" /port:nnn Listening port for the bridge (default is {0}", DefaultPortNumber);
Console.WriteLine(" /allowRemote If specified, allows access from other than localHost (default is localhost only)");
Console.WriteLine(" /remoteAddresses Comma-separated list of addresses firewall rules will accept (default is 'LocalSubnet')");
Console.WriteLine("Usage is: Bridge.exe [/ping] [/stop] [/stopIfLocal] [/allowRemote] [/remoteAddresses:x,y,z] [/{BridgeProperty}:value");
Console.WriteLine(" /ping Pings the Bridge to check if it is running");
Console.WriteLine(" /stop Stops the Bridge if it is running");
Console.WriteLine(" /stopIfLocal Stops the Bridge if it is running locally");
Console.WriteLine(" /allowRemote If starting the Bridge, allows access from other than localHost (default is localhost only)");
Console.WriteLine(" /remoteAddresses If starting the Bridge, comma-separated list of addresses firewall rules will accept (default is 'LocalSubnet')");
Console.WriteLine(" /BridgeConfig:file Treat file as json name/value pairs to initialize any or all other options");
string bridgePropertyList = String.Join(Environment.NewLine + " /", new BridgeConfiguration().ToDictionary().Keys);
Console.WriteLine(" /{0}", bridgePropertyList);
Console.WriteLine();
Console.WriteLine("If no other action is specified, the Bridge will be started unless it is already running.");
}
}
}

Просмотреть файл

@ -67,7 +67,7 @@ namespace Bridge
Trace.WriteLine(String.Format("{0:T} - Exception executing PUT for resource {1}{2}:{3}",
DateTime.Now, resourceName, Environment.NewLine, exception.ToString()),
this.GetType().Name);
return Request.CreateResponse(HttpStatusCode.InternalServerError, new resourceResponse
return request.CreateResponse(HttpStatusCode.InternalServerError, new resourceResponse
{
id = correlationId,
details = exception.Message

Просмотреть файл

@ -17,7 +17,7 @@ namespace Bridge
}
// Disallow concurrent resource instantation or configuration changes
lock (ConfigController.BridgeLock)
lock (ConfigController.ConfigLock)
{
AppDomain appDomain;
if (String.IsNullOrWhiteSpace(ConfigController.CurrentAppDomainName))
@ -52,7 +52,7 @@ namespace Bridge
}
// Disallow concurrent resource instantation or configuration changes
lock (ConfigController.BridgeLock)
lock (ConfigController.ConfigLock)
{
AppDomain appDomain;
if (!TypeCache.AppDomains.TryGetValue(ConfigController.CurrentAppDomainName, out appDomain))

Просмотреть файл

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<asmv1:assembly manifestVersion="1.0"
xmlns="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv1="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv2="urn:schemas-microsoft-com:asm.v2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<assemblyIdentity version="1.0.0.0" name="Bridge.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel
level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</asmv1:assembly>

Просмотреть файл

@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace WcfTestBridgeCommon
{
public class CertificateManager
public static class CertificateManager
{
private static object s_certificateLock = new object();
private static bool s_registeredForProcessExit = false;

Просмотреть файл

@ -5,6 +5,7 @@ using NetFwTypeLib;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@ -12,7 +13,7 @@ namespace WcfTestBridgeCommon
{
// This class exists to create and delete firewall rules to
// manage which ports are open on behalf of the Bridge.
public class PortManager
public static class PortManager
{
// This prefix is used both to name rules and to discover existing
// rules created by this class, so it must be unique
@ -157,7 +158,18 @@ namespace WcfTestBridgeCommon
foreach (string ruleName in ruleSet)
{
NetFwPolicy2.Rules.Remove(ruleName);
try {
NetFwPolicy2.Rules.Remove(ruleName);
Console.WriteLine("Removed firewall rule '{0}'", ruleName);
}
catch (FileNotFoundException fnfe)
{
// This exception can happen when multiple processes
// are cleaning up the rules, and the rule has already
// been removed.
Console.WriteLine("Unable to remove rule '{0}' : {1}",
ruleName, fnfe.Message);
}
}
}
}

Просмотреть файл

@ -1,14 +1,13 @@
@echo off
pushd %~dp0src\System.Private.ServiceModel\tools\setupfiles
echo Building the Bridge...
call start /wait BuildWCFTestService.cmd
echo Launching the Bridge (elevated) ...
start /wait RunElevated.vbs SetupWCFTestService.cmd %*
set BridgeKeepRunning=true
pushd %~dp0src\System.Private.ServiceModel\tools\setupfiles
call SetupWCFTestService.cmd %*
popd
echo Because you started the Bridge manually, it will remain running until you close it manually.
echo Set the BridgeKeepRunning environment variable to 'false' to allow it to be closed by OuterLoop tests.
:done
popd
popd