Here is my websocket code:
#WebSocket
public class SocketServer {
BetterLogger logger = new BetterLogger(Main.logger) {{
loggerName = "socket-server";
}};
public static class ConnectionInit {
String tty;
String device;
}
static class ConnectionOpenResponse {
String tty;
}
#OnWebSocketConnect
public void onConnect(Session user) throws Exception {
Main.logger.info("Websocket connected!");
}
#OnWebSocketClose
public void onClose(Session user, int statusCode, String reason) {
Main.logger.info("Websocket disconnected!");
Main.sessions.closeSession(user);
}
#OnWebSocketMessage
public void onMessage(Session user, String message) {
Main.logger.info("Recevied websock connection: " + message);
try {
if (Main.sessions.contains(user)) {
// treat as raw buffer
Main.sessions.write(user, message);
}
else {
ConnectionInit heartbeat = Main.gson.fromJson(message, ConnectionInit.class);
String s = Main.config.getTTY(heartbeat.device + '.' + heartbeat.tty);
// make sure that the tty exists in config
if (s == null)
user.close(400, "Invalid device/tty!");
// get UUID
Main.sessions.newSession(s, user);
logger.info("created session (" + user + ") with dev " + heartbeat.device + ":" + heartbeat.tty);
user.getRemote().sendString(Main.gson.toJson(new ConnectionOpenResponse() {{
tty = s;
}}, ConnectionOpenResponse.class));
}
}
catch (SessionException e ) {
user.close(500, e.getMessage());
}
catch (JsonSyntaxException e) {
user.close(400, e.getMessage());
}
catch (IOException e) {
// wtf
}
}
}
I have registered it correctly in my Main class, and all seems to work well when I attempt to connect to it using websocat, I can send data and all works well. However, as soon as I create a webpage, the websocket never even opens:
console.log("ws://" + location.hostname + ":" + location.port + "/device");
const socket = new WebSocket("ws://" + location.hostname + ":" + location.port + "/device");
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Press enter to re-flush buffers\n');
//while(socket.readyState !== 1);
//console.log("connected!");
socket.send("testdata");
term.onData( (data) => {
console.log(data);
if (data.charCodeAt(0) == 13)
socket.send('\n');
socket.send(data);
});
// Listen for messages
socket.addEventListener('message', function (event) {
term.write(event.data);
});
Now, the code prints out the correct URL (ws://localhost:16838/device), however, it is stuck at CONNECTING, and throws the following error (which I expect because it's still connecting):
Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.
Looking at my server logs, I can see that it does print Websocket connected!, which doesn't even make any sense. Any help with fixing why my JS websocket client gets stuck?
Turns out you need to add the open event or else it doesnt actually open
Related
Question:
Why am I being able to send messages to all users but not to a specific user? Am I missing something obvious here?
Problem:
My POC uses the following resources:
The official Spring Boot tutorial for sending messages to all users
This Baeldung tutorial for sending messages to specific users
I am currently using two endpoints:
The first one sends messages to all users and works fine.
Send to all users - working
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public StompResponse greeting(StompRequest message) throws Exception {
Thread.sleep(1000); // simulated delay
return new StompResponse("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
The second one doesn't work. The method sendSpecific never gets called and I'm stuck trying to understand why.
Send to specific user - NOT working
/**
* Example of sending message to specific user using 'convertAndSendToUser()' and '/queue'
*/
#MessageMapping(Constants.SECURED_CHAT_ROOM)
public void sendSpecific(#Payload StompRequest message, Principal user, #Header("simpSessionId") String sessionId) throws Exception {
System.out.println("METHOD CALLED"); // This method never gets called
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
StompResponse out = new StompResponse(HtmlUtils.htmlEscape(message.getName()));
simpMessagingTemplate.convertAndSendToUser("hardcoded_username", Constants.SECURED_CHAT_SPECIFIC_USER, out, headerAccessor.getMessageHeaders());
}
Socket configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", Constants.SECURED_CHAT_SPECIFIC_USER);
config.setUserDestinationPrefix("/secured/user");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connection-uri").withSockJS();
registry.addEndpoint(SECURED_CHAT_ROOM).withSockJS();
}
}
Client code:
const SocketConnection = function(l) {
const listener = l;
let sessionId;
const parseSessionId = (url) => {
console.log(stompClient.ws._transport.url);
url = url.replace("ws://localhost:8080" + Uri.CONNECTION_URI + "/", "");
url = url.replace("/websocket", "");
url = url.replace(/^[0-9]+\//, "");
console.log("Your current session is: " + url);
return url;
};
this.connect = () => {
const socket = new SockJS(Uri.CONNECTION_URI);
stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
listener.onSocketConnect(true);
console.log('Connected: ' + frame);
stompClient.subscribe(Uri.ALL_USERS, function (greeting) {
listener.onResponseReceived(JSON.parse(greeting.body).content);
});
// WIP
const url = stompClient.ws._transport.url
sessionId = parseSessionId(url);
console.log('SUBSCRIBING FOR SPECIFIC USER: ', Uri.SECURED_CHAT_SPECIFIC_USER)
// stompClient.subscribe(Uri.SECURED_CHAT_SPECIFIC_USER + sessionId, function (msgOut) {
stompClient.subscribe(Uri.SECURED_CHAT_SPECIFIC_USER + "-user" + sessionId, function (msgOut) {
// that.messageOut(JSON.parse(msgOut.body), opts);
console.log('SINGLE USER WORKS!!!!!!!',JSON.parse(msgOut.body))
});
});
}
this.disconnect = () => {
if (stompClient !== null) {
stompClient.disconnect();
}
listener.onSocketConnect(false);
console.log("Disconnected");
}
this.sendRequest = (uri) => {
stompClient.send(uri, {}, JSON.stringify({'name': $("#name").val()}));
}
};
Client initialization:
$(function () {
const socketClient = new SocketConnection(new FormUI());
$("form").on('submit', function (e) {
e.preventDefault();
});
$( "#connect" ).click(function() { socketClient.connect(); });
$( "#disconnect" ).click(function() { socketClient.disconnect(); });
$( "#send" ).click(function() {
socketClient.sendRequest(Uri.ALL_USERS);
socketClient.sendRequest(Uri.SECURED_CHAT_ROOM);
});
});
I implemented the WKScriptMessageHandler protocol and I have defined the userContentController(:didReceiveScriptMessage:) method.
When there is a error in the Javascript, I get (in the WKScriptMessage object) something not really useful like:
{
col = 0;
file = "";
line = 0;
message = "Script error.";
type = error;
}
On the other hand, if I open the Safari Web Inspector, I can see the real error which is (for instance):
TypeError: FW.Ui.Modal.sho is not a function. (In 'FW.Ui.Modal.sho', 'FW.Ui.Modal.sho' is undefined)
Is there a way to get that error back in my native code?
EDIT:
Just to clarify, the javascript code is written by a javascript developer (who doesn't have access to the native source code, so he can't debug the app via Xcode). The code he writes it's then pushed to the iOS app (downloaded from an enterprise app store).
You can wrap the expression in a try catch block before evaluating it. Then have the JavaScript return the error message if it fails. Here is an example taken from the Turbolinks-iOS adapter, available on GitHub.
extension WKWebView {
func callJavaScriptFunction(functionExpression: String, withArguments arguments: [AnyObject?] = [], completionHandler: ((AnyObject?) -> ())? = nil) {
guard let script = scriptForCallingJavaScriptFunction(functionExpression, withArguments: arguments) else {
NSLog("Error encoding arguments for JavaScript function `%#'", functionExpression)
return
}
evaluateJavaScript(script) { (result, error) in
if let result = result as? [String: AnyObject] {
if let error = result["error"] as? String, stack = result["stack"] as? String {
NSLog("Error evaluating JavaScript function `%#': %#\n%#", functionExpression, error, stack)
} else {
completionHandler?(result["value"])
}
} else if let error = error {
self.delegate?.webView(self, didFailJavaScriptEvaluationWithError: error)
}
}
}
private func scriptForCallingJavaScriptFunction(functionExpression: String, withArguments arguments: [AnyObject?]) -> String? {
guard let encodedArguments = encodeJavaScriptArguments(arguments) else { return nil }
return
"(function(result) {\n" +
" try {\n" +
" result.value = " + functionExpression + "(" + encodedArguments + ")\n" +
" } catch (error) {\n" +
" result.error = error.toString()\n" +
" result.stack = error.stack\n" +
" }\n" +
" return result\n" +
"})({})"
}
private func encodeJavaScriptArguments(arguments: [AnyObject?]) -> String? {
let arguments = arguments.map { $0 == nil ? NSNull() : $0! }
if let data = try? NSJSONSerialization.dataWithJSONObject(arguments, options: []),
string = NSString(data: data, encoding: NSUTF8StringEncoding) as? String {
return string[string.startIndex.successor() ..< string.endIndex.predecessor()]
}
return nil
}
}
I am using stompit STOMP client. github - https://github.com/gdaws/node-stomp.
I am using ConnectFailover API for reconnect management. I have below code:
var stompit = require('stompit')
var reconnectOptions = {
'maxReconnects': 100,
'randomize' : false
};
var connManager = new stompit.ConnectFailover("failover:(stomp://mqbroker.nyc:61613,stomp://failovermqbroker.nyc:61613)", reconnectOptions);
connManager.on('error', function(error) {
var connectArgs = error.connectArgs;
var address = connectArgs.host + ':' + connectArgs.port;
console.error('Could not connect to ' + address + ' : ' + error.message);
});
connManager.on('connecting', function(connector) {
var address = connector.serverProperties.remoteAddress.transportPath;
console.log('Connecting to ' + address);
});
var totalMsgs = 50;
var count = 0;
var delayMs = 10000;
connManager.connect(function(error, client, reconnect) {
if (error) {
console.log("terminal error, given up reconnecting: " + error);
return;
}
client.on('error', function(error) {
// destroy the current client
client.destroy(error);
// calling reconnect is optional and you may not want to reconnect if the
// same error will be repeated.
reconnect();
});
var sendParams = {
'destination' : '/queue/myqueue',
'persistent' : 'true'
}
function sendMsg (){
setTimeout( function () {
console.log ('sending message ' + (count));
client.send(sendParams).end('Hello number ' + (count));
if (count++ < totalMsgs) {
sendMsg(count);
}
else {
client.send(sendParams).end('DISCONNECT');
client.disconnect();
console.log("Done.");
}
}, delayMs);
}
sendMsg();
});
The problem is that When the client gets disconnected from message broker, The producer keeps executing the sendMsg code and this causes loss of 2-3 messages in between. I want the client to stop executing when in disconnected state and resume when it is connected to failover instance.
Am I using the API incorrectly? What will be correct way to achieve this?
Have hacked at it for some time but this API lacks little documentation on how to use the features. Appreciate all the help.
Thanks,
xabhi
There is no problem with API but the with setTimeout code. I should clear the timeout when the client sees a connection failure.
So I have an AngularJS(1) app which essentially runs a server side script when a user clicks a button.
client side controller Angular calls this method when a button is clicked:
$scope.executeDeployScript = function (demo) {
console.log("Starting "+ demo.instanceName);
$scope.clearDeploymentStatus();
$scope.processStarted = true;
$http.get('/api/demoSelect/startDemo', {
params: {
name: demo.instanceName,
path: demo.scriptPath
}
}).success(function (data) {
if (data === "Done") {
$http.get("/api/demoSelect/startDemo/ip", {
params: {
name: demo.instanceName
}
}).success(function (data) {
demo.url = data.replace(/\s/g, '') + ":" + demo.port
$scope.deploymentStatus = data.replace(/\s/g, '') + ":" + demo.port;
$scope.deploymentSuccess = true;
});
} else {
$scope.deploymentStatus = "Demo deployed failed. Try again : " + data;
$scope.deploymentSuccess = false;
}
});
server side route:
app.get("/api/demoSelect/startDemo", function (req, res, next) {
var child;
var demoName = req.query.name;
var demoPath = req.query.path;
var response = "";
console.log("Running boot script for "+ demoName);
child = exec(demoPath + " " + demoName,
function (error, stdout, stderr) {
if (error !== null) {
console.log('exec error!: ' + stderr);
res.send(stderr);
} else {
console.log(stdout);
res.send("Done");
}
});
});
The script takes a long time to complete (a couple minutes) and I noticed that the GET request is being repeated after a couple of minutes which in turn interrupts the currently running script and starts it again. The repeat is only happening server side since none of the angular client side code is being executed again. I am noticing the get request being repeated in the gulp debug output
Is there some time of time-out value I need to adjust for long get requests to prevent them from being repeated? If so, is that something that's handled in Node or Angular?
Any help is appreciated
I'm developing a game on cordova that uses facebook integration. I have a facebook game canvas running on a secure site.
The friend request works fine on the web site version (returns more than 25 results, as I'm iterating the paging.next url that is also returned).
However, on the cordova build (android) it only ever returns the first result set of 25. It does still have the page.next url JSON field but it just returns a response object with a type=website.
Has anyone else come across this?
After quite a lot of digging I found an issue with the way requests are handled in the FacebookLib for Android. The current version of the com.phonegap.plugins.facebookconnect plugin uses Android FacebookSDK 3.21.1 so I'm not sure if this will still be an issue with v4.
A graph result with a paging url is used to request the next page however using the entire url, which includes the https://graph.facebook.com/ as well as the usual graphAction causes an incorrect result set to be returned. However I determined that if you remove the schema and host parts it will be correct.
I modified the ConnectPlugin.java to check that any schema and host is removed from the graphAction. Seems to work well now.
ConnectPlugin.java before:
private void makeGraphCall() {
Session session = Session.getActiveSession();
Request.Callback graphCallback = new Request.Callback() {
#Override
public void onCompleted(Response response) {
if (graphContext != null) {
if (response.getError() != null) {
graphContext.error(getFacebookRequestErrorResponse(response.getError()));
} else {
GraphObject graphObject = response.getGraphObject();
JSONObject innerObject = graphObject.getInnerJSONObject();
graphContext.success(innerObject);
}
graphPath = null;
graphContext = null;
}
}
};
//If you're using the paging URLs they will be URLEncoded, let's decode them.
try {
graphPath = URLDecoder.decode(graphPath, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] urlParts = graphPath.split("\\?");
String graphAction = urlParts[0];
Request graphRequest = Request.newGraphPathRequest(null, graphAction, graphCallback);
Bundle params = graphRequest.getParameters();
if (urlParts.length > 1) {
String[] queries = urlParts[1].split("&");
for (String query : queries) {
int splitPoint = query.indexOf("=");
if (splitPoint > 0) {
String key = query.substring(0, splitPoint);
String value = query.substring(splitPoint + 1, query.length());
params.putString(key, value);
if (key.equals("access_token")) {
if (value.equals(session.getAccessToken())) {
Log.d(TAG, "access_token URL: " + value);
Log.d(TAG, "access_token SESSION: " + session.getAccessToken());
}
}
}
}
}
params.putString("access_token", session.getAccessToken());
graphRequest.setParameters(params);
graphRequest.executeAsync();
}
ConnectPlugin.java after:
private void makeGraphCall() {
Session session = Session.getActiveSession();
Request.Callback graphCallback = new Request.Callback() {
#Override
public void onCompleted(Response response) {
if (graphContext != null) {
if (response.getError() != null) {
graphContext.error(getFacebookRequestErrorResponse(response.getError()));
} else {
GraphObject graphObject = response.getGraphObject();
JSONObject innerObject = graphObject.getInnerJSONObject();
graphContext.success(innerObject);
}
graphPath = null;
graphContext = null;
}
}
};
//If you're using the paging URLs they will be URLEncoded, let's decode them.
try {
graphPath = URLDecoder.decode(graphPath, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] urlParts = graphPath.split("\\?");
String graphAction = urlParts[0];
///////////////////////
// SECTION ADDED
///////////////////////
final String GRAPH_BASE_URL = "https://graph.facebook.com/";
if(graphAction.indexOf(GRAPH_BASE_URL)==0) {
URL graphUrl = null;
try {
graphUrl = new URL(graphAction);
} catch (MalformedURLException e) {
e.printStackTrace();
}
graphAction = graphUrl.getPath();
}
///////////////////////
// END SECTION ADDED
///////////////////////
Request graphRequest = Request.newGraphPathRequest(null, graphAction, graphCallback);
Bundle params = graphRequest.getParameters();
if (urlParts.length > 1) {
String[] queries = urlParts[1].split("&");
for (String query : queries) {
int splitPoint = query.indexOf("=");
if (splitPoint > 0) {
String key = query.substring(0, splitPoint);
String value = query.substring(splitPoint + 1, query.length());
params.putString(key, value);
if (key.equals("access_token")) {
if (value.equals(session.getAccessToken())) {
Log.d(TAG, "access_token URL: " + value);
Log.d(TAG, "access_token SESSION: " + session.getAccessToken());
}
}
}
}
}
params.putString("access_token", session.getAccessToken());
graphRequest.setParameters(params);
graphRequest.executeAsync();
}
There's no way to know that you call their api from cordova vs website, so it's some problem on your side, maybe you use some different implementation of the api on corodva and website, so that cordova sends a pagination request or send to other api version which does pagination.