@efffrida/rpc 0.0.27 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/frida/FridaRpcClient.d.ts +4 -29
  2. package/dist/frida/FridaRpcClient.d.ts.map +1 -1
  3. package/dist/frida/FridaRpcClient.js +47 -83
  4. package/dist/frida/FridaRpcClient.js.map +1 -1
  5. package/dist/frida/FridaRpcServer.d.ts +7 -7
  6. package/dist/frida/FridaRpcServer.d.ts.map +1 -1
  7. package/dist/frida/FridaRpcServer.js +37 -49
  8. package/dist/frida/FridaRpcServer.js.map +1 -1
  9. package/dist/node/FridaRpcClient.d.ts +6 -10
  10. package/dist/node/FridaRpcClient.d.ts.map +1 -1
  11. package/dist/node/FridaRpcClient.js +67 -46
  12. package/dist/node/FridaRpcClient.js.map +1 -1
  13. package/dist/node/FridaRpcServer.d.ts +10 -3
  14. package/dist/node/FridaRpcServer.d.ts.map +1 -1
  15. package/dist/node/FridaRpcServer.js +63 -64
  16. package/dist/node/FridaRpcServer.js.map +1 -1
  17. package/dist/shared/{constants.d.ts → Constants.d.ts} +7 -3
  18. package/dist/shared/Constants.d.ts.map +1 -0
  19. package/dist/shared/Constants.js +10 -0
  20. package/dist/shared/Constants.js.map +1 -0
  21. package/dist/shared/index.d.ts +10 -0
  22. package/dist/shared/index.d.ts.map +1 -0
  23. package/dist/shared/index.js +10 -0
  24. package/dist/shared/index.js.map +1 -0
  25. package/package.json +12 -20
  26. package/src/frida/FridaRpcClient.ts +61 -141
  27. package/src/frida/FridaRpcServer.ts +47 -54
  28. package/src/node/FridaRpcClient.ts +84 -71
  29. package/src/node/FridaRpcServer.ts +92 -80
  30. package/src/shared/Constants.ts +13 -0
  31. package/src/shared/index.ts +10 -0
  32. package/dist/shared/constants.d.ts.map +0 -1
  33. package/dist/shared/constants.js +0 -7
  34. package/dist/shared/constants.js.map +0 -1
  35. package/dist/shared/predicates.d.ts +0 -10
  36. package/dist/shared/predicates.d.ts.map +0 -1
  37. package/dist/shared/predicates.js +0 -12
  38. package/dist/shared/predicates.js.map +0 -1
  39. package/src/shared/constants.ts +0 -11
  40. package/src/shared/predicates.ts +0 -19
@@ -6,150 +6,89 @@
6
6
  */
7
7
 
8
8
  import "@efffrida/polyfills";
9
- import type * as RpcMessage from "@effect/rpc/RpcMessage";
10
- import type * as Duration from "effect/Duration";
9
+
11
10
  import type * as Scope from "effect/Scope";
11
+ import type * as RpcMessage from "effect/unstable/rpc/RpcMessage";
12
12
 
13
- import * as RpcClient from "@effect/rpc/RpcClient";
14
- import * as RpcSerialization from "@effect/rpc/RpcSerialization";
15
- import * as FridaStream from "@efffrida/platform/Stream";
16
- import * as Cause from "effect/Cause";
17
- import * as Deferred from "effect/Deferred";
18
13
  import * as Effect from "effect/Effect";
19
14
  import * as Function from "effect/Function";
20
15
  import * as Layer from "effect/Layer";
21
- import * as Option from "effect/Option";
22
- import * as ParseResult from "effect/ParseResult";
23
- import * as Predicate from "effect/Predicate";
24
- import * as Schema from "effect/Schema";
25
- import * as Stream from "effect/Stream";
16
+ import * as Random from "effect/Random";
17
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
18
+ import * as RpcClientError from "effect/unstable/rpc/RpcClientError";
19
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
26
20
 
27
- import * as constants from "../shared/constants.ts";
28
- import * as sharedPredicates from "../shared/predicates.ts";
21
+ import * as constants from "../shared/Constants.ts";
29
22
 
30
23
  /**
31
24
  * @since 1.0.0
32
25
  * @category Protocol
33
26
  */
34
- export const makeProtocolFrida = (
35
- options?:
36
- | {
37
- readonly generateExportName?: (() => string) | undefined;
38
- readonly receivingStreamShareOptions?:
39
- | {
40
- readonly capacity: "unbounded";
41
- readonly replay?: number | undefined;
42
- readonly idleTimeToLive?: Duration.DurationInput | undefined;
43
- }
44
- | {
45
- readonly capacity: number;
46
- readonly strategy?: "sliding" | "dropping" | "suspend" | undefined;
47
- readonly replay?: number | undefined;
48
- readonly idleTimeToLive?: Duration.DurationInput | undefined;
49
- }
50
- | undefined;
51
- }
52
- | undefined
53
- ): Effect.Effect<RpcClient.Protocol["Type"], never, RpcSerialization.RpcSerialization | Scope.Scope> =>
27
+ export const makeProtocolFrida = (): Effect.Effect<
28
+ RpcClient.Protocol["Service"],
29
+ never,
30
+ RpcSerialization.RpcSerialization | Scope.Scope
31
+ > =>
54
32
  RpcClient.Protocol.make(
55
- Effect.fnUntraced(function* (writeResponse) {
33
+ Effect.fnUntraced(function* (writeResponse, clientIds) {
56
34
  const serialization = yield* RpcSerialization.RpcSerialization;
57
35
 
58
36
  const encoder = new TextEncoder();
59
- const parser = serialization.unsafeMake();
60
-
61
- /**
62
- * Once the server has acknowledged our initiation request and sent
63
- * use a client id to identify ourselves, this deferred will
64
- * complete.
65
- */
66
- const clientIdDeferred = yield* Deferred.make<number, ParseResult.ParseError>();
37
+ const parser = serialization.makeUnsafe();
38
+ const requestClientMap = new Map<string, number>();
67
39
 
68
- /**
69
- * Setup the receiving handler for the client id. We receive the
70
- * client id over a script export instead of over the receiving
71
- * stream since there could be multiple clients in this frida script
72
- * and multiple could request client ids at the same time. This
73
- * could be mitigated with a nonce of some kind in the request
74
- * message, but receiving over a script export is full-proof so long
75
- * as the exports are unique.
76
- */
77
- const exportName = options?.generateExportName?.() ?? constants.generateClientCallbackExportNameForServer();
78
- yield* Effect.addFinalizer(() => Effect.sync(() => delete rpc.exports[exportName]));
79
- rpc.exports[exportName] = (maybeIncomingClientId: unknown): void => {
80
- const parsedClientId = Schema.decodeUnknown(Schema.Number)(maybeIncomingClientId);
81
- Deferred.unsafeDone(clientIdDeferred, parsedClientId);
82
- };
83
-
84
- /**
85
- * Request a client id from the node side and wait for the response,
86
- * handling any transient errors that might occur by retrying with
87
- * the retry policy.
88
- */
40
+ const exportName = yield* Random.nextUUIDv4;
89
41
  const connectionRequest = constants.nodeRpcClientMakeConnectionRequestForServer(exportName);
90
- const connectionRequestTimeout: Duration.DurationInput = "1 second";
91
- const clientId = yield* Effect.sync(() => send(connectionRequest)).pipe(
92
- Effect.flatMap(() => Deferred.await(clientIdDeferred)),
93
- Effect.timeout(connectionRequestTimeout),
94
- Effect.catchIf(ParseResult.isParseError, () => Effect.dieMessage("Failed to parse client ID")),
95
- Effect.catchIf(Cause.isTimeoutException, () => Effect.dieMessage("Timed out too many times"))
96
- );
97
42
 
98
- /**
99
- * Attach to the receiving stream, where all future messages will
100
- * come in at, and filter for only our messages.
101
- */
102
- const receivingPredicate = sharedPredicates.isTaggedForClient(clientId);
103
- const receiveStream = yield* FridaStream.receiveStream(
104
- options?.receivingStreamShareOptions ?? {
105
- replay: 100,
106
- capacity: "unbounded",
107
- }
108
- );
43
+ const broadcast = (response: RpcMessage.FromServerEncoded) =>
44
+ Effect.forEach(clientIds, (id) => writeResponse(id, response), { discard: true });
109
45
 
110
- /**
111
- * For every response message received, decode it and send it the
112
- * response back to the implementation.
113
- */
114
- yield* receiveStream.pipe(
115
- Stream.filterMap((unfiltered) => {
116
- if (receivingPredicate(unfiltered.message)) return Option.fromNullable(unfiltered.data);
117
- else return Option.none<Uint8Array<ArrayBufferLike>>();
118
- }),
119
- Stream.runForEach((filtered) => {
120
- try {
121
- const responses = parser.decode(filtered) as Array<RpcMessage.FromServerEncoded>;
122
- if (responses.length === 0) return Effect.void;
123
- let i = 0;
124
- return Effect.whileLoop({
125
- while: () => i < responses.length,
126
- body: () => writeResponse(responses[i++]),
127
- step: Function.constVoid,
128
- });
129
- } catch (defect) {
130
- return writeResponse({ _tag: "Defect", defect });
131
- }
132
- }),
133
- Effect.interruptible,
134
- Effect.forkScoped
135
- );
46
+ yield* Effect.addFinalizer(() => Effect.sync(() => delete rpc.exports[exportName]));
47
+ rpc.exports[exportName] = (data: string | Uint8Array): Promise<void> => {
48
+ try {
49
+ const responses = parser.decode(data) as Array<RpcMessage.FromServerEncoded>;
50
+ if (responses.length === 0) return Promise.resolve();
51
+ let i = 0;
52
+ return Effect.whileLoop({
53
+ step: Function.constVoid,
54
+ while: () => i < responses.length,
55
+ body: () => {
56
+ const response = responses[i++]!;
57
+ if ("requestId" in response) {
58
+ const clientId = requestClientMap.get(response.requestId)!;
59
+ if (response._tag === "Exit") requestClientMap.delete(response.requestId);
60
+ return writeResponse(clientId, response);
61
+ } else return broadcast(response);
62
+ },
63
+ }).pipe(Effect.runPromise);
64
+ } catch (defect) {
65
+ return broadcast({
66
+ _tag: "ClientProtocolError",
67
+ error: new RpcClientError.RpcClientError({
68
+ reason: new RpcClientError.RpcClientDefect({
69
+ message: "Error decoding message",
70
+ cause: defect,
71
+ }),
72
+ }),
73
+ }).pipe(Effect.runPromise);
74
+ }
75
+ };
136
76
 
137
- /**
138
- * Sending messages is as simple as encoding them, then sending them
139
- * to the node side tagged with our client id so it knows where to
140
- * send them back to.
141
- */
142
- const sendHelper = Effect.fnUntraced(function* (message: RpcMessage.FromClientEncoded) {
143
- const encoded = parser.encode(message);
144
- if (Predicate.isUndefined(encoded)) return;
145
- const transformed = typeof encoded === "string" ? encoder.encode(encoded) : encoded;
146
- send({ clientId }, (transformed as Uint8Array<ArrayBuffer>).buffer);
147
- });
77
+ send(connectionRequest);
148
78
 
149
79
  return {
150
- send: sendHelper,
151
80
  supportsAck: true,
152
81
  supportsTransferables: false,
82
+ send(clientId, request) {
83
+ if (request._tag === "Request") {
84
+ requestClientMap.set(request.id, clientId);
85
+ }
86
+
87
+ const encoded = parser.encode(request);
88
+ if (encoded === undefined) return Effect.void;
89
+ const transformed = typeof encoded === "string" ? encoder.encode(encoded) : encoded;
90
+ return Effect.sync(() => send(exportName, (transformed as Uint8Array<ArrayBuffer>).buffer));
91
+ },
153
92
  };
154
93
  })
155
94
  );
@@ -158,24 +97,5 @@ export const makeProtocolFrida = (
158
97
  * @since 1.0.0
159
98
  * @category Layers
160
99
  */
161
- export const layerProtocolFrida = (
162
- options?:
163
- | {
164
- readonly generateExportName?: (() => string) | undefined;
165
- readonly receivingStreamShareOptions?:
166
- | {
167
- readonly capacity: "unbounded";
168
- readonly replay?: number | undefined;
169
- readonly idleTimeToLive?: Duration.DurationInput | undefined;
170
- }
171
- | {
172
- readonly capacity: number;
173
- readonly strategy?: "sliding" | "dropping" | "suspend" | undefined;
174
- readonly replay?: number | undefined;
175
- readonly idleTimeToLive?: Duration.DurationInput | undefined;
176
- }
177
- | undefined;
178
- }
179
- | undefined
180
- ): Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization> =>
181
- Layer.scoped(RpcClient.Protocol, makeProtocolFrida(options));
100
+ export const layerProtocolFrida: Layer.Layer<RpcClient.Protocol, never, RpcSerialization.RpcSerialization> =
101
+ Layer.effect(RpcClient.Protocol, makeProtocolFrida());
@@ -6,22 +6,22 @@
6
6
  */
7
7
 
8
8
  import "@efffrida/polyfills";
9
- import * as RpcMessage from "@effect/rpc/RpcMessage";
10
- import * as RpcSerialization from "@effect/rpc/RpcSerialization";
11
- import * as RpcServer from "@effect/rpc/RpcServer";
9
+
12
10
  import * as Effect from "effect/Effect";
13
11
  import * as Function from "effect/Function";
14
12
  import * as Layer from "effect/Layer";
15
- import * as Mailbox from "effect/Mailbox";
16
- import * as Predicate from "effect/Predicate";
13
+ import * as Queue from "effect/Queue";
14
+ import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
15
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
16
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
17
17
 
18
- import * as constants from "../shared/constants.ts";
18
+ import * as constants from "../shared/Constants.ts";
19
19
 
20
20
  /**
21
21
  * @since 1.0.0
22
22
  * @category Protocol
23
23
  */
24
- export const makeProtocolFrida = (
24
+ export const makeProtocolFridaNoSendRecv = (
25
25
  options?:
26
26
  | {
27
27
  /** Generates server listener rpc exports for individual clients. */
@@ -30,71 +30,66 @@ export const makeProtocolFrida = (
30
30
  | undefined
31
31
  ): Effect.Effect<
32
32
  {
33
- readonly protocol: RpcServer.Protocol["Type"];
34
- readonly rpcExport: () => Promise<number>;
33
+ readonly protocol: RpcServer.Protocol["Service"];
34
+ readonly rpcExport: () => Promise<string>;
35
35
  },
36
36
  never,
37
37
  RpcSerialization.RpcSerialization
38
38
  > =>
39
39
  Effect.gen(function* () {
40
- const encoder = new TextEncoder();
41
- const disconnects = yield* Mailbox.make<number>();
42
40
  const serialization = yield* RpcSerialization.RpcSerialization;
43
41
 
42
+ const disconnects = yield* Queue.unbounded<number>();
43
+ const parser = serialization.makeUnsafe();
44
+ const encoder = new TextEncoder();
45
+
44
46
  let clientId = 0;
45
47
  const clientIds = new Set<number>();
46
- const clients = new Map<number, { readonly write: (bytes: RpcMessage.FromServerEncoded) => void }>();
48
+ const clients = new Map<
49
+ number,
50
+ {
51
+ readonly write: (bytes: RpcMessage.FromServerEncoded) => void;
52
+ }
53
+ >();
47
54
 
48
55
  let writeRequest!: (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect<void>;
49
56
  const makeExportName = options?.generateExportName ?? constants.generateServerExportNameForClient;
50
57
 
51
- // Listen for new clients on the main rpc export
52
- const rpcExport = Effect.gen(function* () {
53
- const id = ++clientId;
54
- const parser = serialization.unsafeMake();
58
+ const writeRaw = (data: string | Uint8Array): void => {
59
+ const transformed = typeof data === "string" ? encoder.encode(data) : data;
60
+ return send(void 0, (transformed as Uint8Array<ArrayBuffer>).buffer);
61
+ };
55
62
 
56
- const writeRaw = (data: string | Uint8Array): void => {
57
- const transformed = typeof data === "string" ? encoder.encode(data) : data;
58
- return send({ clientId: id }, (transformed as Uint8Array<ArrayBuffer>).buffer);
59
- };
60
- const write = (response: RpcMessage.FromServerEncoded): void => {
61
- try {
62
- const encoded = parser.encode(
63
- // TODO: handle defects on the node client better
64
- response._tag === "Defect" && response.defect instanceof Error
65
- ? RpcMessage.ResponseDefectEncoded({
66
- stack: response.defect.stack,
67
- cause: response.defect.cause,
68
- ...response.defect,
69
- })
70
- : response
71
- );
72
- if (Predicate.isNotUndefined(encoded)) {
73
- return writeRaw(encoded);
74
- }
75
- } catch (cause) {
76
- const encoded = parser.encode(RpcMessage.ResponseDefectEncoded(cause))!;
77
- return writeRaw(encoded);
78
- }
79
- };
63
+ const write = (response: RpcMessage.FromServerEncoded): void => {
64
+ try {
65
+ const encoded = parser.encode(response);
66
+ if (encoded === undefined) return;
67
+ return writeRaw(encoded);
68
+ } catch (cause) {
69
+ const encoded = parser.encode(RpcMessage.ResponseDefectEncoded(cause))!;
70
+ return writeRaw(encoded);
71
+ }
72
+ };
80
73
 
81
- clientIds.add(id);
74
+ // @effect-diagnostics-next-line runEffectInsideEffect:off
75
+ const rpcExport = Effect.gen(function* () {
76
+ const id = ++clientId;
82
77
  clients.set(id, { write });
78
+ clientIds.add(id);
83
79
 
84
80
  const onMessage = (input: string | Record<number, string>): Effect.Effect<void, never, never> => {
85
- const data = typeof input === "string" ? input : Uint8Array.from(Object.values(input));
86
-
87
81
  try {
82
+ const data = typeof input === "string" ? input : Uint8Array.from(Object.values(input));
88
83
  const decoded = parser.decode(data) as ReadonlyArray<RpcMessage.FromClientEncoded>;
89
84
  if (decoded.length === 0) return Effect.void;
90
85
  let i = 0;
91
86
  return Effect.whileLoop({
92
87
  while: () => i < decoded.length,
88
+ step: Function.constVoid,
93
89
  body() {
94
90
  const message = decoded[i++];
95
91
  return writeRequest(id, message);
96
92
  },
97
- step: Function.constVoid,
98
93
  });
99
94
  } catch (cause) {
100
95
  return Effect.sync(() => writeRaw(parser.encode(RpcMessage.ResponseDefectEncoded(cause))!));
@@ -104,8 +99,8 @@ export const makeProtocolFrida = (
104
99
  rpc.exports[makeExportName(id)] = (data: string | Record<number, string>): Promise<void> =>
105
100
  Effect.runPromise(onMessage(data));
106
101
 
107
- return id;
108
- });
102
+ return makeExportName(id);
103
+ }).pipe((export_) => () => Effect.runPromise(export_));
109
104
 
110
105
  const protocol = yield* RpcServer.Protocol.make((writeRequest_) => {
111
106
  writeRequest = writeRequest_;
@@ -117,9 +112,6 @@ export const makeProtocolFrida = (
117
112
  return Effect.sync(() => client.write(response));
118
113
  },
119
114
  end(clientId) {
120
- clientIds.delete(clientId); // TODO: Is this required?
121
- clients.delete(clientId); // TODO: Is this required?
122
- const makeExportName = options?.generateExportName ?? constants.generateServerExportNameForClient;
123
115
  delete rpc.exports[makeExportName(clientId)];
124
116
  return Effect.void;
125
117
  },
@@ -133,7 +125,7 @@ export const makeProtocolFrida = (
133
125
 
134
126
  return {
135
127
  protocol,
136
- rpcExport: () => Effect.runPromise(rpcExport),
128
+ rpcExport,
137
129
  };
138
130
  });
139
131
 
@@ -141,7 +133,7 @@ export const makeProtocolFrida = (
141
133
  * @since 1.0.0
142
134
  * @category Protocol
143
135
  */
144
- export const makeProtocolFridaWithExport = (
136
+ export const makeProtocolFrida = (
145
137
  options?:
146
138
  | {
147
139
  /**
@@ -154,9 +146,10 @@ export const makeProtocolFridaWithExport = (
154
146
  readonly generateExportName?: ((clientId: number) => string) | undefined;
155
147
  }
156
148
  | undefined
157
- ): Effect.Effect<RpcServer.Protocol["Type"], never, RpcSerialization.RpcSerialization> =>
149
+ ): Effect.Effect<RpcServer.Protocol["Service"], never, RpcSerialization.RpcSerialization> =>
158
150
  Effect.gen(function* () {
159
- const { protocol, rpcExport } = yield* makeProtocolFrida({ generateExportName: options?.generateExportName });
151
+ const protocolOptions = { generateExportName: options?.generateExportName };
152
+ const { protocol, rpcExport } = yield* makeProtocolFridaNoSendRecv(protocolOptions);
160
153
  rpc.exports[options?.exportName ?? constants.defaultServerMainExportName] = rpcExport;
161
154
  return protocol;
162
155
  });
@@ -179,4 +172,4 @@ export const layerProtocolFrida = (
179
172
  }
180
173
  | undefined
181
174
  ): Layer.Layer<RpcServer.Protocol, never, RpcSerialization.RpcSerialization> =>
182
- Layer.effect(RpcServer.Protocol, makeProtocolFridaWithExport(options));
175
+ Layer.effect(RpcServer.Protocol, makeProtocolFrida(options));
@@ -5,23 +5,26 @@
5
5
  * @since 1.0.0
6
6
  */
7
7
 
8
- import type * as RpcMessage from "@effect/rpc/RpcMessage";
9
- import type * as FridaSessionError from "@efffrida/frida-tools/FridaSessionError";
10
8
  import type * as Scope from "effect/Scope";
9
+ import type * as RpcMessage from "effect/unstable/rpc/RpcMessage";
11
10
 
12
- import * as RpcClient from "@effect/rpc/RpcClient";
13
- import * as RpcSerialization from "@effect/rpc/RpcSerialization";
14
- import * as FridaScript from "@efffrida/frida-tools/FridaScript";
11
+ import * as Cause from "effect/Cause";
15
12
  import * as Effect from "effect/Effect";
16
13
  import * as Function from "effect/Function";
17
14
  import * as Layer from "effect/Layer";
18
15
  import * as Option from "effect/Option";
19
- import * as ParseResult from "effect/ParseResult";
20
- import * as Predicate from "effect/Predicate";
16
+ import * as Result from "effect/Result";
21
17
  import * as Schema from "effect/Schema";
22
18
  import * as Stream from "effect/Stream";
19
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
20
+ import * as RpcClientError from "effect/unstable/rpc/RpcClientError";
21
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
22
+
23
+ import type * as FridaSessionError from "@efffrida/frida-tools/FridaSessionError";
24
+
25
+ import * as FridaScript from "@efffrida/frida-tools/FridaScript";
23
26
 
24
- import * as constants from "../shared/constants.ts";
27
+ import * as constants from "../shared/Constants.ts";
25
28
 
26
29
  /**
27
30
  * @since 1.0.0
@@ -35,84 +38,97 @@ export const makeProtocolFrida = (
35
38
  * connecting to.
36
39
  */
37
40
  readonly exportName?: string | undefined;
38
-
39
- /** Generates server listener rpc exports for individual clients. */
40
- readonly generateExportName?: ((clientId: number) => string) | undefined;
41
41
  }
42
42
  | undefined
43
43
  ): Effect.Effect<
44
- RpcClient.Protocol["Type"],
44
+ RpcClient.Protocol["Service"],
45
45
  FridaSessionError.FridaSessionError,
46
46
  RpcSerialization.RpcSerialization | FridaScript.FridaScript | Scope.Scope
47
47
  > =>
48
48
  RpcClient.Protocol.make(
49
- Effect.fnUntraced(function* (writeResponse) {
49
+ Effect.fnUntraced(function* (writeResponse, clientIds) {
50
50
  const script = yield* FridaScript.FridaScript;
51
51
  const serialization = yield* RpcSerialization.RpcSerialization;
52
52
 
53
- const parser = serialization.unsafeMake();
54
53
  const mainExportName = options?.exportName ?? constants.defaultServerMainExportName;
55
- const makeExportName = options?.generateExportName ?? constants.generateServerExportNameForClient;
56
-
57
- /**
58
- * Obtain a client id from the frida script export, this will allow
59
- * us to filter future messages for ours since the script channels
60
- * are shared.
61
- */
62
- const clientId = yield* Effect.catchIf(
63
- script.callExport(mainExportName, Schema.Number)(),
64
- ParseResult.isParseError,
65
- () => Effect.dieMessage("Failed to obtain client ID from Frida script export")
66
- );
54
+ const informServerToSetupExport = script.callExport(mainExportName, Schema.String);
55
+ const clientExportName = yield* Effect.orDie(informServerToSetupExport());
67
56
 
68
- const isClientId = Predicate.compose(Predicate.isNumber, (id) => id === clientId);
69
- const receivingPredicate = Predicate.compose(
70
- Predicate.isUnknown,
71
- Predicate.struct({ clientId: isClientId })
72
- );
57
+ const parser = serialization.makeUnsafe();
58
+ const requestClientMap = new Map<string, number>();
59
+ let currentError: RpcClientError.RpcClientError | undefined = undefined;
73
60
 
74
- /**
75
- * Start listening for responses to our requests, decode them, and
76
- * send the responses back to the implementation.
77
- */
78
- yield* script.stream.pipe(
79
- Stream.filterMap((unfiltered) => {
80
- if (receivingPredicate(unfiltered.message)) return unfiltered.data;
81
- else return Option.none<Buffer<ArrayBufferLike>>();
82
- }),
83
- Stream.runForEach((filtered) => {
84
- try {
85
- const responses = parser.decode(filtered) as Array<RpcMessage.FromServerEncoded>;
86
- if (responses.length === 0) return Effect.void;
87
- let i = 0;
88
- return Effect.whileLoop({
89
- while: () => i < responses.length,
90
- body: () => writeResponse(responses[i++]),
91
- step: Function.constVoid,
92
- });
93
- } catch (defect) {
94
- return writeResponse({ _tag: "Defect", defect });
95
- }
61
+ const broadcast = (response: RpcMessage.FromServerEncoded) =>
62
+ Effect.forEach(clientIds, (clientId) => writeResponse(clientId, response));
63
+
64
+ yield* Stream.runForEach(script.stream, (message) => {
65
+ if (Option.isNone(message.data)) {
66
+ return Effect.void;
67
+ }
68
+
69
+ try {
70
+ const responses = parser.decode(message.data.value) as Array<RpcMessage.FromServerEncoded>;
71
+ if (responses.length === 0) return Effect.void;
72
+ let i = 0;
73
+ return Effect.whileLoop({
74
+ step: Function.constVoid,
75
+ while: () => i < responses.length,
76
+ body: () => {
77
+ const response = responses[i++]!;
78
+ if ("requestId" in response) {
79
+ const clientId = requestClientMap.get(response.requestId)!;
80
+ if (response._tag === "Exit") requestClientMap.delete(response.requestId);
81
+ return writeResponse(clientId, response);
82
+ } else return broadcast(response);
83
+ },
84
+ });
85
+ } catch (defect) {
86
+ return broadcast({
87
+ _tag: "ClientProtocolError",
88
+ error: new RpcClientError.RpcClientError({
89
+ reason: new RpcClientError.RpcClientDefect({
90
+ message: "Error decoding message",
91
+ cause: defect,
92
+ }),
93
+ }),
94
+ });
95
+ }
96
+ }).pipe(
97
+ Effect.tapCause((cause) => {
98
+ const error = Cause.findError(cause);
99
+ const hasError = Result.isSuccess(error);
100
+ const defect = hasError
101
+ ? new RpcClientError.RpcClientDefect({
102
+ message: error.success.message,
103
+ cause: error.success.cause,
104
+ })
105
+ : new RpcClientError.RpcClientDefect({
106
+ message: "Unknown error in Frida RPC client stream",
107
+ cause: Cause.squash(cause),
108
+ });
109
+
110
+ currentError = new RpcClientError.RpcClientError({ reason: defect });
111
+ return broadcast({ _tag: "ClientProtocolError", error: currentError });
96
112
  }),
97
- Effect.interruptible,
98
113
  Effect.forkScoped
99
114
  );
100
115
 
101
- /**
102
- * Sending messages is as simple as encoding them, then posting them
103
- * to the frida side tagged with our client id so it knows where to
104
- * send them back to.
105
- */
106
- const send = Effect.fnUntraced(function* (message: RpcMessage.FromClientEncoded) {
107
- const encoded = parser.encode(message);
108
- if (Predicate.isUndefined(encoded)) return;
109
- yield* script.callExport(makeExportName(clientId), Schema.Void)(encoded).pipe(Effect.orDie);
110
- });
111
-
112
116
  return {
113
- send,
114
117
  supportsAck: true,
115
118
  supportsTransferables: false,
119
+ send(clientId, request) {
120
+ if (currentError) {
121
+ return Effect.fail(currentError);
122
+ }
123
+
124
+ if (request._tag === "Request") {
125
+ requestClientMap.set(request.id, clientId);
126
+ }
127
+
128
+ const encoded = parser.encode(request);
129
+ if (encoded === undefined) return Effect.void;
130
+ return Effect.orDie(script.callExport(clientExportName)(encoded));
131
+ },
116
132
  };
117
133
  })
118
134
  );
@@ -126,16 +142,13 @@ export const layerProtocolFrida = (
126
142
  | {
127
143
  /**
128
144
  * Name for the main rpc export that all clients start by
129
- * connecting to.
145
+ * connecting to to inform the script of their client id.
130
146
  */
131
147
  readonly exportName?: string | undefined;
132
-
133
- /** Generates server listener rpc exports for individual clients. */
134
- readonly generateExportName?: ((clientId: number) => string) | undefined;
135
148
  }
136
149
  | undefined
137
150
  ): Layer.Layer<
138
151
  RpcClient.Protocol,
139
152
  FridaSessionError.FridaSessionError,
140
153
  RpcSerialization.RpcSerialization | FridaScript.FridaScript
141
- > => Layer.scoped(RpcClient.Protocol, makeProtocolFrida(options));
154
+ > => Layer.effect(RpcClient.Protocol, makeProtocolFrida(options));