I'm trying to stream back to browser the results of a ping command as they happen... I'd really like that the user could see the command running instead of just seeing the final result. Here's what I have (it's Groovy BTW, not that it matters) :)
@Controller
class IntranetWebsocketController {
@MessageMapping("/ping")
@SendToUser(destinations = "/topic/ping", broadcast = false)
static ResponseEntity (Map<String, String> address) {
def builder = new ProcessBuilder("/bin/ping", "-c", "10", "-s", "1400", "-W", "200", "-i", "0.2", "-D", "-O", address.get("ip"))
builder.redirectErrorStream(true)
def process = builder.start()
ResponseEntity.ok().body(new InputStreamResource(process.inputStream))
}
}
But then I get this:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.lang.UNIXProcess$ProcessPipeInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.http.ResponseEntity["body"]->org.springframework.core.io.InputStreamResource["inputStream"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:275) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.SerializerProvider.mappingException(SerializerProvider.java:1109) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.SerializerProvider.reportMappingProblem(SerializerProvider.java:1134) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:69) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:32) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:693) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:690) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:693) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:690) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:292) ~[jackson-databind-2.8.1.jar:2.8.1]
at com.fasterxml.jackson.databind.ObjectMapper.writeValue(ObjectMapper.java:2484) ~[jackson-databind-2.8.1.jar:2.8.1]
at org.springframework.messaging.converter.MappingJackson2MessageConverter.convertToInternal(MappingJackson2MessageConverter.java:240) ~[spring-messaging-4.3.2.RELEASE.jar:4.3.2.RELEASE]
... 17 common frames omitted
Any ideas on how can I solve this? Obviously it's a mapping problem but I have no clue on how I can deal with this. Is there a way to tell the mapper to just send everything as a String maybe?
[EDIT 1]
Based on what benjamin.d said I changed the code a bit to the following:
@Controller
class IntranetWebsocketController {
@Autowired
SimpMessagingTemplate template
@MessageMapping("/ping")
void pingSend (Map<String, String> ipaddress, Principal principal) {
def builder = new ProcessBuilder("/bin/ping", "-c", "10", "-s", "1400", "-W", "200", "-i", "0.2", "-D", "-O", ipaddress.get("ip"))
builder.redirectErrorStream(true)
def process = builder.start()
def inputReader = new InputStreamReader(process.inputStream)
def bufferedReader = new BufferedReader(inputReader)
def line
while ((line = bufferedReader.readLine()) != null) {
template.convertAndSendToUser(principal.name, "/topic/ping", line)
}
}
}
The only problem now is that if I have multiple tabs opened listening to this websocket, they all will react when the data is received since I removed the @SendToUser annotation. But that is a different matter.
[UPDATE] To those interested in this topic, here is what I ended up doing:
@Service
class ThreadedPinger {
@Async
Future<Process> run(SimpMessagingTemplate template, Principal principal, String hash, String ip) {
def builder = new ProcessBuilder("/usr/bin/sudo", "/bin/ping", "-c", "500", "-s", "1400", "-W", "200", "-i", "0.001", "-D", "-O", ip)
builder.redirectErrorStream(true)
def process = builder.start()
def bufferedReader = new BufferedReader(new InputStreamReader(process.inputStream))
def line
while ((line = bufferedReader.readLine()) != null) {
if (Thread.interrupted()) {
process.destroyForcibly()
break
}
template.convertAndSendToUser(principal.name, "/topic/ping", read)
}
bufferedReader.close()
new AsyncResult<Process>(process)
}
}
@Controller
class IntranetWebsocketController {
@Autowired
SimpMessagingTemplate template
@Autowired
ThreadedPinger threadedPinger
@MessageMapping("/ping")
void startPing(Map<String, String> request, Principal principal, SimpMessageHeaderAccessor headerAccessor) {
def pinger = threadedPinger.run(template, principal, request.get("hash"), request.get("ip"))
headerAccessor.sessionAttributes.put("pinger", pinger)
}
@MessageMapping("/pingclose")
static void closePing(SimpMessageHeaderAccessor headerAccessor) {
def pinger = (Future) headerAccessor.sessionAttributes.get("pinger")
pinger.cancel(true)
}
}
I added a "/pingclose" message mapping so the browser can kill the thread on the server side. I had a little trouble doing that since I couldn't reference the "pinger" variable between requests, so I added it to the session.
Jackson will look for getters and annotated methods for serialization. In your case, it cannot find anything, and by default it will fail for empty beans.
You can disable this feature using:
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
This code however will still not work, as Jackson will not be aware that new data is available for serialization. Instead you will have to launch a thread that will read in the process input stream (using the read method on InputStream). The read method will block execution until new data is available. Once you get data from InputStream.read(), then you push it back to your websocket clients.