Merge branch 'release/2.2'

This commit is contained in:
BrennanConroy 2018-09-12 09:29:51 -07:00
commit b423313e5c
56 changed files with 856 additions and 9343 deletions

4
.vscode/launch.json vendored
View File

@ -21,7 +21,7 @@
"type": "node",
"request": "launch",
"name": "Jest - All",
"program": "${workspaceFolder}/clients/ts/node_modules/jest/bin/jest",
"program": "${workspaceFolder}/clients/ts/common/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/clients/ts",
"args": ["--runInBand"],
"console": "integratedTerminal",
@ -31,7 +31,7 @@
"type": "node",
"request": "launch",
"name": "Jest - Current File",
"program": "${workspaceFolder}/clients/ts/node_modules/jest/bin/jest",
"program": "${workspaceFolder}/clients/ts/common/node_modules/jest/bin/jest",
"cwd": "${workspaceFolder}/clients/ts",
"args": ["${relativeFile}"],
"console": "integratedTerminal",

View File

@ -8,7 +8,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class CallbackMap {
class CallbackMap {
private ConcurrentHashMap<String, List<ActionBase>> handlers = new ConcurrentHashMap<>();
public void put(String target, ActionBase action) {

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class CloseMessage extends HubMessage {
class CloseMessage extends HubMessage {
String error;
@Override

View File

@ -5,7 +5,7 @@ package com.microsoft.aspnet.signalr;
import com.google.gson.Gson;
public class HandshakeProtocol {
class HandshakeProtocol {
public static Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class HandshakeRequestMessage {
class HandshakeRequestMessage {
String protocol;
int version;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class HandshakeResponseMessage {
class HandshakeResponseMessage {
public String error;
public HandshakeResponseMessage() {

View File

@ -6,6 +6,6 @@ package com.microsoft.aspnet.signalr;
/**
* A base class for hub messages.
*/
public abstract class HubMessage {
abstract class HubMessage {
public abstract HubMessageType getMessageType();
}

View File

@ -6,7 +6,7 @@ package com.microsoft.aspnet.signalr;
/**
* A protocol abstraction for communicating with SignalR hubs.
*/
public interface HubProtocol {
interface HubProtocol {
String getName();
int getVersion();
TransferFormat getTransferFormat();

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class InvocationMessage extends HubMessage {
class InvocationMessage extends HubMessage {
int type = HubMessageType.INVOCATION.value;
String invocationId;
String target;

View File

@ -11,7 +11,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class JsonHubProtocol implements HubProtocol {
class JsonHubProtocol implements HubProtocol {
private final JsonParser jsonParser = new JsonParser();
private final Gson gson = new Gson();
private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -10,7 +10,7 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Negotiate {
class Negotiate {
public static NegotiateResponse processNegotiate(String url) throws IOException {
return processNegotiate(url, null);

View File

@ -10,7 +10,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class NegotiateResponse {
class NegotiateResponse {
private String connectionId;
private Set<String> availableTransports = new HashSet<>();
private String redirectUrl;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class NullLogger implements Logger {
class NullLogger implements Logger {
@Override
public void log(LogLevel logLevel, String message) { }

View File

@ -3,6 +3,6 @@
package com.microsoft.aspnet.signalr;
public interface OnReceiveCallBack {
interface OnReceiveCallBack {
void invoke(String message) throws Exception;
}

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public class PingMessage extends HubMessage {
class PingMessage extends HubMessage {
int type = HubMessageType.PING.value;

View File

@ -3,7 +3,7 @@
package com.microsoft.aspnet.signalr;
public interface Transport {
interface Transport {
void start() throws Exception;
void send(String message) throws Exception;
void setOnReceive(OnReceiveCallBack callback);

View File

@ -10,7 +10,7 @@ import java.util.Map;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
public class WebSocketTransport implements Transport {
class WebSocketTransport implements Transport {
private WebSocketClient webSocketClient;
private OnReceiveCallBack onReceiveCallBack;
private URI url;

View File

@ -1,16 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*;
import org.junit.Test;
import com.microsoft.aspnet.signalr.HandshakeProtocol;
import com.microsoft.aspnet.signalr.HandshakeRequestMessage;
import com.microsoft.aspnet.signalr.HandshakeResponseMessage;
public class HandshakeProtocolTest {
@Test

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*;
@ -12,7 +12,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import com.microsoft.aspnet.signalr.*;
public class HubConnectionTest {
private static final String RECORD_SEPARATOR = "\u001e";

View File

@ -1,14 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.microsoft.aspnet.signalr.HubException;
public class HubExceptionTest {
@Test
public void VeryHubExceptionMesssageIsSet() {

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*;
@ -10,7 +10,6 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import com.google.gson.JsonArray;
import com.microsoft.aspnet.signalr.*;
public class JsonHubProtocolTest {
private JsonHubProtocol jsonHubProtocol = new JsonHubProtocol();

View File

@ -1,13 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*;
import org.junit.Test;
import com.microsoft.aspnet.signalr.NegotiateResponse;
public class NegotiateResponseTest {

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.assertEquals;
@ -12,7 +12,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import com.microsoft.aspnet.signalr.Negotiate;
@RunWith(Parameterized.class)
public class ResolveNegotiateUrlTest {

View File

@ -1,16 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import com.microsoft.aspnet.signalr.NullLogger;
import com.microsoft.aspnet.signalr.Transport;
import com.microsoft.aspnet.signalr.WebSocketTransport;
public class WebSocketTransportTest {
@Rule

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
package com.microsoft.aspnet.signalr.test;
package com.microsoft.aspnet.signalr;
import static org.junit.Assert.*;
@ -13,8 +13,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import com.microsoft.aspnet.signalr.NullLogger;
import com.microsoft.aspnet.signalr.WebSocketTransport;
@RunWith(Parameterized.class)
public class WebSocketTransportUrlFormatTest {

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
module.exports = {
transformIgnorePatterns: [
// We reference the ESM output from tests and don't want to run them through jest as it won't understand the syntax
".*/node_modules/(?!@aspnet)"
],
globals: {
"ts-jest": {
"tsConfigFile": "../tsconfig.jest.json",
"skipBabel": true,
// Needed in order to properly process the JS files
// We run 'tsc --noEmit' to get TS diagnostics before the test instead
"enableTsDiagnostics": false,
}
},
reporters: [
"default",
["../common/node_modules/jest-junit/index.js", { "output": "../../../artifacts/logs/" + `${process.platform}` + ".node.functional.junit.xml" }]
],
transform: {
"^.+\\.(jsx?|tsx?)$": "../common/node_modules/ts-jest"
},
testEnvironment: "node",
testRegex: "(Tests)\\.(jsx?|tsx?)$",
moduleNameMapper: {
"^ts-jest$": "<rootDir>/../common/node_modules/ts-jest",
"^@aspnet/signalr$": "<rootDir>/../signalr/dist/cjs/index.js"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
};

View File

@ -6,14 +6,88 @@
"dependencies": {
"@aspnet/signalr": {
"version": "file:../signalr",
"requires": {
"eventsource": "^1.0.7",
"websocket": "^1.0.26"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"bundled": true,
"requires": {
"ms": "2.0.0"
}
},
"es6-promise": {
"version": "4.2.2",
"bundled": true
},
"eventsource": {
"version": "1.0.7",
"bundled": true,
"requires": {
"original": "^1.0.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"bundled": true
},
"ms": {
"version": "2.0.0",
"bundled": true
},
"nan": {
"version": "2.11.0",
"bundled": true
},
"original": {
"version": "1.0.2",
"bundled": true,
"requires": {
"url-parse": "^1.4.3"
}
},
"querystringify": {
"version": "2.0.0",
"bundled": true
},
"requires-port": {
"version": "1.0.0",
"bundled": true
},
"tslib": {
"version": "1.9.3",
"bundled": true
},
"typedarray-to-buffer": {
"version": "3.1.5",
"bundled": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"url-parse": {
"version": "1.4.3",
"bundled": true,
"requires": {
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"websocket": {
"version": "1.0.26",
"bundled": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
}
},
"yaeti": {
"version": "0.0.6",
"bundled": true
}
}
},
@ -1449,12 +1523,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1469,17 +1545,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -1596,7 +1675,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -1608,6 +1688,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -1622,6 +1703,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -1629,12 +1711,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -1653,6 +1737,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -1733,7 +1818,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -1745,6 +1831,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -1866,6 +1953,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -2711,8 +2799,7 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true,
"optional": true
"dev": true
},
"nanomatch": {
"version": "1.2.13",
@ -3623,6 +3710,15 @@
"mime-types": "~2.1.18"
}
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz",
@ -3922,6 +4018,29 @@
}
}
},
"websocket": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
"integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==",
"dev": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -3972,6 +4091,12 @@
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
"dev": true
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",

View File

@ -31,7 +31,8 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-summary-reporter": "^1.5.0",
"ts-node": "^4.1.0",
"typescript": "^3.0.1"
"typescript": "^3.0.1",
"websocket": " ^1.0.26"
},
"scripts": {
"clean": "node ../common/node_modules/rimraf/bin.js ./wwwroot/dist ./obj/js",
@ -40,7 +41,7 @@
"build:webpack": "node ../common/node_modules/webpack-cli/bin/cli.js",
"build:parent": "cd .. && npm run build",
"pretest": "npm run build:parent && npm run build && dotnet build --no-restore",
"test": "npm run test:local --",
"test": "tsc --noEmit && npm run test:local --",
"test:inner": "npm run build && dotnet build && npm run test:local --",
"test:local": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts",
"test:all": "ts-node --project ./scripts/tsconfig.json ./scripts/run-tests.ts --all-browsers",

View File

@ -1,4 +1,4 @@
import { ChildProcess, spawn } from "child_process";
import { ChildProcess, execSync, spawn } from "child_process";
import { EOL } from "os";
import { Readable } from "stream";
@ -9,6 +9,7 @@ import { promisify } from "util";
import * as karma from "karma";
import * as _debug from "debug";
const debug = _debug("signalr-functional-tests:run");
const ARTIFACTS_DIR = path.resolve(__dirname, "..", "..", "..", "..", "artifacts");
@ -156,6 +157,22 @@ function runKarma(karmaConfig) {
});
}
function runJest(url: string) {
const jestPath = path.resolve(__dirname, "..", "..", "common", "node_modules", "jest", "bin", "jest.js");
const configPath = path.resolve(__dirname, "..", "func.jest.config.js");
console.log("Starting Node tests using Jest.");
try {
execSync(`"${process.execPath}" "${jestPath}" --config "${configPath}"`, { env: { SERVER_URL: url }, timeout: 200000 });
return 0;
} catch (error) {
console.log(error.message);
console.log(error.stderr);
console.log(error.stdout);
return error.status;
}
}
(async () => {
try {
// Check if we got any browsers
@ -223,7 +240,12 @@ function runKarma(karmaConfig) {
conf.client.args = ["--server", url];
const results = await runKarma(conf);
process.exit(results.exitCode);
const jestExit = runJest(url);
console.log(`karma exit code: ${results.exitCode}`);
console.log(`jest exit code: ${jestExit}`);
process.exit(results.exitCode !== 0 ? results.exitCode : jestExit);
} catch (e) {
console.error(e);
process.exit(1);

View File

@ -6,7 +6,7 @@ import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
export let ENDPOINT_BASE_URL: string;
if ((window as any).__karma__) {
if (typeof window !== "undefined" && (window as any).__karma__) {
const args = (window as any).__karma__.config.args as string[];
let server = "";
@ -22,19 +22,28 @@ if ((window as any).__karma__) {
// Running in Karma? Need to use an absolute URL
ENDPOINT_BASE_URL = server;
console.log(`Using SignalR Server: ${ENDPOINT_BASE_URL}`);
} else {
} else if (typeof document !== "undefined") {
ENDPOINT_BASE_URL = `${document.location.protocol}//${document.location.host}`;
} else if (process && process.env && process.env.SERVER_URL) {
ENDPOINT_BASE_URL = process.env.SERVER_URL;
} else {
throw new Error("The server could not be found.");
}
export const ECHOENDPOINT_URL = ENDPOINT_BASE_URL + "/echo";
export function getHttpTransportTypes(): HttpTransportType[] {
const transportTypes = [];
if (typeof WebSocket !== "undefined") {
if (typeof window === "undefined") {
transportTypes.push(HttpTransportType.WebSockets);
}
if (typeof EventSource !== "undefined") {
transportTypes.push(HttpTransportType.ServerSentEvents);
} else {
if (typeof WebSocket !== "undefined") {
transportTypes.push(HttpTransportType.WebSockets);
}
if (typeof EventSource !== "undefined") {
transportTypes.push(HttpTransportType.ServerSentEvents);
}
}
transportTypes.push(HttpTransportType.LongPolling);
@ -49,9 +58,8 @@ export function eachTransport(action: (transport: HttpTransportType) => void) {
export function eachTransportAndProtocol(action: (transport: HttpTransportType, protocol: IHubProtocol) => void) {
const protocols: IHubProtocol[] = [new JsonHubProtocol()];
// IE9 does not support XmlHttpRequest advanced features so disable for now
// This can be enabled if we fix: https://github.com/aspnet/SignalR/issues/742
if (typeof new XMLHttpRequest().responseType === "string") {
// Run messagepack tests in Node and Browsers that support binary content (indicated by the presence of responseType property)
if (typeof XMLHttpRequest === "undefined" || typeof new XMLHttpRequest().responseType === "string") {
// Because of TypeScript stuff, we can't get "ambient" or "global" declarations to work with the MessagePackHubProtocol module
// This is only a limitation of the .d.ts file.
// Everything works fine in the module
@ -65,3 +73,7 @@ export function eachTransportAndProtocol(action: (transport: HttpTransportType,
});
});
}
export function getGlobalObject(): any {
return typeof window !== "undefined" ? window : global;
}

View File

@ -4,7 +4,7 @@
// This code uses a lot of `.then` instead of `await` and TSLint doesn't like it.
// tslint:disable:no-floating-promises
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol } from "@aspnet/signalr";
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@aspnet/signalr";
import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
import { eachTransport, eachTransportAndProtocol, ENDPOINT_BASE_URL } from "./Common";
@ -604,6 +604,12 @@ describe("hubConnection", () => {
});
it("transport falls back from WebSockets to SSE or LongPolling", async (done) => {
// Skip test on Node as there will always be a WebSockets implementation on Node
if (typeof window === "undefined") {
done();
return;
}
// Replace Websockets with a function that just
// throws to force fallback.
const oldWebSocket = (window as any).WebSocket;
@ -683,6 +689,11 @@ describe("hubConnection", () => {
});
it("populates the Content-Type header when sending XMLHttpRequest", async (done) => {
// Skip test on Node as this header isn't set (it was added for React-Native)
if (typeof window === "undefined") {
done();
return;
}
const hubConnection = getConnectionBuilder(HttpTransportType.LongPolling, TESTHUB_NOWEBSOCKETS_ENDPOINT_URL)
.withHubProtocol(new JsonHubProtocol())
.build();
@ -704,22 +715,14 @@ describe("hubConnection", () => {
function getJwtToken(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.send();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response || xhr.responseText);
const httpClient = new DefaultHttpClient(NullLogger.instance);
httpClient.get(url).then((response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(response.content as string);
} else {
reject(new Error(xhr.statusText));
reject(new Error(response.statusText));
}
};
xhr.onerror = () => {
reject(new Error(xhr.statusText));
};
});
});
}
});

View File

@ -6,28 +6,45 @@ import { ECHOENDPOINT_URL } from "./Common";
// On slower CI machines, these tests sometimes take longer than 5s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000;
if (typeof WebSocket !== "undefined") {
describe("WebSockets", () => {
it("can be used to connect to SignalR", (done) => {
const message = "message";
const webSocket = new WebSocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
webSocket.onopen = () => {
webSocket.send(message);
};
webSocket.onmessage = (event) => {
expect(event.data).toEqual(message);
webSocket.close();
};
webSocket.onclose = (event) => {
expect(event.code).toEqual(1000);
expect(event.wasClean).toBe(true, "WebSocket did not close cleanly");
describe("WebSockets", () => {
it("can be used to connect to SignalR", (done) => {
const message = "message";
let webSocket: WebSocket;
if (typeof window !== "undefined") {
if (typeof WebSocket !== "undefined") {
webSocket = new WebSocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
} else {
// Running in a browser that doesn't support WebSockets
done();
};
});
return;
}
} else {
const websocketModule = require("websocket");
const hasWebsocket = websocketModule && websocketModule.w3cwebsocket;
if (hasWebsocket) {
webSocket = new websocketModule.w3cwebsocket(ECHOENDPOINT_URL.replace(/^http/, "ws"));
} else {
// No WebSockets implementations in current environment, skip test
done();
return;
}
}
webSocket.onopen = () => {
webSocket.send(message);
};
webSocket.onmessage = (event) => {
expect(event.data).toEqual(message);
webSocket.close();
};
webSocket.onclose = (event) => {
expect(event.code).toEqual(1000);
expect(event.wasClean).toBe(true);
done();
};
});
}
});

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
const path = require("path");
const webpack = require("../common/node_modules/webpack");
module.exports = {
entry: path.resolve(__dirname, "ts", "index.ts"),

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr-protocol-msgpack",
"version": "1.1.0-preview1-t000",
"version": "3.0.0-alpha1-t000",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -26,13 +26,13 @@ export class MessagePackHubProtocol implements IHubProtocol {
/** Creates an array of HubMessage objects from the specified serialized representation.
*
* @param {ArrayBuffer} input An ArrayBuffer containing the serialized representation.
* @param {ArrayBuffer | Buffer} input An ArrayBuffer containing the serialized representation.
* @param {ILogger} logger A logger that will be used to log messages that occur during parsing.
*/
public parseMessages(input: ArrayBuffer, logger: ILogger): HubMessage[] {
public parseMessages(input: ArrayBuffer | Buffer, logger: ILogger): HubMessage[] {
// The interface does allow "string" to be passed in, but this implementation does not. So let's throw a useful error.
if (!(input instanceof ArrayBuffer)) {
throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer.");
if (!(input instanceof ArrayBuffer) && !(input instanceof Buffer)) {
throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");
}
if (logger === null) {

View File

@ -1,14 +1,124 @@
{
"name": "@aspnet/signalr",
"version": "1.1.0-preview1-t000",
"version": "3.0.0-alpha1-t000",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/events": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
"dev": true
},
"@types/eventsource": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.0.2.tgz",
"integrity": "sha512-CprOekOB/lzAiGDF1MPWHX053RVTCYyYU3M8HOQXpdD0QfXijM//Na/hZxHaQv4ydsiB1uOBQ3p8S5nXpP4nNQ==",
"dev": true
},
"@types/node": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz",
"integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==",
"dev": true
},
"@types/websocket": {
"version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.40.tgz",
"integrity": "sha512-ldteZwWIgl9cOy7FyvYn+39Ah4+PfpVE72eYKw75iy2L0zTbhbcwvzeJ5IOu6DQP93bjfXq0NGHY6FYtmYoqFQ==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/node": "*"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"es6-promise": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.2.tgz",
"integrity": "sha512-LSas5vsuA6Q4nEdf9wokY5/AJYXry98i0IzXsv49rYsgDGDNDPbqAYR1Pe23iFxygfbGZNR/5VrHXBCh2BhvUQ==",
"dev": true
},
"eventsource": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz",
"integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==",
"requires": {
"original": "^1.0.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz",
"integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw=="
},
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
"integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
"requires": {
"url-parse": "^1.4.3"
}
},
"querystringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
"integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw=="
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"url-parse": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
"integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
"requires": {
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"websocket": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
"integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==",
"requires": {
"debug": "^2.2.0",
"nan": "^2.3.3",
"typedarray-to-buffer": "^3.1.2",
"yaeti": "^0.0.6"
}
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
}
}
}

View File

@ -36,6 +36,12 @@
"src/**/*"
],
"devDependencies": {
"es6-promise": "^4.2.2"
"es6-promise": "^4.2.2",
"@types/websocket": "^0.0.40",
"@types/eventsource": "^1.0.2"
},
"dependencies": {
"websocket": "^1.0.26",
"eventsource": "^1.0.7"
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger } from "./ILogger";
import { XhrHttpClient } from "./XhrHttpClient";
let nodeHttpClientModule: any;
if (typeof XMLHttpRequest === "undefined") {
// tslint:disable-next-line:no-var-requires
nodeHttpClientModule = require("./NodeHttpClient");
}
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */
export class DefaultHttpClient extends HttpClient {
private readonly httpClient: HttpClient;
/** Creates a new instance of the {@link @aspnet/signalr.DefaultHttpClient}, using the provided {@link @aspnet/signalr.ILogger} to log messages. */
public constructor(logger: ILogger) {
super();
if (typeof XMLHttpRequest !== "undefined") {
this.httpClient = new XhrHttpClient(logger);
} else if (typeof nodeHttpClientModule !== "undefined") {
this.httpClient = new nodeHttpClientModule.NodeHttpClient(logger);
} else {
throw new Error("No HttpClient could be created.");
}
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
return Promise.reject(new AbortError());
}
if (!request.method) {
return Promise.reject(new Error("No method defined."));
}
if (!request.url) {
return Promise.reject(new Error("No url defined."));
}
return this.httpClient.send(request);
}
}

View File

@ -27,7 +27,7 @@ export class HandshakeProtocol {
let messageData: string;
let remainingData: any;
if (data instanceof ArrayBuffer) {
if (data instanceof ArrayBuffer || (typeof Buffer !== "undefined" && data instanceof Buffer)) {
// Format is binary but still need to read JSON text from handshake response
const binaryData = new Uint8Array(data);
const separatorIndex = binaryData.indexOf(TextMessageFormat.RecordSeparatorCode);

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortSignal } from "./AbortController";
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { ILogger, LogLevel } from "./ILogger";
/** Represents an HTTP request. */
export interface HttpRequest {
@ -144,89 +142,3 @@ export abstract class HttpClient {
*/
public abstract send(request: HttpRequest): Promise<HttpResponse>;
}
/** Default implementation of {@link @aspnet/signalr.HttpClient}. */
export class DefaultHttpClient extends HttpClient {
private readonly logger: ILogger;
/** Creates a new instance of the {@link @aspnet/signalr.DefaultHttpClient}, using the provided {@link @aspnet/signalr.ILogger} to log messages. */
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
reject(new AbortError());
return;
}
const xhr = new XMLHttpRequest();
if (!request.method) {
reject(new Error("No method defined."));
return;
}
if (!request.url) {
reject(new Error("No url defined."));
return;
}
xhr.open(request.method, request.url, true);
xhr.withCredentials = true;
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Explicitly setting the Content-Type header for React Native on Android platform.
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
const headers = request.headers;
if (headers) {
Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
}
if (request.responseType) {
xhr.responseType = request.responseType;
}
if (request.abortSignal) {
request.abortSignal.onabort = () => {
xhr.abort();
reject(new AbortError());
};
}
if (request.timeout) {
xhr.timeout = request.timeout;
}
xhr.onload = () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText));
} else {
reject(new HttpError(xhr.statusText, xhr.status));
}
};
xhr.onerror = () => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${xhr.status}: ${xhr.statusText}`);
reject(new HttpError(xhr.statusText, xhr.status));
};
xhr.ontimeout = () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
};
xhr.send(request.content || "");
});
}
}

View File

@ -1,7 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { DefaultHttpClient, HttpClient } from "./HttpClient";
import { DefaultHttpClient } from "./DefaultHttpClient";
import { HttpClient } from "./HttpClient";
import { IConnection } from "./IConnection";
import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
import { ILogger, LogLevel } from "./ILogger";
@ -34,6 +35,15 @@ export interface IAvailableTransport {
const MAX_REDIRECTS = 100;
let WebSocketModule: any = null;
let EventSourceModule: any = null;
if (typeof window === "undefined" && typeof require !== "undefined") {
// tslint:disable-next-line:no-var-requires
WebSocketModule = require("websocket");
// tslint:disable-next-line:no-var-requires
EventSourceModule = require("eventsource");
}
/** @private */
export class HttpConnection implements IConnection {
private connectionState: ConnectionState;
@ -59,11 +69,22 @@ export class HttpConnection implements IConnection {
options = options || {};
options.logMessageContent = options.logMessageContent || false;
if (typeof WebSocket !== "undefined" && !options.WebSocket) {
const isNode = typeof window === "undefined";
if (!isNode && typeof WebSocket !== "undefined" && !options.WebSocket) {
options.WebSocket = WebSocket;
} else if (isNode && !options.WebSocket) {
const websocket = WebSocketModule && WebSocketModule.w3cwebsocket;
if (websocket) {
options.WebSocket = WebSocketModule.w3cwebsocket;
}
}
if (typeof EventSource !== "undefined" && !options.EventSource) {
if (!isNode && typeof EventSource !== "undefined" && !options.EventSource) {
options.EventSource = EventSource;
} else if (isNode && !options.EventSource) {
if (typeof EventSourceModule !== "undefined") {
options.EventSource = EventSourceModule;
}
}
this.httpClient = options.httpClient || new DefaultHttpClient(this.logger);
@ -103,6 +124,10 @@ export class HttpConnection implements IConnection {
public async stop(error?: Error): Promise<void> {
this.connectionState = ConnectionState.Disconnected;
// Set error as soon as possible otherwise there is a race between
// the transport closing and providing an error and the error from a close message
// We would prefer the close message error.
this.stopError = error;
try {
await this.startPromise;
@ -112,7 +137,6 @@ export class HttpConnection implements IConnection {
// The transport's onclose will trigger stopConnection which will run our onclose event.
if (this.transport) {
this.stopError = error;
await this.transport.stop();
this.transport = undefined;
}

View File

@ -52,8 +52,9 @@ export class LongPollingTransport implements ITransport {
this.logger.log(LogLevel.Trace, "(LongPolling transport) Connecting");
if (transferFormat === TransferFormat.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
// This will work if we fix: https://github.com/aspnet/SignalR/issues/742
// Allow binary format on Node and Browsers that support binary content (indicated by the presence of responseType property)
if (transferFormat === TransferFormat.Binary &&
(typeof XMLHttpRequest !== "undefined" && typeof new XMLHttpRequest().responseType !== "string")) {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
}

View File

@ -0,0 +1,91 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import * as http from "http";
import { URL } from "url";
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
export class NodeHttpClient extends HttpClient {
private readonly logger: ILogger;
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
public send(request: HttpRequest): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
const url = new URL(request.url!);
const options: http.RequestOptions = {
headers: {
// Tell auth middleware to 401 instead of redirecting
"X-Requested-With": "XMLHttpRequest",
...request.headers,
},
hostname: url.hostname,
method: request.method,
// /abc/xyz + ?id=12ssa_30
path: url.pathname + url.search,
port: url.port,
};
const req = http.request(options, (res: http.IncomingMessage) => {
const data: Buffer[] = [];
let dataLength = 0;
res.on("data", (chunk: any) => {
data.push(chunk);
// Buffer.concat will be slightly faster if we keep track of the length
dataLength += chunk.length;
});
res.on("end", () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
let resp: string | ArrayBuffer;
if (request.responseType === "arraybuffer") {
resp = Buffer.concat(data, dataLength);
resolve(new HttpResponse(res.statusCode, res.statusMessage || "", resp));
} else {
resp = Buffer.concat(data, dataLength).toString();
resolve(new HttpResponse(res.statusCode, res.statusMessage || "", resp));
}
} else {
reject(new HttpError(res.statusMessage || "", res.statusCode || 0));
}
});
});
if (request.abortSignal) {
request.abortSignal.onabort = () => {
req.abort();
reject(new AbortError());
};
}
if (request.timeout) {
req.setTimeout(request.timeout, () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
});
}
req.on("error", (e) => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${e}`);
reject(e);
});
if (request.content instanceof ArrayBuffer) {
req.write(Buffer.from(request.content));
} else {
req.write(request.content || "");
}
req.end();
});
}
}

View File

@ -68,9 +68,11 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`);
const responseType = content instanceof ArrayBuffer ? "arraybuffer" : "text";
const response = await httpClient.post(url, {
content,
headers,
responseType,
});
logger.log(LogLevel.Trace, `(${transportName} transport) request complete. Response status: ${response.statusCode}.`);

View File

@ -57,7 +57,11 @@ export class WebSocketTransport implements ITransport {
};
webSocket.onerror = (event: Event) => {
const error = (event instanceof ErrorEvent) ? event.error : null;
let error: any = null;
// ErrorEvent is a browser only type we need to check if the type exists before using it
if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
error = event.error;
}
reject(error);
};

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { AbortError, HttpError, TimeoutError } from "./Errors";
import { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
export class XhrHttpClient extends HttpClient {
private readonly logger: ILogger;
public constructor(logger: ILogger) {
super();
this.logger = logger;
}
/** @inheritDoc */
public send(request: HttpRequest): Promise<HttpResponse> {
// Check that abort was not signaled before calling send
if (request.abortSignal && request.abortSignal.aborted) {
return Promise.reject(new AbortError());
}
if (!request.method) {
return Promise.reject(new Error("No method defined."));
}
if (!request.url) {
return Promise.reject(new Error("No url defined."));
}
return new Promise<HttpResponse>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(request.method!, request.url!, true);
xhr.withCredentials = true;
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Explicitly setting the Content-Type header for React Native on Android platform.
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
const headers = request.headers;
if (headers) {
Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
}
if (request.responseType) {
xhr.responseType = request.responseType;
}
if (request.abortSignal) {
request.abortSignal.onabort = () => {
xhr.abort();
reject(new AbortError());
};
}
if (request.timeout) {
xhr.timeout = request.timeout;
}
xhr.onload = () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText));
} else {
reject(new HttpError(xhr.statusText, xhr.status));
}
};
xhr.onerror = () => {
this.logger.log(LogLevel.Warning, `Error from HTTP request. ${xhr.status}: ${xhr.statusText}`);
reject(new HttpError(xhr.statusText, xhr.status));
};
xhr.ontimeout = () => {
this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
reject(new TimeoutError());
};
xhr.send(request.content || "");
});
}
}

View File

@ -8,7 +8,8 @@ export const VERSION: string = "0.0.0-DEV_BUILD";
// Everything that users need to access must be exported here. Including interfaces.
export { AbortSignal } from "./AbortController";
export { AbortError, HttpError, TimeoutError } from "./Errors";
export { DefaultHttpClient, HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
export { HttpClient, HttpRequest, HttpResponse } from "./HttpClient";
export { DefaultHttpClient } from "./DefaultHttpClient";
export { IHttpConnectionOptions } from "./IHttpConnectionOptions";
export { HubConnection, HubConnectionState } from "./HubConnection";
export { HubConnectionBuilder } from "./HubConnectionBuilder";

View File

@ -12,7 +12,7 @@ import { EventSourceConstructor, WebSocketConstructor } from "../src/Polyfills";
import { eachEndpointUrl, eachTransport, VerifyLogger } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource, registerUnhandledRejectionHandler } from "./Utils";
import { PromiseSource, registerUnhandledRejectionHandler, SyncPoint } from "./Utils";
const commonOptions: IHttpConnectionOptions = {
logger: NullLogger.instance,
@ -320,8 +320,30 @@ describe("HttpConnection", () => {
for (const [val, name] of [[null, "null"], [undefined, "undefined"], [0, "0"]]) {
it(`can be started when transport mask is ${name}`, async () => {
let websocketOpen: (() => any) | null = null;
const sync: SyncPoint = new SyncPoint();
const websocket = class WebSocket {
constructor() {
this._onopen = null;
}
// tslint:disable-next-line:variable-name
private _onopen: ((this: WebSocket, ev: Event) => any) | null;
public get onopen(): ((this: WebSocket, ev: Event) => any) | null {
return this._onopen;
}
public set onopen(onopen: ((this: WebSocket, ev: Event) => any) | null) {
this._onopen = onopen;
websocketOpen = () => this._onopen!({} as Event);
sync.continue();
}
public close(): void {
}
};
await VerifyLogger.run(async (logger) => {
const options: IHttpConnectionOptions = {
WebSocket: websocket as any,
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", () => defaultNegotiateResponse)
@ -333,7 +355,10 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
await connection.start(TransferFormat.Text);
const startPromise = connection.start(TransferFormat.Text);
await sync.waitToContinue();
websocketOpen!();
await startPromise;
await connection.stop();
});
@ -359,10 +384,18 @@ describe("HttpConnection", () => {
});
it("does not send negotiate request if WebSockets transport requested explicitly and skipNegotiation is true", async () => {
const websocket = class WebSocket {
constructor() {
throw new Error("WebSocket constructor called.");
}
};
await VerifyLogger.run(async (logger) => {
const options: IHttpConnectionOptions = {
WebSocket: websocket as any,
...commonOptions,
httpClient: new TestHttpClient(),
httpClient: new TestHttpClient()
.on("POST", () => { throw new Error("Should not be called"); })
.on("GET", () => { throw new Error("Should not be called"); }),
logger,
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
@ -371,9 +404,9 @@ describe("HttpConnection", () => {
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("'WebSocket' is not supported in your environment.");
.toThrow("WebSocket constructor called.");
},
"Failed to start the connection: Error: 'WebSocket' is not supported in your environment.");
"Failed to start the connection: Error: WebSocket constructor called.");
});
it("does not start non WebSockets transport if requested explicitly and skipNegotiation is true", async () => {
@ -603,120 +636,12 @@ describe("HttpConnection", () => {
});
});
it("does not select ServerSentEvents transport when not available in environment", async () => {
await VerifyLogger.run(async (logger) => {
const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", () => ({ connectionId: "42", availableTransports: [serverSentEventsTransport] })),
logger,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
},
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
it("does not select WebSockets transport when not available in environment", async () => {
await VerifyLogger.run(async (logger) => {
const webSocketsTransport = { transport: "WebSockets", transferFormats: ["Text"] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", () => ({ connectionId: "42", availableTransports: [webSocketsTransport] })),
logger,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
},
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
describe(".constructor", () => {
it("throws if no Url is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :)
expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
});
it("uses global WebSocket if defined", async () => {
await VerifyLogger.run(async (logger) => {
// tslint:disable-next-line:no-string-literal
global["WebSocket"] = class WebSocket {
constructor() {
throw new Error("WebSocket constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
logger,
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start())
.rejects
.toThrow("WebSocket constructor called.");
// tslint:disable-next-line:no-string-literal
delete global["WebSocket"];
},
"Failed to start the connection: Error: WebSocket constructor called.");
});
it("uses global EventSource if defined", async () => {
await VerifyLogger.run(async (logger) => {
let eventSourceConstructorCalled: boolean = false;
// tslint:disable-next-line:no-string-literal
global["EventSource"] = class EventSource {
constructor() {
eventSourceConstructorCalled = true;
throw new Error("EventSource constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient().on("POST", () => {
return {
availableTransports: [
{ transport: "ServerSentEvents", transferFormats: ["Text"] },
],
connectionId: defaultConnectionId,
};
}),
logger,
transport: HttpTransportType.ServerSentEvents,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
expect(eventSourceConstructorCalled).toEqual(true);
// tslint:disable-next-line:no-string-literal
delete global["EventSource"];
},
"Failed to start the transport 'ServerSentEvents': Error: EventSource constructor called.",
"Failed to start the connection: Error: Unable to initialize any of the available transports.");
});
it("uses EventSource constructor from options if provided", async () => {
await VerifyLogger.run(async (logger) => {
let eventSourceConstructorCalled: boolean = false;

View File

@ -2,4 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
const baseConfig = require("../webpack.config.base");
module.exports = baseConfig(__dirname, "signalr");
module.exports = baseConfig(__dirname, "signalr", {
// These are only used in Node environments
// so we tell webpack not to pull them in for the browser
externals: [
"websocket",
"eventsource",
"http",
"url",
]
});

View File

@ -6,7 +6,8 @@
"noUnusedParameters": false,
"typeRoots": [
"./common/node_modules/@types"
]
],
"allowJs": true
},
"include": [
"./*/tests/**/*"

View File

@ -72,6 +72,9 @@ module.exports = function (modulePath, browserBaseName, options) {
}),
// ES6 Promise uses this module in certain circumstances but we don't need it.
new webpack.IgnorePlugin(/vertx/),
new webpack.IgnorePlugin(/NodeHttpClient/),
new webpack.IgnorePlugin(/eventsource/),
new webpack.IgnorePlugin(/websocket/),
],
externals: options.externals,
};

View File

@ -218,8 +218,18 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// Cancel the previous request
connection.Cancellation?.Cancel();
// Always wait for the previous request to drain
await connection.PreviousPollTask;
try
{
// Wait for the previous request to drain
await connection.PreviousPollTask;
}
catch (OperationCanceledException)
{
// Previous poll canceled due to connection closing, close this poll too
context.Response.ContentType = "text/plain";
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
connection.PreviousPollTask = currentRequestTcs.Task;
}
@ -289,6 +299,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
// If the status code is a 204 it means the connection is done
if (context.Response.StatusCode == StatusCodes.Status204NoContent)
{
// Cancel current request to release any waiting poll and let dispose aquire the lock
currentRequestTcs.TrySetCanceled();
// We should be able to safely dispose because there's no more data being written
// We don't need to wait for close here since we've already waited for both sides
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);
@ -299,6 +312,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
else if (resultTask.IsFaulted)
{
// Cancel current request to release any waiting poll and let dispose aquire the lock
currentRequestTcs.TrySetCanceled();
// transport task was faulted, we should remove the connection
await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false);

View File

@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
return PublishAsync(_channels.Group(groupName), message);
}
public override async Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
public override Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
{
if (groupName == null)
{
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
}
var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds);
await PublishAsync(_channels.Group(groupName), message);
return PublishAsync(_channels.Group(groupName), message);
}
public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default)
@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
return PublishAsync(_channels.User(userId), message);
}
public override async Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{
if (connectionId == null)
{
@ -176,14 +176,13 @@ namespace Microsoft.AspNetCore.SignalR.Redis
if (connection != null)
{
// short circuit if connection is on this server
await AddGroupAsyncCore(connection, groupName);
return;
return AddGroupAsyncCore(connection, groupName);
}
await SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add);
return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add);
}
public override async Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{
if (connectionId == null)
{
@ -199,11 +198,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
if (connection != null)
{
// short circuit if connection is on this server
await RemoveGroupAsyncCore(connection, groupName);
return;
return RemoveGroupAsyncCore(connection, groupName);
}
await SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove);
return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove);
}
public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
@ -271,7 +269,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
await _bus.PublishAsync(channel, payload);
}
private async Task AddGroupAsyncCore(HubConnectionContext connection, string groupName)
private Task AddGroupAsyncCore(HubConnectionContext connection, string groupName)
{
var feature = connection.Features.Get<IRedisFeature>();
var groupNames = feature.Groups;
@ -281,12 +279,12 @@ namespace Microsoft.AspNetCore.SignalR.Redis
// Connection already in group
if (!groupNames.Add(groupName))
{
return;
return Task.CompletedTask;
}
}
var groupChannel = _channels.Group(groupName);
await _groups.AddSubscriptionAsync(groupChannel, connection, SubscribeToGroupAsync);
return _groups.AddSubscriptionAsync(groupChannel, connection, SubscribeToGroupAsync);
}
/// <summary>
@ -297,10 +295,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
var groupChannel = _channels.Group(groupName);
await _groups.RemoveSubscriptionAsync(groupChannel, connection, async channelName =>
await _groups.RemoveSubscriptionAsync(groupChannel, connection, channelName =>
{
RedisLog.Unsubscribe(_logger, channelName);
await _bus.UnsubscribeAsync(channelName);
return _bus.UnsubscribeAsync(channelName);
});
var feature = connection.Features.Get<IRedisFeature>();
@ -329,10 +327,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
var userChannel = _channels.User(connection.UserIdentifier);
return _users.RemoveSubscriptionAsync(userChannel, connection, async channelName =>
return _users.RemoveSubscriptionAsync(userChannel, connection, channelName =>
{
RedisLog.Unsubscribe(_logger, channelName);
await _bus.UnsubscribeAsync(channelName);
return _bus.UnsubscribeAsync(channelName);
});
}
@ -343,10 +341,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
_ackHandler.Dispose();
}
private void SubscribeToAll()
private Task SubscribeToAll()
{
RedisLog.Subscribing(_logger, _channels.All);
_bus.Subscribe(_channels.All, async (c, data) =>
return _bus.SubscribeAsync(_channels.All, async (c, data) =>
{
try
{
@ -373,9 +371,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis
});
}
private void SubscribeToGroupManagementChannel()
private Task SubscribeToGroupManagementChannel()
{
_bus.Subscribe(_channels.GroupManagement, async (c, data) =>
return _bus.SubscribeAsync(_channels.GroupManagement, async (c, data) =>
{
try
{
@ -408,10 +406,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
});
}
private void SubscribeToAckChannel()
private Task SubscribeToAckChannel()
{
// Create server specific channel in order to send an ack to a single server
_bus.Subscribe(_channels.Ack(_serverName), (c, data) =>
return _bus.SubscribeAsync(_channels.Ack(_serverName), (c, data) =>
{
var ackId = _protocol.ReadAck((byte[])data);
@ -435,9 +433,10 @@ namespace Microsoft.AspNetCore.SignalR.Redis
{
var userChannel = _channels.User(connection.UserIdentifier);
return _users.AddSubscriptionAsync(userChannel, connection, async (channelName, subscriptions) =>
return _users.AddSubscriptionAsync(userChannel, connection, (channelName, subscriptions) =>
{
await _bus.SubscribeAsync(channelName, async (c, data) =>
RedisLog.Subscribing(_logger, channelName);
return _bus.SubscribeAsync(channelName, async (c, data) =>
{
try
{
@ -534,9 +533,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis
RedisLog.NotConnected(_logger);
}
SubscribeToAll();
SubscribeToGroupManagementChannel();
SubscribeToAckChannel();
await SubscribeToAll();
await SubscribeToGroupManagementChannel();
await SubscribeToAckChannel();
}
}
finally

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
private static readonly string _dockerContainerName = "redisTestContainer";
private static readonly string _dockerMonitorContainerName = _dockerContainerName + "Monitor";
private static readonly Lazy<Docker> _instance = new Lazy<Docker>(Create);
public static Docker Default => _instance.Value;
@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
var docker = new Docker(location);
docker.RunCommand("info --format '{{.OSType}}'", out var output);
docker.RunCommand("info --format '{{.OSType}}'", "docker info", out var output);
if (!string.Equals(output.Trim('\'', '"', '\r', '\n', ' '), "linux"))
{
@ -74,40 +75,49 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
logger.LogInformation("Starting docker container");
// stop container if there is one, could be from a previous test run, ignore failures
RunProcess(_path, $"stop {_dockerContainerName}", logger, TimeSpan.FromSeconds(5), out var output);
RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var output);
// create and run docker container, remove automatically when stopped, map 6379 from the container to 6379 localhost
// use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more
// use redis base docker image
// 20 second timeout to allow redis image to be downloaded, should be a rare occurance, only happening when a new version is released
RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d redis", logger, TimeSpan.FromSeconds(20));
RunProcessAndThrowIfFailed(_path, $"run --rm -p 6379:6379 --name {_dockerContainerName} -d redis", "redis", logger, TimeSpan.FromSeconds(20));
// inspect the redis docker image and extract the IPAddress. Necessary when running tests from inside a docker container, spinning up a new docker container for redis
// outside the current container requires linking the networks (difficult to automate) or using the IP:Port combo
RunProcess(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, logger, TimeSpan.FromSeconds(5), out output);
RunProcessAndWait(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, "docker ipaddress", logger, TimeSpan.FromSeconds(5), out output);
output = output.Trim().Replace(Environment.NewLine, "");
// variable used by Startup.cs
Environment.SetEnvironmentVariable("REDIS_CONNECTION", $"{output}:6379");
var (monitorProcess, monitorOutput) = RunProcess(_path, $"run -i --name {_dockerMonitorContainerName} --link {_dockerContainerName}:redis --rm redis redis-cli -h redis -p 6379", "redis monitor", logger);
monitorProcess.StandardInput.WriteLine("MONITOR");
monitorProcess.StandardInput.Flush();
}
public void Stop(ILogger logger)
{
// Get logs from Redis container before stopping the container
RunProcessAndThrowIfFailed(_path, $"logs {_dockerContainerName}", "docker logs", logger, TimeSpan.FromSeconds(5));
logger.LogInformation("Stopping docker container");
RunProcessAndThrowIfFailed(_path, $"stop {_dockerContainerName}", logger, TimeSpan.FromSeconds(5));
RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
}
public int RunCommand(string commandAndArguments, out string output) =>
RunCommand(commandAndArguments, NullLogger.Instance, out output);
public int RunCommand(string commandAndArguments, string prefix, out string output) =>
RunCommand(commandAndArguments, prefix, NullLogger.Instance, out output);
public int RunCommand(string commandAndArguments, ILogger logger, out string output)
public int RunCommand(string commandAndArguments, string prefix, ILogger logger, out string output)
{
return RunProcess(_path, commandAndArguments, logger, TimeSpan.FromSeconds(5), out output);
return RunProcessAndWait(_path, commandAndArguments, prefix, logger, TimeSpan.FromSeconds(5), out output);
}
private static void RunProcessAndThrowIfFailed(string fileName, string arguments, ILogger logger, TimeSpan timeout)
private static void RunProcessAndThrowIfFailed(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout)
{
var exitCode = RunProcess(fileName, arguments, logger, timeout, out var output);
var exitCode = RunProcessAndWait(fileName, arguments, prefix, logger, timeout, out var output);
if (exitCode != 0)
{
@ -115,39 +125,9 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
}
}
private static int RunProcess(string fileName, string arguments, ILogger logger, TimeSpan timeout, out string output)
private static int RunProcessAndWait(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout, out string output)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true
},
EnableRaisingEvents = true
};
var exitCode = 0;
var lines = new ConcurrentQueue<string>();
process.Exited += (_, __) => exitCode = process.ExitCode;
process.OutputDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogInformation, "stdout: {0}", a.Data);
lines.Enqueue(a.Data);
};
process.ErrorDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogError, "stderr: {0}", a.Data);
lines.Enqueue(a.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
var (process, lines) = RunProcess(fileName, arguments, prefix, logger);
if (!process.WaitForExit((int)timeout.TotalMilliseconds))
{
@ -160,7 +140,45 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
output = string.Join(Environment.NewLine, lines);
return exitCode;
return process.ExitCode;
}
private static (Process, ConcurrentQueue<string>) RunProcess(string fileName, string arguments, string prefix, ILogger logger)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
},
EnableRaisingEvents = true
};
var exitCode = 0;
var lines = new ConcurrentQueue<string>();
process.Exited += (_, __) => exitCode = process.ExitCode;
process.OutputDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogInformation, $"'{prefix}' stdout: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.ErrorDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogError, $"'{prefix}' stderr: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
return (process, lines);
}
private static void LogIfNotNull(Action<string, object[]> logger, string message, string data)

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis.Tests
if(Docker.Default != null)
{
// Docker is present, but is it working?
if (Docker.Default.RunCommand("ps", out var output) != 0)
if (Docker.Default.RunCommand("ps", "docker ps", out var output) != 0)
{
SkipReason = $"Failed to invoke test command 'docker ps'. Output: {output}";
}