firebase-tools icon indicating copy to clipboard operation
firebase-tools copied to clipboard

Allow setting webSocketPort on the firestore emulator

Open dannycoates opened this issue 2 years ago • 11 comments

[REQUIRED] Environment info

firebase-tools: 9.20.0

Platform: Linux

[REQUIRED] Test case

The tools-ui seems to handle config for the firestore websocket https://github.com/firebase/firebase-tools-ui/blob/f1178c0d8fa1f35cc978f5615eccc0ada98804f3/src/store/config/types.ts#L25-L28

The firebase-tools schema doesn't support it: https://github.com/firebase/firebase-tools/blob/master/schema/firebase-config.json

[REQUIRED] Steps to reproduce

  1. have a firebase.json that includes the firestore emulator and ui
  2. firebase emulators:start

[REQUIRED] Expected behavior

I should be able to configure which port the firestore emulator websocket server uses in firebase.json.

[REQUIRED] Actual behavior

firestore emulator starts with a randomly assigned websocket port

dannycoates avatar Oct 15 '21 22:10 dannycoates

As a workaround, I think you can directly start the firestore with the --webchannel_port flag

Usage: cloud-firestore-emulator [options] Options: --functions_emulator The HTTP host and port of the Functions Emulator. (e.g. localhost:9002) --help Print usage and exit Default: false --host The address to bind to on the local machine Default: localhost --licenses Print open-source dependencies and licenses, then exit Default: false --port The port number to listen to on the local machine Default: 8080 --rules Cloud Firestore rules file --seed_from_export Local filesystem path to a Cloud Firestore export (e.g., '/home/foo/firestore_export/2019-10-22T19:41:32_89121.overall_export_metadata') --version Print version and exit Default: false --webchannel_port The port number to bind for WebChannel traffic

You can also fork the tool repo directly and add your new config entries.

UPDATE: Seems to set a webchannel port but does not affect the ui tools websocket portt

cchatfield avatar Oct 27 '21 15:10 cchatfield

The --webchannel_port does not set the port the websocket listens on, it's another service of the emulator (I believe it's a gRPC server?).

To force the websocket port, your best bet for now is to patch the emulator. Fortunately, the Java archive is not obfuscated so JADX is able to recover the source code for the part we're interested in. Here's how you can do it (I'm hardcoding port 8093, you can change it on the line + WebSocketServer websocket = new WebSocketServer(...).

# Get the latest emulator from https://github.com/firebase/firebase-tools/blob/master/src/emulator/downloadableEmulators.ts#L41
wget https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.13.1.jar
# Extract the class initializing the websocket
mkdir classes
unzip cloud-firestore-emulator-v1.13.1.jar com/google/cloud/datastore/emulator/firestore/CloudFirestore\* -d classes
# Decompile the class to plain java with JADX
mkdir java
jadx -ds java classes/com/google/cloud/datastore/emulator/firestore/*
# Patch it!
cat <<EOF >firestore-emulator-v1.13.1.patch
diff -ur java/com/google/cloud/datastore/emulator/firestore/CloudFirestore.java java-patched/com/google/cloud/datastore/emulator/firestore/CloudFirestore.java
--- java/com/google/cloud/datastore/emulator/firestore/CloudFirestore.java  2021-11-08 16:21:43.000000000 +0100
+++ java-patched/com/google/cloud/datastore/emulator/firestore/CloudFirestore.java  2021-11-08 16:22:49.000000000 +0100
@@ -9,7 +9,7 @@
 import com.google.cloud.datastore.emulator.impl.ExportImportUtil;
 import com.google.cloud.datastore.emulator.impl.FirestoreEmulatorConfig;
 import com.google.cloud.datastore.emulator.impl.events.FunctionsEmulatorEventPublisher;
-import com.google.common.annotations.VisibleForTesting;
+// import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.Files;
 import com.google.common.net.HostAndPort;
@@ -65,7 +65,7 @@
         if (opts.seedFromExport != null) {
             config.seedData(ExportImportUtil.fetchEntities(new java.io.File(opts.seedFromExport), ImmutableList.of()));
         }
-        WebSocketServer websocket = new WebSocketServer(opts.host, findOpenPort(), config.build().timeSource());
+        WebSocketServer websocket = new WebSocketServer(opts.host, 8093, config.build().timeSource());
         config.requestsListener(websocket.getRequestsListener());
         FirestoreEmulator firestore = FirestoreEmulator.fromConfig(config.build());
         GrpcServer server = buildGrpcServer(opts.host, opts.port, firestore);
@@ -88,7 +88,7 @@
         webchannel.shutdown();
     }

-    @VisibleForTesting
+    // @VisibleForTesting
     public static GrpcServer buildGrpcServer(String hostname, int port, FirestoreEmulator emulator) throws IOException {
         GrpcServer server = new GrpcServer(hostname, port, ImmutableList.of(ServerInterceptors.intercept(FirestoreV1GrpcAdapter.build(emulator), new ServerInterceptor[]{new FirebaseAuthInterceptor(), new FirebaseDatabaseRefInterceptor()}), ServerInterceptors.intercept(FirestoreV1Beta1GrpcAdapter.build(emulator), new ServerInterceptor[]{new FirebaseAuthInterceptor(), new FirebaseDatabaseRefInterceptor()}), ServerInterceptors.intercept(EmulatorAuxiliaryGrpcAdapter.build(emulator), new ServerInterceptor[]{new FirebaseAuthInterceptor(), new FirebaseDatabaseRefInterceptor()})));
         ManagedChannel channel = NettyChannelBuilder.forAddress(hostname, port).usePlaintext().maxInboundMessageSize(Integer.MAX_VALUE).build();
@@ -115,8 +115,8 @@
             return localPort;
         } catch (Throwable th) {
             th.addSuppressed(th);
+            throw th;
         }
-        throw th;
     }

     /* access modifiers changed from: private */
EOF
cd java
patch -p1 -i ../firestore-emulator-v1.13.1.patch
cd ..
# Compile back the java class
javac -cp ./cloud-firestore-emulator-v1.13.1.jar java/com/google/cloud/datastore/emulator/firestore/CloudFirestore.java
# Put it back in the jar file
cd java
zip ../cloud-firestore-emulator-v1.13.1.jar com/google/cloud/datastore/emulator/firestore/*.class
# Done!
# java -jar cloud-firestore-emulator-v1.13.1.jar

⚠️ If you run your emulator in a custom environment (e.g. Docker), your Java version might differ from the one you've compiled your class on. Make sure to run javac in the same environment where you'll run the emulator.

To get the firebase CLI to use your custom JAR file, you'll have to host it somewhere then patch downloadableEmulators.ts - which is bundled as downloadableEmulators.js. Here's a small Node.JS script doing it:

const fs = require("fs"),
  path = "/usr/local/lib/node_modules/firebase-tools/lib/emulator/downloadableEmulators.js",
  r = /(remoteUrl:\s*")[a-z:\/_.-]+\/cloud-firestore-emulator-[v0-9.]+\.jar(",\s*expectedSize: )\d+(,\s*expectedChecksum:\s*")[a-f0-9]+(")/,
  originalData = fs.readFileSync(path).toString(),
  url = "<URL OF YOUR JAR FILE>",
  size = <SIZE OF YOUR JAR FILE IN BYTES>,
  checksum = "<MD5 CHECKSUM OF YOUR FILE>",
  patchedData = originalData.replace(r, (_,a,b,c,d) => a + url + b + size + c + checksum + d);
assert(patchedData !== originalData);
fs.writeFileSync(path, patchedData);

... I tried to do it with Sed or Perl but we need Node.JS to run the Firebase CLI anyway 😄

TPXP avatar Nov 08 '21 17:11 TPXP

Hi, this is Yuchen from the Emulator Suite team. First things first, the --webchannel_port flag is an implementation detail for the Firestore Emulator. And as both of you observed, WebChannel has nothing to do with WebSocket. We do not recommend setting that flag and it will not solve OP's problem.

For context, the Firestore Emulator serves normal traffic on the main port (configurable via port in firebase.json), but also exposes an auxiliary WebSocket server that exposes requests and rules evaluations for the Requests Monitor feature in Emulator UI. For the latter, a random available port is picked to minimize the chance of conflicts. If I understand correctly, the feature request from OP is to make that port configurable. We haven't thought about that yet since the Emulator UI discovers the auxiliary port through an discovery API call to the normal port and there's no need for configuration in most use cases. @dannycoates would you share some background on why you'd like such a feature? This will help us prioritize.


Depending on your exact use cases, there may be other workarounds too. For example, you may get the WebSocket port by using the discovery API at http://$FIRESTORE_EMULATOR_HOST/ws/discovery:

$ curl http://127.0.0.1:8080/ws/discovery
{"url": "ws://127.0.0.1:55885"}

The Firestore Emulator also prints the port picked on start, as a log line like INFO: Started WebSocket server on ws://127.0.0.1:55885. When launched from the Firebase CLI, this line ends up in firestore-debug.log, which can be extracted with a simple grep command for scripting use cases.

Please let us know if none of these workarounds work for you and we'll see what we can do.

yuchenshi avatar Nov 08 '21 21:11 yuchenshi

While not the original poster, I am using the emulator in a container hosted in k8s. I am also using port forwarding to expose the web ui. I restart the emulator several times throughout a dev day and this assigns another random port. Constantly changing the exposed port on the k8s deployment and the tunnel is not really tenable with dozens of restarts.

On Mon, Nov 8, 2021 at 2:51 PM Yuchen Shi @.***> wrote:

Hi, this is Yuchen from the Emulator Suite team. First things first, the --webchannel_port flag is an implementation detail for the Firestore Emulator. And as both of you observed, WebChannel has nothing to do with WebSocket. We do not recommend setting that flag and it will not solve OP's problem.

For context, the Firestore Emulator serves normal traffic on the main port (configurable via port in firebase.json), but also exposes an auxiliary WebSocket server that exposes requests and rules evaluations for the Requests Monitor feature in Emulator UI. For the latter, a random available port is picked to minimize the chance of conflicts. If I understand correctly, the feature request from OP is to make that port configurable. We haven't thought about that yet since the Emulator UI discovers the auxiliary port through an discovery API call to the normal port and there's no need for configuration in most use cases. @dannycoates https://github.com/dannycoates would you share some background on why you'd like such a feature? This will help us prioritize.

Depending on your exact use cases, there may be other workarounds too. For example, you may get the WebSocket port by using the discovery API at http://$FIRESTORE_EMULATOR_HOST/ws/discovery:

$ curl http://127.0.0.1:8080/ws/discovery {"url": "ws://127.0.0.1:55885"}

The Firestore Emulator also prints the port picked on start, as a log line like INFO: Started WebSocket server on ws://127.0.0.1:55885. When launched from the Firebase CLI, this line ends up in firestore-debug.log, which can be extracted with a simple grep command for scripting use cases.

Please let us know if none of these workarounds work for you and we'll see what we can do.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/firebase/firebase-tools/issues/3837#issuecomment-963606823, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEKBOMFHDCNC7NCWOZNZ43ULBA65ANCNFSM5GC35HAQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

cchatfield avatar Nov 09 '21 00:11 cchatfield

I'm running the firebase emulator suite in a containerized environment too : I'm replicating my project stack locally with docker-compose, which allows every developer to work on a separate environment. Our app relies on Firestore but we also use other components (a Redis cache, ElasticSearch,...). Docker compose makes it easy for us to expose the services to each component. Of course, for the app to be able to connect to the firebase emulator, we expose ports from the Firebase emulator container.

Container ports cannot be changed dynamically : they are set once for all on container creation. The WebUI tries to connect to the WebSocket port, but cannot since we can't open it beforehand. Being able to set it would allow me to open the port on the container, thus making the host (on which the webui runs) able to connect to it.

With the patch I proposed above, I'm able to open port 8093 on the container running the firebase emulator and the WebUI is able to show the firestore requests history.

TPXP avatar Nov 09 '21 08:11 TPXP

@yuchenshi thanks, like cchatfield and TPXP we're running the emulator in a container and exposing the ports so in order to reach the websocket from outside we need a static port number.

dannycoates avatar Nov 17 '21 23:11 dannycoates

Thanks for the use cases and I've tracked this as a feature request. We are unable to promise any timeline for this, but if others also have this request, adding a +1 on this issue can help us prioritize adding this to the roadmap.

(Googler-only internal tracking bug: b/207011409)

yuchenshi avatar Nov 18 '21 21:11 yuchenshi

@yuchenshi I wanted to reach out to see if this was ever prioritized? I'm guessing not...

cam-carter avatar Apr 29 '22 16:04 cam-carter

+1

Resolving this issue would reduce the tedium of spinning up the emulator suite in any environment requiring the use of static port forwards for whichever reason - containers, remote dev, remote testing, anything non localhost. The fact that all the other ports are configurable sticks out like a sore thumb here.

stildalf avatar Jun 30 '22 08:06 stildalf

+1

I'm unable to see Firestore requests in the emulator UI due to this issue as I am also running the emulator inside Docker. Just like all the other emulator ports can be configured for this reason we need the ability to configure this as well. Or instead use the port specified in Firebase.json + 100, or whatever.

michaelalhilly avatar Jul 07 '22 05:07 michaelalhilly

Based on @TPXP 's instructions, I ended up building support for specifying a web socket port via environment variables into our docker container: https://hub.docker.com/repository/docker/fixl/firebase-emulator-suite

Here's the commit for those who are interested in replicating this: https://gitlab.com/fixl/docker-firebase-emulator-suite/-/commit/21eec64a0d4141514558d5b6865efeeb05f1d017

fixl avatar Jul 30 '22 08:07 fixl

+1

peterfortuin avatar Aug 19 '22 12:08 peterfortuin

+1 for containerized environments to work properly.

radyz avatar Aug 20 '22 21:08 radyz

Whooop, +10 Thank You's to all involved, will be testing this ASAP.

stildalf avatar Sep 28 '22 04:09 stildalf

Thanks for adding support for this natively.

I'm still running into issues when I try to run this inside a docker container.

With the following config:


{
  "emulators": {
    "ui": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 4000
    },
    "hosting": {
      "host": "0.0.0.0",
      "port": "5000"
    },
    "functions": {
      "host": "0.0.0.0",
      "port": "5001"
    },
    "firestore": {
      "host": "0.0.0.0",
      "port": "8081",
      "websocketPort": "54321"
    },
    "pubsub": {
      "host": "0.0.0.0",
      "port": "8085"
    },
    "database": {
      "host": "0.0.0.0",
      "port": "9000"
    },
    "auth": {
      "host": "0.0.0.0",
      "port": "9099"
    },
    "storage": {
      "host": "0.0.0.0",
      "port": "9199"
    }
  },
  "storage": {
    "rules": "/storage.rules"
  }
}

I get the following error:

firestore: Port 54321 is not open on 0.0.0.0, could not start websocket server for Firestore emulator.
firestore: To select a different port, specify that port in a firebase.json config file:
   {
     // ...
     "emulators": {
       "firestore": {
         "host": "HOST",
         ...
         "websocketPort": "WEBSOCKET_PORT"
       }
     }

To reproduce, you can clone https://gitlab.com/fixl/docker-firebase-emulator-suite/-/tree/feature/move-to-native-websocket and then run:

make build
docker compose -f docker-compose.example.yaml up

Am I missing something?

fixl avatar Oct 02 '22 06:10 fixl

@fixl we think this is a specific issue - could you open a new Issue on GH so we can track it better?

bkendall avatar Oct 03 '22 20:10 bkendall