Skip to content

Commit 25a81bf

Browse files
authored
Merge pull request #41 from browserstack/PPLT-2916
Executing customRequests in Playwright session
2 parents 4ed6c2c + b49d716 commit 25a81bf

File tree

10 files changed

+896
-25
lines changed

10 files changed

+896
-25
lines changed

lib/config/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const env = process.env.NODE_ENV || 'dev';
4+
const customRequestEnabled = process.env.CUSTOM_REQUEST_ENABLED === 'true';
45
const config = require('./config.json')[env];
56
const logger = require('../util/loggerFactory');
67
const {
@@ -252,7 +253,8 @@ module.exports = {
252253
kEnableIncomingQueue,
253254
kEnableOutgoingQueue,
254255
kUpstreamRestart,
256+
customRequestEnabled,
255257
PROXY_LOCKED,
256258
PROXY_RESTART,
257-
HTTPLOG
259+
HTTPLOG,
258260
};

lib/core/CustomRequestHandler.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const logger = require('../util/loggerFactory');
4+
5+
/**
6+
* Handles the custom requests made to existing playwright connection.
7+
* This class is implemented as a Singleton to maintain a map of commands for which
8+
* responses can be resolved once received from the Playwright server.
9+
* @class
10+
*/
11+
class CustomRequestHandler {
12+
/**
13+
* Creates an instance of CustomRequestHandler.
14+
* @constructor
15+
*/
16+
constructor() {
17+
if (!CustomRequestHandler.instance) {
18+
// Initialize the instance if it doesn't exist
19+
CustomRequestHandler.instance = this;
20+
// Initialize the map {} as part of the instance
21+
this.customRequestList = {};
22+
}
23+
// Return the existing instance if it already exists
24+
return CustomRequestHandler.instance;
25+
}
26+
27+
/**
28+
* Static method to get the single instance of the class.
29+
* @returns {CustomRequestHandler} The single instance of the CustomRequestHandler class.
30+
*/
31+
static getInstance() {
32+
if (!CustomRequestHandler.instance) {
33+
// Create a new instance if it doesn't exist
34+
CustomRequestHandler.instance = new CustomRequestHandler();
35+
}
36+
// Return the existing instance
37+
return CustomRequestHandler.instance;
38+
}
39+
40+
/**
41+
* Checks if the custom request list is empty.
42+
* @returns {boolean} Returns true if the custom request list is empty, otherwise false.
43+
*/
44+
isCustomRequestListEmpty() {
45+
for (const prop in this.customRequestList) {
46+
if (this.customRequestList.hasOwnProperty(prop)) {
47+
return false;
48+
}
49+
}
50+
51+
return true;
52+
}
53+
54+
/**
55+
* Adds an item to the custom request list.
56+
* @param {string} request_id - The ID of the request to be added.
57+
*/
58+
addCustomRequest(request_id) {
59+
let resolveFunc;
60+
let rejectFunc;
61+
let promise = new Promise((resolve, reject) => {
62+
resolveFunc = resolve;
63+
rejectFunc = reject;
64+
});
65+
this.customRequestList[request_id] = {
66+
resolve: resolveFunc,
67+
reject: rejectFunc,
68+
promise: promise,
69+
};
70+
logger.info(`Added request '${request_id}' to the customRequestList.`);
71+
}
72+
73+
/**
74+
* Gets the items in the custom request list.
75+
* @returns {Object} The custom request list.
76+
*/
77+
getList() {
78+
return this.customRequestList;
79+
}
80+
81+
/**
82+
* Resets the instance of the CustomRequestHandler class.
83+
* Only for testing purposes. Do not use it in production code.
84+
* @static
85+
*/
86+
static resetInstance() {
87+
CustomRequestHandler.instance = null;
88+
}
89+
}
90+
91+
module.exports = CustomRequestHandler;

lib/core/IncomingWebSocket.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ class IncomingWebSocket extends EventEmitter {
117117
*/
118118
close(code, msg) {
119119
this.teardown = true;
120-
if(code >= 1000 && code < 1004)
121-
this.socket.close(code, msg);
120+
if (code >= 1000 && code < 1004) this.socket.close(code, msg);
122121
this.socket.terminate();
123122
}
124123

lib/core/OutgoingWebSocket.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ const {
2424
PROXY_RESTART,
2525
DISALLOWED_HEADERS,
2626
OUTGOING,
27+
customRequestEnabled,
2728
} = require('../config/constants');
2829
const { extractConnectionId } = require('../util/util');
2930
const { incrReconnectionCount } = require('../util/metrics');
3031
const { isNotUndefined } = require('../util/typeSanity');
32+
const CustomRequestHandler = require('./CustomRequestHandler');
3133

3234
/**
3335
* Outgoing WebSocket connection is the connection object
@@ -107,6 +109,20 @@ class OutgoingWebSocket extends EventEmitter {
107109
this.emit(kEnableIncomingQueue);
108110
return;
109111
}
112+
113+
const customReqInstance = CustomRequestHandler.getInstance();
114+
if (customRequestEnabled && !customReqInstance.isCustomRequestListEmpty()) {
115+
let resp;
116+
try {
117+
resp = JSON.parse(msg);
118+
if (resp && customReqInstance.getList().hasOwnProperty(resp.id)) {
119+
customReqInstance.customRequestList[resp.id].resolve(msg);
120+
return;
121+
}
122+
} catch (error) {
123+
logger.error(`Error parsing JSON: ${error}`);
124+
}
125+
}
110126
this.emit(kMessageReceived, msg);
111127
}
112128

@@ -136,6 +152,19 @@ class OutgoingWebSocket extends EventEmitter {
136152
* Triggers when error occured on socket.
137153
*/
138154
errorHandler(error) {
155+
const customReqInstance = CustomRequestHandler.getInstance();
156+
if (customRequestEnabled && !customReqInstance.isCustomRequestListEmpty()) {
157+
let resp;
158+
try {
159+
resp = JSON.parse(error);
160+
if (resp && customReqInstance.getList().hasOwnProperty(resp.id)) {
161+
customReqInstance.customRequestList[resp.id].reject(error);
162+
return;
163+
}
164+
} catch (error) {
165+
logger.error(`Error parsing JSON: ${error}`);
166+
}
167+
}
139168
this.emit(kError, error);
140169
}
141170

@@ -164,12 +193,9 @@ class OutgoingWebSocket extends EventEmitter {
164193
* Closes the socket connection.
165194
*/
166195
close(code, msg) {
167-
if(code == 1006)
168-
this.socket.terminate();
169-
else if(code == 1005)
170-
this.socket.close();
171-
else
172-
this.socket.close(code, msg);
196+
if (code == 1006) this.socket.terminate();
197+
else if (code == 1005) this.socket.close();
198+
else this.socket.close(code, msg);
173199
}
174200

175201
/**

lib/core/Proxy.js

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
'use strict';
22

33
const WebSocket = require('ws');
4-
const { config, kCleanup, kAddNewContext, HTTPLOG } = require('../config/constants');
4+
const {
5+
config,
6+
kCleanup,
7+
kAddNewContext,
8+
HTTPLOG,
9+
customRequestEnabled,
10+
} = require('../config/constants');
511
const logger = require('../util/loggerFactory');
612
const Context = require('./Context');
713
const {
@@ -16,6 +22,12 @@ const { setMetrics } = require('../util/metrics');
1622
const AlertManager = require('../util/AlertManager');
1723
const Instrumentation = require('../util/Instrumentation');
1824
const ErrorHandler = require('../util/ErrorHandler');
25+
const CustomRequestHandler = require('./CustomRequestHandler');
26+
const responseHeaders = {
27+
'content-type': 'application/json; charset=utf-8',
28+
accept: 'application/json',
29+
'WWW-Authenticate': 'Basic realm="WS Reconnect Proxy"',
30+
}
1931

2032
/**
2133
* Proxy is the entrypoint and instantiates the context among the socket connection.
@@ -64,9 +76,58 @@ class Proxy {
6476
headers: request.headers,
6577
};
6678
logger.info(`${HTTPLOG} Received http request ${options}`);
67-
if(request.url.indexOf('/status') > -1){
68-
response.writeHead(200, {'content-type': 'application/json; charset=utf-8', 'accept': 'application/json', 'WWW-Authenticate': 'Basic realm="WS Reconnect Proxy"'});
69-
response.end(JSON.stringify({"status" : "Running"}));
79+
if (request.url.indexOf('/status') > -1) {
80+
response.writeHead(200, responseHeaders);
81+
response.end(JSON.stringify({ status: 'Running' }));
82+
return;
83+
} else if (
84+
customRequestEnabled &&
85+
request.url.indexOf('/customRequest') > -1 &&
86+
request.method == 'POST'
87+
) {
88+
try {
89+
logger.info(`Handling request to execute custom command in server`);
90+
let body = '';
91+
92+
// Read data from the request
93+
request.on('data', (chunk) => {
94+
body += chunk.toString(); // Convert Buffer to string
95+
});
96+
97+
// When the request ends, process the received data
98+
request.on('end', () => {
99+
body = JSON.parse(body);
100+
const command = body.command;
101+
const commandId = body.command.id;
102+
//Create singleton object and map the command id with pending promise
103+
const customReqInstance = CustomRequestHandler.getInstance();
104+
customReqInstance.addCustomRequest(commandId);
105+
106+
//Send to playwright server
107+
const sessionId = [...this.contexts.keys()][0];
108+
const sessionContext = this.contexts.get(sessionId);
109+
sessionContext.outgoingSocket.send(JSON.stringify(command));
110+
111+
//Get the resolved promise and returning it to end user
112+
customReqInstance.customRequestList[commandId].promise
113+
.then((result) => {
114+
delete customReqInstance.customRequestList[commandId];
115+
response.writeHead(200, responseHeaders);
116+
response.end(
117+
JSON.stringify({ status: 'success', value: result })
118+
);
119+
})
120+
.catch((err) => {
121+
delete customReqInstance.customRequestList[commandId];
122+
response.writeHead(500, responseHeaders);
123+
response.end(JSON.stringify({ status: 'failure', value: err }));
124+
});
125+
});
126+
} catch (err) {
127+
logger.error(`Error while handling custom request ${err}`);
128+
response.writeHead(500, responseHeaders);
129+
response.end(JSON.stringify({ status: 'failure', value: err.message }));
130+
}
70131
return;
71132
}
72133
const proxyReq = http.request(options, (proxyResponse) => {
@@ -75,13 +136,14 @@ class Proxy {
75136
end: true,
76137
});
77138
});
78-
proxyReq.on('error', (error)=>{
139+
140+
proxyReq.on('error', (error) => {
79141
logger.error(`${request.url} received error ${error}`);
80142
});
81-
proxyReq.on('timeout', ()=>{
143+
proxyReq.on('timeout', () => {
82144
logger.info(`${request.url} timed out`);
83145
});
84-
proxyReq.on('drain', ()=>{
146+
proxyReq.on('drain', () => {
85147
logger.info(`${request.url} drained out`);
86148
});
87149
request.pipe(proxyReq, {

0 commit comments

Comments
 (0)