websocket.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package websocket
  2. import (
  3. pb "github.com/OliveTin/OliveTin/gen/grpc"
  4. "github.com/OliveTin/OliveTin/internal/executor"
  5. ws "github.com/gorilla/websocket"
  6. log "github.com/sirupsen/logrus"
  7. "google.golang.org/protobuf/encoding/protojson"
  8. "net/http"
  9. )
  10. var upgrader = ws.Upgrader{}
  11. type WebsocketClient struct {
  12. conn *ws.Conn
  13. }
  14. var clients []*WebsocketClient
  15. var marshalOptions = protojson.MarshalOptions{
  16. UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
  17. EmitUnpopulated: true,
  18. }
  19. var ExecutionListener WebsocketExecutionListener
  20. type WebsocketExecutionListener struct{}
  21. func (WebsocketExecutionListener) OnExecutionStarted(title string) {
  22. /*
  23. broadcast(ExecutionStarted{
  24. Type: "ExecutionStarted",
  25. Action: title,
  26. });
  27. */
  28. }
  29. func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
  30. le := &pb.LogEntry{
  31. ActionTitle: logEntry.ActionTitle,
  32. ActionIcon: logEntry.ActionIcon,
  33. DatetimeStarted: logEntry.DatetimeStarted,
  34. DatetimeFinished: logEntry.DatetimeFinished,
  35. Stdout: logEntry.Stdout,
  36. Stderr: logEntry.Stderr,
  37. TimedOut: logEntry.TimedOut,
  38. Blocked: logEntry.Blocked,
  39. ExitCode: logEntry.ExitCode,
  40. Tags: logEntry.Tags,
  41. Uuid: logEntry.UUID,
  42. ExecutionStarted: logEntry.ExecutionStarted,
  43. ExecutionFinished: logEntry.ExecutionFinished,
  44. }
  45. broadcast("ExecutionFinished", le)
  46. }
  47. func broadcast(messageType string, pbmsg *pb.LogEntry) {
  48. payload, err := marshalOptions.Marshal(pbmsg)
  49. // <EVIL>
  50. // So, the websocket wants to encode messages using the same protomarshaller
  51. // as the REST API - this gives consistency instead of using encoding/json
  52. // and allows us to set specific marshalOptions.
  53. //
  54. // However, the protomarshaller will marshal the type, but the JavaScript at
  55. // the other end has no idea what type this object is - as we're just sending
  56. // it as JSON over the websocket.
  57. //
  58. // Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
  59. // literal and encode that string just with a byte array cast.
  60. hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
  61. hackyMessage := []byte{}
  62. hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
  63. hackyMessage = append(hackyMessage, payload...)
  64. hackyMessage = append(hackyMessage, []byte("}")...)
  65. // </EVIL>
  66. if err != nil {
  67. log.Errorf("websocket marshal error: %v", err)
  68. return
  69. }
  70. for _, client := range clients {
  71. client.conn.WriteMessage(1, hackyMessage)
  72. }
  73. }
  74. func (c *WebsocketClient) messageLoop() {
  75. for {
  76. mt, message, err := c.conn.ReadMessage()
  77. if err != nil {
  78. log.Printf("err: %v", err)
  79. break
  80. }
  81. log.Infof("websocket recv: %s %d", message, mt)
  82. }
  83. }
  84. func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
  85. c, err := upgrader.Upgrade(w, r, nil)
  86. if err != nil {
  87. log.Warnf("Websocket issue: %v", err)
  88. return false
  89. }
  90. // defer c.Close()
  91. wsclient := &WebsocketClient{
  92. conn: c,
  93. }
  94. clients = append(clients, wsclient)
  95. go wsclient.messageLoop()
  96. return true
  97. }