Merge branch 'release/2.2'
This commit is contained in:
commit
b423313e5c
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package com.microsoft.aspnet.signalr;
|
||||
|
||||
public class CloseMessage extends HubMessage {
|
||||
class CloseMessage extends HubMessage {
|
||||
String error;
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package com.microsoft.aspnet.signalr;
|
||||
|
||||
public class HandshakeRequestMessage {
|
||||
class HandshakeRequestMessage {
|
||||
String protocol;
|
||||
int version;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package com.microsoft.aspnet.signalr;
|
||||
|
||||
public class HandshakeResponseMessage {
|
||||
class HandshakeResponseMessage {
|
||||
public String error;
|
||||
|
||||
public HandshakeResponseMessage() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
|
||||
package com.microsoft.aspnet.signalr;
|
||||
|
||||
public interface OnReceiveCallBack {
|
||||
interface OnReceiveCallBack {
|
||||
void invoke(String message) throws Exception;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package com.microsoft.aspnet.signalr;
|
||||
|
||||
public class PingMessage extends HubMessage {
|
||||
class PingMessage extends HubMessage {
|
||||
|
||||
int type = HubMessageType.PING.value;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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";
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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();
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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"
|
||||
]
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 || "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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}.`);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 || "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
});
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
"noUnusedParameters": false,
|
||||
"typeRoots": [
|
||||
"./common/node_modules/@types"
|
||||
]
|
||||
],
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"./*/tests/**/*"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue