xshell 1.0.19 → 1.0.20

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.
package/i18n/dict.json CHANGED
@@ -292,5 +292,20 @@
292
292
  },
293
293
  "连接被关闭": {
294
294
  "en": "connection closed"
295
+ },
296
+ "状态码 {{status}}, 非 2xx": {
297
+ "en": "Status code {{status}}, not 2xx"
298
+ },
299
+ "构建 Remote 时 url 和 websocket 最多只能传一个": {
300
+ "en": "When building Remote, only one url and websocket can be passed at most"
301
+ },
302
+ "创建 Remote 时传入的 websocket 连接已断开": {
303
+ "en": "The incoming websocket connection was broken while creating the Remote"
304
+ },
305
+ "传入的 websocket 连接已断开": {
306
+ "en": "The incoming websocket connection was disconnected"
307
+ },
308
+ "尝试释放未锁定的锁,这不应该发生": {
309
+ "en": "Attempt to release an unlocked lock, this should not happen"
295
310
  }
296
311
  }
package/net.browser.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import './prototype.browser.js';
2
+ import { Lock } from './utils.browser.js';
2
3
  export interface BasicAuth {
3
4
  type: 'basic';
4
5
  username: string;
@@ -14,12 +15,12 @@ export interface RequestOptions {
14
15
  headers?: Record<string, string> | Headers;
15
16
  body?: string | Record<string, any> | ArrayBufferView | ArrayBuffer | Blob | URLSearchParams | FormData;
16
17
  type?: 'application/json' | 'application/x-www-form-urlencoded' | 'multipart/form-data';
18
+ encoding?: 'binary';
17
19
  retries?: true | number;
18
20
  timeout?: number;
19
21
  auth?: BasicAuth | BearerAuth;
20
22
  cookies?: Record<string, string>;
21
23
  cors?: boolean;
22
- by?: 'fetch' | 'GM_xmlhttpRequest';
23
24
  }
24
25
  export interface RequestRawOptions extends RequestOptions {
25
26
  raw: true;
@@ -47,11 +48,11 @@ export interface RequestError extends Error {
47
48
  - body?: http 请求体,可以是 string, Record<string, any> (会自动 JSON.stringify), ArrayBuffer(View), Blob,
48
49
  URLSearchParams (type 为 x-www-form-urlencoded), FormData (type 为 form-data)
49
50
  - type?: `'application/json'` 有 body 时设置 http 请求头中的 content-type 头
51
+ - encoding?: 设置为 'binary' 时返回 ArrayBuffer 的 body
50
52
  - retries?: `false` 可以传入 true (默认 2 次) 或 重试次数
51
53
  - timeout?: `5 * 1000`
52
54
  - auth?: BasicAuth | BearerAuth
53
- - raw?: `false` 传入后返回整个 response
54
- - by: `window.GM_xmlhttpRequest ? 'GM_xmlhttpRequest' : 'fetch'` 发起请求所使用的底层方法 */
55
+ - raw?: `false` 传入后返回整个 response */
55
56
  export declare function request(url: string | URL): Promise<string>;
56
57
  export declare function request(url: string | URL, options: RequestRawOptions): Promise<Response>;
57
58
  export declare function request(url: string | URL, options: RequestOptions & {
@@ -99,8 +100,7 @@ export type MessageHandler = (message: Message, websocket?: WebSocket) => void |
99
100
  /** 二进制消息格式
100
101
  - json.length (小端序): 4 字节
101
102
  - json 数据
102
- - binary 数据
103
- */
103
+ - binary 数据 */
104
104
  export interface Message<TData extends any[] = any[]> {
105
105
  /** rpc id: 在 rpc 系统中认为是唯一的。用来在单个 websocket 连接上复用多个 rpc 请求。多个相同 id 的 message 组成一个请求流 */
106
106
  id?: number;
@@ -119,8 +119,7 @@ export interface Message<TData extends any[] = any[]> {
119
119
  里面是可序列化为 json 的 js 变量,或者是 Uint8Array
120
120
  Uint8Array 的参数处理后被替换为 Uint8Array.byteLength, 并将下标记录在 bins 中
121
121
 
122
- 注意: 数组中如果有 undefined 值,传输到对面会变成 null 值
123
- */
122
+ 注意: 数组中如果有 undefined 值,传输到对面会变成 null 值 */
124
123
  data?: TData;
125
124
  /** bins: data 中哪些下标对应的原始值是 Uint8Array 类型的,如: [0, 3] */
126
125
  bins?: number[];
@@ -128,35 +127,60 @@ export interface Message<TData extends any[] = any[]> {
128
127
  /** 通过创建 remote 对象对 websocket rpc 进行抽象
129
128
  调用方使用 remote.call 进行调用
130
129
  被调方在创建 remote 对象时传入 funcs 注册处理函数,并使用 remote.handle 方法处理 websocket message
131
- 未连接时自动连接,断开后自动重连 */
130
+ 未连接时自动连接,断开后自动重连 (保证执行 send 时已连接,否则报错)
131
+ 从设计上与 rpc 状态与底层连接的状态无关 (通过传入 url 创建的),底层连接断开后,只要检测到断线,会尝试重连,如果重连成功,则不影响调用的状态
132
+ @example
133
+ // Zero 继承自 Remote 并通过 call 实现了一些方法
134
+ let zero = new Zero({ local: true })
135
+
136
+ // 一元 rpc
137
+ await zero.repl_ts('1234')
138
+
139
+ // 订阅流
140
+ const id = genid()
141
+
142
+ zero.handlers.set(id, ({ data: [chunk] }: Message<[Uint8Array]>) => {
143
+ term.write(chunk)
144
+ })
145
+
146
+ zero.send({ id, func: 'subscribe_stdio' }) */
132
147
  export declare class Remote {
133
- url: string;
148
+ /** 是否为建立 rpc 连接的发起方 */
149
+ initiator: boolean;
150
+ /** websocket url */
151
+ url?: string;
134
152
  /** 主动发起 websocket 连接的客户端 (构造函数传 url) 有这个属性 */
135
- websocket?: WebSocket;
153
+ lwebsocket?: Lock<WebSocket>;
136
154
  /** 通过 rpc message.func 被调用的 rpc 函数 */
137
155
  funcs: Record<string, MessageHandler>;
138
156
  /** map<id, message handler>: 通过 rpc message.id 找到对应的 handler, unary rpc 接收方不需要设置 handlers, 发送方需要 */
139
157
  handlers: Map<number, MessageHandler>;
140
158
  print: boolean;
141
159
  pconnect: Promise<any>;
142
- get connected(): boolean;
143
- static parse<TData extends any[] = any[]>(array_buffer: ArrayBuffer): Message<TData>;
160
+ /** 使用 Uint8Array 作为参数更灵活 https://stackoverflow.com/a/74505197/7609214 */
161
+ static parse<TData extends any[] = any[]>(buffer: Uint8Array): Message<TData>;
144
162
  static pack({ id, func, data, done, error }: Message): Uint8Array;
145
- constructor({ url, funcs, websocket }?: {
163
+ /** 作为 rpc 发起方,可以通过传入 url 或者 websocket 定义远程 Remote
164
+ 作为 rpc 接收方,可以不传 url 和 websocket 定义本地 Remote */
165
+ constructor({ url, funcs, websocket, on_error }?: {
146
166
  url?: string;
147
167
  funcs?: Remote['funcs'];
148
168
  websocket?: WebSocket;
169
+ on_error?(error: WebSocketConnectionError, websocket: WebSocket): void;
149
170
  });
150
- /** 幂等,保证 websocket 已连接,否则抛出异常,不需要手动调用,在其它方法中已默认自动重连 */
151
- connect(): Promise<void>;
171
+ on_error?(error: WebSocketConnectionError, websocket: WebSocket): void;
172
+ /** 幂等,保证 websocket 已连接,否则抛出异常,不需要手动调用,在其它方法中已默认自动重连
173
+ 作为接收方需要传入使用的 websocket 连接,确保这个这个连接的状态 */
174
+ connect(websocket?: WebSocket): Promise<void>;
152
175
  disconnect(): void;
153
- /** 接收 websocket 连接的 remote 端必传 websocket 参数;发起端选传,如果传了必须等于 this.websocket
176
+ /** 接收 websocket 连接的本地 remote 必传 websocket 参数;发起端选传,如果传了必须等于 this.websocket_lock.resource
154
177
  发送或连接出错时自动清理 message.id 对应的 handler */
155
178
  send(message: Message, websocket?: WebSocket): Promise<void>;
156
179
  /** 处理接收到的 websocket message 并解析, 根据 id dispatch 到对应的 handler 进行处理
157
180
  如果 message.done == true 则清理 handler
158
- 如果 handler 返回了值,则包装为 message 发送 */
159
- handle(data: ArrayBuffer, websocket: WebSocket): Promise<void>;
181
+ 如果 handler 返回了值,则包装为 message 发送
182
+ 使用 Uint8Array 作为参数更灵活 https://stackoverflow.com/a/74505197/7609214 */
183
+ handle(data: Uint8Array, websocket: WebSocket): Promise<void>;
160
184
  /** 调用 remote 中的 func, 只适用于最简单的一元 rpc (请求, 响应) */
161
185
  call<TReturn extends any[] = any[]>(func: string, args?: any[]): Promise<TReturn>;
162
186
  }
package/net.browser.js CHANGED
@@ -1,7 +1,6 @@
1
- /// <reference types='tampermonkey' />
2
1
  import { t } from './i18n/instance.js';
3
2
  import './prototype.browser.js'; // to_time_str()
4
- import { assert, concat, genid, delay } from './utils.browser.js';
3
+ import { assert, concat, genid, delay, Lock } from './utils.browser.js';
5
4
  async function fetch_retry(url, options, timeout, retries = 0, count = 0) {
6
5
  try {
7
6
  options.signal = AbortSignal.timeout(timeout);
@@ -19,7 +18,7 @@ async function fetch_retry(url, options, timeout, retries = 0, count = 0) {
19
18
  }
20
19
  }
21
20
  }
22
- export async function request(url, { method, queries, headers: _headers, body, type = 'application/json', retries, timeout = 5 * 1000, auth, raw = false, cors, by = window.GM_xmlhttpRequest ? 'GM_xmlhttpRequest' : 'fetch', } = {}) {
21
+ export async function request(url, { method, queries, headers: _headers, body, type = 'application/json', encoding, retries, timeout = 5 * 1000, auth, raw = false, cors, } = {}) {
23
22
  url = new URL(url, location.href);
24
23
  if (queries)
25
24
  for (const key in queries) {
@@ -51,117 +50,71 @@ export async function request(url, { method, queries, headers: _headers, body, t
51
50
  headers[key] = value;
52
51
  }
53
52
  }
54
- if (by === 'fetch') {
55
- let options = {
56
- ...method ? { method } : {},
57
- keepalive: true,
58
- redirect: 'follow',
59
- credentials: 'include',
60
- ...cors ? { mode: 'cors' } : {},
61
- headers,
62
- // --- body
63
- body: (() => {
64
- if (body === undefined)
65
- return;
66
- switch (type) {
67
- case 'application/json':
68
- return typeof body === 'string' ||
69
- ArrayBuffer.isView(body) ||
70
- body instanceof ArrayBuffer ||
71
- body instanceof Blob ?
72
- body
73
- :
74
- JSON.stringify(body);
75
- case 'application/x-www-form-urlencoded':
76
- return body instanceof URLSearchParams ? body : new URLSearchParams(body);
77
- case 'multipart/form-data':
78
- if (body instanceof FormData)
79
- return body;
80
- else {
81
- let form = new FormData();
82
- for (const key in body) {
83
- let value = body[key];
84
- form.set(key, value);
85
- }
86
- return form;
53
+ let options = {
54
+ ...method ? { method } : {},
55
+ keepalive: true,
56
+ redirect: 'follow',
57
+ credentials: 'include',
58
+ ...cors ? { mode: 'cors' } : {},
59
+ headers,
60
+ // --- body
61
+ body: (() => {
62
+ if (body === undefined)
63
+ return;
64
+ switch (type) {
65
+ case 'application/json':
66
+ return typeof body === 'string' ||
67
+ ArrayBuffer.isView(body) ||
68
+ body instanceof ArrayBuffer ||
69
+ body instanceof Blob ?
70
+ body
71
+ :
72
+ JSON.stringify(body);
73
+ case 'application/x-www-form-urlencoded':
74
+ return body instanceof URLSearchParams ? body : new URLSearchParams(body);
75
+ case 'multipart/form-data':
76
+ if (body instanceof FormData)
77
+ return body;
78
+ else {
79
+ let form = new FormData();
80
+ for (const key in body) {
81
+ let value = body[key];
82
+ form.set(key, value);
87
83
  }
88
- }
89
- })(),
90
- };
91
- let response;
92
- try {
93
- response = await fetch_retry(url, options, timeout, retries);
94
- if (!response.ok)
95
- throw Object.assign(new Error(`状态码 ${response.status}, 非 2xx`), { name: 'StatusCodeError' });
96
- }
97
- catch (error) {
98
- error.url = url;
99
- error.options = options;
100
- if (response)
101
- error.response = {
102
- status: response.status,
103
- url: response.url,
104
- headers: response.headers,
105
- ok: response.ok,
106
- type: response.type,
107
- text: await response.text(),
108
- redirected: response.redirected
109
- };
110
- throw error;
111
- }
112
- if (raw)
113
- return response;
114
- if (!response.body)
115
- return response.body;
116
- return response.text();
84
+ return form;
85
+ }
86
+ }
87
+ })(),
88
+ };
89
+ let response;
90
+ try {
91
+ response = await fetch_retry(url, options, timeout, retries);
92
+ if (!response.ok)
93
+ throw Object.assign(new Error(t('状态码 {{status}}, 非 2xx', { status: response.status })), { name: 'StatusCodeError' });
117
94
  }
118
- else
119
- return new Promise((resolve, reject) => {
120
- const options = {
121
- ...method ? { method: method } : {},
122
- url: url.toString(),
123
- // @ts-ignore ts 类型不支持,实际上已经有了
124
- headers: Object.fromEntries(headers),
125
- ...body ? {
126
- data: typeof body === 'string' ? body : JSON.stringify(body)
127
- } : {},
128
- onload(response) {
129
- if (200 <= response.status && response.status <= 299)
130
- resolve(response.responseText);
131
- else
132
- reject(Object.assign(new Error(`状态码 ${response.status}, 非 2xx`), {
133
- name: 'StatusCodeError',
134
- url,
135
- options,
136
- response: {
137
- stauts: response.status,
138
- url: url.toString(),
139
- headers: response.responseHeaders,
140
- ok: false,
141
- text: response.responseText
142
- }
143
- }));
144
- },
145
- onerror(response) {
146
- reject(Object.assign(new Error(response.error), {
147
- name: 'TimeoutError',
148
- url: url,
149
- options,
150
- response: {
151
- stauts: response.status,
152
- url: url.toString(),
153
- headers: response.responseHeaders,
154
- ok: false,
155
- text: response.responseText
156
- }
157
- }));
158
- },
159
- ontimeout() {
160
- reject(Object.assign(new Error('GM_xmlhttpRequest 超时'), { name: 'TimeoutError', url, options }));
161
- },
95
+ catch (error) {
96
+ error.url = url;
97
+ error.options = options;
98
+ if (response)
99
+ error.response = {
100
+ status: response.status,
101
+ url: response.url,
102
+ headers: response.headers,
103
+ ok: response.ok,
104
+ type: response.type,
105
+ text: await response.text(),
106
+ redirected: response.redirected
162
107
  };
163
- GM_xmlhttpRequest(options);
164
- });
108
+ throw error;
109
+ }
110
+ if (raw)
111
+ return response;
112
+ if (!response.body)
113
+ return response.body;
114
+ if (encoding === 'binary')
115
+ return response.arrayBuffer();
116
+ else
117
+ return response.text();
165
118
  }
166
119
  /** 发起 http 请求并将响应体作为 json 解析 */
167
120
  export async function request_json(url, options) {
@@ -272,32 +225,48 @@ export async function connect_websocket(url, { protocols, on_message, on_error,
272
225
  /** 通过创建 remote 对象对 websocket rpc 进行抽象
273
226
  调用方使用 remote.call 进行调用
274
227
  被调方在创建 remote 对象时传入 funcs 注册处理函数,并使用 remote.handle 方法处理 websocket message
275
- 未连接时自动连接,断开后自动重连 */
228
+ 未连接时自动连接,断开后自动重连 (保证执行 send 时已连接,否则报错)
229
+ 从设计上与 rpc 状态与底层连接的状态无关 (通过传入 url 创建的),底层连接断开后,只要检测到断线,会尝试重连,如果重连成功,则不影响调用的状态
230
+ @example
231
+ // Zero 继承自 Remote 并通过 call 实现了一些方法
232
+ let zero = new Zero({ local: true })
233
+
234
+ // 一元 rpc
235
+ await zero.repl_ts('1234')
236
+
237
+ // 订阅流
238
+ const id = genid()
239
+
240
+ zero.handlers.set(id, ({ data: [chunk] }: Message<[Uint8Array]>) => {
241
+ term.write(chunk)
242
+ })
243
+
244
+ zero.send({ id, func: 'subscribe_stdio' }) */
276
245
  export class Remote {
246
+ /** 是否为建立 rpc 连接的发起方 */
247
+ initiator;
248
+ /** websocket url */
277
249
  url;
278
250
  /** 主动发起 websocket 连接的客户端 (构造函数传 url) 有这个属性 */
279
- websocket;
251
+ lwebsocket;
280
252
  /** 通过 rpc message.func 被调用的 rpc 函数 */
281
253
  funcs;
282
254
  /** map<id, message handler>: 通过 rpc message.id 找到对应的 handler, unary rpc 接收方不需要设置 handlers, 发送方需要 */
283
255
  handlers = new Map();
284
256
  print = false;
285
257
  pconnect;
286
- get connected() {
287
- return this.websocket?.readyState === WebSocket.OPEN;
288
- }
289
- static parse(array_buffer) {
290
- const buf = new Uint8Array(array_buffer);
291
- const dv = new DataView(array_buffer);
258
+ /** 使用 Uint8Array 作为参数更灵活 https://stackoverflow.com/a/74505197/7609214 */
259
+ static parse(buffer) {
260
+ const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
292
261
  const len_json = dv.getUint32(0, true);
293
262
  let offset = 4 + len_json;
294
- let message = JSON.parse(decoder.decode(buf.subarray(4, offset)));
263
+ let message = JSON.parse(decoder.decode(buffer.subarray(4, offset)));
295
264
  if (message.bins) {
296
265
  const { bins } = message;
297
266
  let { data } = message;
298
267
  for (const ibin of bins) {
299
268
  const len_buf = data[ibin];
300
- data[ibin] = buf.subarray(offset, offset + len_buf);
269
+ data[ibin] = buffer.subarray(offset, offset + len_buf);
301
270
  offset += len_buf;
302
271
  }
303
272
  }
@@ -332,50 +301,69 @@ export class Remote {
332
301
  dv.setUint32(0, str_json.length, true);
333
302
  return concat([dv, str_json, ...bufs]);
334
303
  }
335
- constructor({ url, funcs = {}, websocket } = {}) {
304
+ /** 作为 rpc 发起方,可以通过传入 url 或者 websocket 定义远程 Remote
305
+ 作为 rpc 接收方,可以不传 url 和 websocket 定义本地 Remote */
306
+ constructor({ url, funcs = {}, websocket, on_error } = {}) {
307
+ assert(!(url && websocket), t('构建 Remote 时 url 和 websocket 最多只能传一个'));
308
+ this.initiator = Boolean(url || websocket);
336
309
  this.url = url;
337
310
  this.funcs = funcs;
338
- this.websocket = websocket;
311
+ if (on_error)
312
+ this.on_error = on_error;
313
+ this.lwebsocket = new Lock(websocket || null);
314
+ }
315
+ on_error(error, websocket) {
316
+ throw error;
339
317
  }
340
- /** 幂等,保证 websocket 已连接,否则抛出异常,不需要手动调用,在其它方法中已默认自动重连 */
341
- async connect() {
342
- if (this.connected)
318
+ /** 幂等,保证 websocket 已连接,否则抛出异常,不需要手动调用,在其它方法中已默认自动重连
319
+ 作为接收方需要传入使用的 websocket 连接,确保这个这个连接的状态 */
320
+ async connect(websocket) {
321
+ if (this.initiator) {
322
+ if (this.lwebsocket.resource?.readyState === WebSocket.OPEN)
323
+ return;
324
+ else if (!this.url)
325
+ throw new Error(t('创建 Remote 时传入的 websocket 连接已断开'));
326
+ // else 等待重连
327
+ }
328
+ else if (websocket.readyState === WebSocket.OPEN)
343
329
  return;
344
- const ptail = this.pconnect;
345
- let resolve;
346
- this.pconnect = new Promise((_resolve, _reject) => {
347
- resolve = _resolve;
330
+ else
331
+ throw new Error(t('传入的 websocket 连接已断开'));
332
+ // 假设有多个请求想要并发连接 websocket, 且此时 websocket 是断开的状态
333
+ // 应该排队依次连接,而不是后续的连接直接使用第一次连接的 promise,后续调用还是应该尝试重连(不止连接一次)
334
+ return this.lwebsocket.request(async (websocket) => {
335
+ // 保存的 rpc 状态在 this.handlers, 与 websocket 无关,因此即使断开重连也不影响 rpc 的运行,即
336
+ // 底层连接断开后自动重连对上层应该是无感知的,除非再次连接时失败
337
+ if (websocket?.readyState === WebSocket.OPEN)
338
+ return;
339
+ else if (!this.url)
340
+ throw new Error(t('创建 Remote 时传入的 websocket 连接已断开'));
341
+ else // 重连
342
+ this.lwebsocket.resource = await connect_websocket(this.url, {
343
+ on_message: (data, websocket) => {
344
+ this.handle(new Uint8Array(data), websocket);
345
+ },
346
+ on_error: this.on_error.bind(this)
347
+ });
348
348
  });
349
- await ptail;
350
- try {
351
- if (!this.connected)
352
- // 保存的 rpc 状态在 this.handlers, 与 websocket 无关,因此即使断开重连也不影响 rpc 的运行,即
353
- // 底层连接断开后自动重连对上层应该是无感知的,除非再次连接时失败
354
- this.websocket = await connect_websocket(this.url, { on_message: this.handle.bind(this) });
355
- }
356
- finally {
357
- resolve();
358
- }
359
349
  }
360
350
  disconnect() {
361
- this.websocket?.close();
351
+ this.lwebsocket.resource?.close(1000);
362
352
  }
363
- /** 接收 websocket 连接的 remote 端必传 websocket 参数;发起端选传,如果传了必须等于 this.websocket
353
+ /** 接收 websocket 连接的本地 remote 必传 websocket 参数;发起端选传,如果传了必须等于 this.websocket_lock.resource
364
354
  发送或连接出错时自动清理 message.id 对应的 handler */
365
355
  async send(message, websocket) {
366
356
  try {
367
- if (this.url) {
368
- assert(!websocket || websocket === this.websocket);
357
+ if (this.initiator) {
358
+ assert(!websocket || websocket === this.lwebsocket.resource);
369
359
  await this.connect();
370
- websocket = this.websocket;
371
- }
372
- else {
373
- assert(websocket);
374
- if (websocket.readyState !== WebSocket.OPEN)
375
- throw new Error(t('remote.send(): websocket client 已断开'));
360
+ websocket = this.lwebsocket.resource;
376
361
  }
362
+ else
363
+ await this.connect(websocket);
377
364
  if (!message.id)
378
365
  message.id = genid();
366
+ // 不需要独占 websocket
379
367
  websocket.send(Remote.pack(message));
380
368
  }
381
369
  catch (error) {
@@ -386,7 +374,8 @@ export class Remote {
386
374
  }
387
375
  /** 处理接收到的 websocket message 并解析, 根据 id dispatch 到对应的 handler 进行处理
388
376
  如果 message.done == true 则清理 handler
389
- 如果 handler 返回了值,则包装为 message 发送 */
377
+ 如果 handler 返回了值,则包装为 message 发送
378
+ 使用 Uint8Array 作为参数更灵活 https://stackoverflow.com/a/74505197/7609214 */
390
379
  async handle(data, websocket) {
391
380
  const message = Remote.parse(data);
392
381
  const { id, func, done } = message;