xshell 1.0.49 → 1.0.51

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/file.d.ts CHANGED
@@ -21,7 +21,7 @@ export declare function fexists(fp: string, { print }?: {
21
21
  - flags: `'r'`
22
22
  - options?:
23
23
  - mode?: `'0o666'` Sets the file mode (permission and sticky bits) if the file is created. */
24
- export declare function fopen(fp: string, flags: string | number, { mode, print }?: {
24
+ export declare function fopen(fp: string, flags?: string | number, { mode, print }?: {
25
25
  mode?: fs.Mode;
26
26
  print?: boolean;
27
27
  }): Promise<fsp.FileHandle & {
@@ -91,6 +91,8 @@ export declare function flist(fpd: string, options?: FListOptions & {
91
91
  }): Promise<FStats[]>;
92
92
  export declare function flist(fpd: string, options?: FListOptions): Promise<string[]>;
93
93
  export declare function fstat(fp: string): Promise<FStats>;
94
+ export declare function flstat(fp: string): Promise<FStats>;
95
+ export declare function ffstat(handle: fsp.FileHandle): Promise<fs.BigIntStats>;
94
96
  /** 删除文件或文件夹 delete files or folders
95
97
  - fp: 文件或文件夹的完整路径 The full path to the file or folder
96
98
  - options?:
@@ -155,6 +157,11 @@ export declare function flink(fp_real: string, fp_link: string, { junction, prin
155
157
  junction?: boolean;
156
158
  print?: boolean;
157
159
  }): Promise<void>;
160
+ export declare let fwatchers: Record<string, fs.FSWatcher>;
161
+ /** 跟踪文本文件追加的内容,类似 tail -f */
162
+ export declare function ftail(fp: string, handler: (lines: string[]) => void | Promise<void>, { print }?: {
163
+ print?: boolean;
164
+ }): Promise<fs.FSWatcher>;
158
165
  /** 打开一个文件并搜索替换某个 pattern */
159
166
  export declare function freplace(fp: string, pattern: string | RegExp, replacement: string, { print }?: {
160
167
  print?: boolean;
package/file.js CHANGED
@@ -3,7 +3,7 @@ import { isUint8Array } from 'util/types';
3
3
  import { path } from './path.js';
4
4
  import { t } from './i18n/instance.js';
5
5
  import { to_json } from './prototype.js';
6
- import { assert } from './utils.js';
6
+ import { assert, Lock } from './utils.js';
7
7
  export const encodings = ['utf-8', 'gb18030', 'shift-jis', 'utf-16le'];
8
8
  /** fp 所指向的 文件/ 文件夹 是否存在
9
9
  Does the file/folder pointed to by fp exist? */
@@ -22,7 +22,7 @@ export function fexists(fp, { print = true } = {}) {
22
22
  - flags: `'r'`
23
23
  - options?:
24
24
  - mode?: `'0o666'` Sets the file mode (permission and sticky bits) if the file is created. */
25
- export async function fopen(fp, flags, { mode, print } = {}) {
25
+ export async function fopen(fp, flags = 'r', { mode, print } = {}) {
26
26
  if (print)
27
27
  console.log(t('打开文件'), fp);
28
28
  return Object.assign(await fsp.open(fp, flags, mode), { fp, flags, mode });
@@ -163,6 +163,22 @@ export async function fstat(fp) {
163
163
  stat.fp = fp;
164
164
  return stat;
165
165
  }
166
+ export async function flstat(fp) {
167
+ assert(path.isAbsolute(fp), t("flstat: 参数 fp: '{{fp}}' 必须是绝对路径", { fp }));
168
+ let stat = await fsp.lstat(fp, { bigint: true });
169
+ stat.fp = fp;
170
+ return stat;
171
+ }
172
+ export async function ffstat(handle) {
173
+ return new Promise((resolve, reject) => {
174
+ fs.fstat(handle.fd, { bigint: true }, (error, stats) => {
175
+ if (error)
176
+ reject(error);
177
+ else
178
+ resolve(stats);
179
+ });
180
+ });
181
+ }
166
182
  /** 删除文件或文件夹 delete files or folders
167
183
  - fp: 文件或文件夹的完整路径 The full path to the file or folder
168
184
  - options?:
@@ -294,6 +310,54 @@ export async function flink(fp_real, fp_link, { junction = false, print = true }
294
310
  else
295
311
  fsp.symlink(fp_real, fp_link, is_fpd_real ? 'dir' : 'file');
296
312
  }
313
+ export let fwatchers = {};
314
+ /** 跟踪文本文件追加的内容,类似 tail -f */
315
+ export async function ftail(fp, handler, { print = true } = {}) {
316
+ fwatchers[fp]?.close();
317
+ const { size } = await fstat(fp);
318
+ let pointer = Number(size);
319
+ let lock = new Lock(await fopen(fp));
320
+ let fbuf = new Uint8Array(2 ** 20);
321
+ let strbuf = '';
322
+ let decoder = new TextDecoder();
323
+ if (print)
324
+ console.log('开始跟踪追加内容', fp);
325
+ const { default: throttle } = await import('lodash/throttle.js');
326
+ const onchange_throttled = throttle(async () => {
327
+ if (lock.locked)
328
+ return;
329
+ await lock.request(async (handle) => {
330
+ const { bytesRead } = await handle.read(fbuf, 0, fbuf.length, pointer);
331
+ pointer += bytesRead;
332
+ const chunk = decoder.decode(fbuf.subarray(0, bytesRead), { stream: true });
333
+ let lines = [];
334
+ let j = 0;
335
+ for (let i = 0; (i = chunk.indexOf('\n', j)) >= 0;) {
336
+ let line = chunk.slice(j, i);
337
+ if (strbuf) {
338
+ line = strbuf + line;
339
+ strbuf = '';
340
+ }
341
+ j = i + 1;
342
+ lines.push(line);
343
+ }
344
+ strbuf = chunk.slice(j);
345
+ await handler(lines);
346
+ });
347
+ }, 250);
348
+ let watcher = fs.watch(fp, event => {
349
+ if (event === 'change')
350
+ onchange_throttled();
351
+ else {
352
+ console.error(`被监听的文件 ${fp.quote()} 出现了 rename 事件,结束 ftail`);
353
+ watcher.close();
354
+ }
355
+ });
356
+ watcher.on('error', error => {
357
+ console.error(error);
358
+ });
359
+ return fwatchers[fp] = watcher;
360
+ }
297
361
  /** 打开一个文件并搜索替换某个 pattern */
298
362
  export async function freplace(fp, pattern, replacement, { print = true } = {}) {
299
363
  await fwrite(fp, (await fread(fp, { print }))
package/i18n/dict.json CHANGED
@@ -343,5 +343,8 @@
343
343
  },
344
344
  "xshell 启动成功,正在监听: http://localhost:8421": {
345
345
  "en": "xshell started successfully and is listening: http://localhost:8421"
346
+ },
347
+ "flstat: 参数 fp: '{{fp}}' 必须是绝对路径": {
348
+ "en": "flstat: parameter fp: '{{fp}}' must be an absolute path"
346
349
  }
347
350
  }
package/net.js CHANGED
@@ -87,7 +87,9 @@ export async function request(url, { method, queries, headers: _headers, body, t
87
87
  // --- headers, http/2 开始都用小写的 headers
88
88
  let headers = new Headers({
89
89
  'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja-JP;q=0.6,ja;q=0.5',
90
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
90
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
91
+ 'sec-ch-ua-platform': '"Windows"',
92
+ 'sec-ch-ua-platform-version': '"15.0.0"',
91
93
  });
92
94
  if (body !== undefined)
93
95
  headers.set('content-type', type);
@@ -541,7 +543,7 @@ export class Remote {
541
543
  作为 websocket 连接接收方,必传 websocket 参数
542
544
  发送或连接出错时自动清理 message.id 对应的 handler */
543
545
  async send(message, websocket) {
544
- assert(!message.data || message.data.every(arg => arg !== undefined), 'message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null');
546
+ assert(!message.data || message.data.every(arg => arg !== undefined), t('message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null'));
545
547
  if (this.print)
546
548
  console.log('remote.send:', message);
547
549
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "interactive programming"
17
17
  ],
18
18
  "engines": {
19
- "node": ">=20.5.0",
19
+ "node": ">=20.5.1",
20
20
  "vscode": ">=1.79.0"
21
21
  },
22
22
  "author": "ShenHongFei <shen.hongfei@outlook.com> (https://github.com/ShenHongFei)",
@@ -77,7 +77,7 @@
77
77
  "strip-ansi": "^7.1.0",
78
78
  "through2": "^4.0.2",
79
79
  "tough-cookie": "^4.1.3",
80
- "tslib": "^2.6.1",
80
+ "tslib": "^2.6.2",
81
81
  "typescript": "^5.1.6",
82
82
  "ua-parser-js": "2.0.0-alpha.2",
83
83
  "undici": "^5.23.0",
@@ -97,18 +97,18 @@
97
97
  "@types/koa-compress": "^4.0.3",
98
98
  "@types/lodash": "^4.14.197",
99
99
  "@types/mime-types": "^2.1.1",
100
- "@types/node": "^20.4.10",
100
+ "@types/node": "^20.5.1",
101
101
  "@types/react": "^18.2.20",
102
102
  "@types/through2": "^2.0.38",
103
103
  "@types/tough-cookie": "^4.0.2",
104
104
  "@types/ua-parser-js": "^0.7.36",
105
105
  "@types/vinyl-fs": "^3.0.2",
106
106
  "@types/vscode": "^1.81.0",
107
- "@typescript-eslint/eslint-plugin": "^6.3.0",
108
- "@typescript-eslint/parser": "^6.3.0",
107
+ "@typescript-eslint/eslint-plugin": "^6.4.0",
108
+ "@typescript-eslint/parser": "^6.4.0",
109
109
  "eslint": "^8.47.0",
110
- "eslint-plugin-react": "^7.33.1",
111
- "eslint-plugin-xlint": "^1.0.6"
110
+ "eslint-plugin-react": "^7.33.2",
111
+ "eslint-plugin-xlint": "^1.0.7"
112
112
  },
113
113
  "scripts": {
114
114
  "start": "node --title=xshell --inspect=0.0.0.0:8420 ./xshell.js",
package/server.d.ts CHANGED
@@ -68,10 +68,7 @@ export declare class Server {
68
68
  format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
69
69
  proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>): Promise<void>;
70
70
  static filter_response_headers(headers: Headers): {};
71
- try_send(ctx: Context, fp: string, { root, log_404, }: {
72
- root: string;
73
- log_404?: boolean;
74
- }): Promise<boolean>;
71
+ try_send(ctx: Context, fpd_root: string, fp: string, log_404: boolean): Promise<boolean>;
75
72
  /** 将 body 设置为 fp 所指文件的 ReadStream
76
73
  检查 fp 是否合法,设置对应的 content-type,支持缓存,支持分块下载
77
74
  - ctx: Koa.Context
package/server.js CHANGED
@@ -308,27 +308,25 @@ export class Server {
308
308
  headers_[key] = value;
309
309
  return headers_;
310
310
  }
311
- async try_send(ctx, fp, { root, log_404, }) {
311
+ async try_send(ctx, fpd_root, fp, log_404) {
312
312
  const { request: { _path, path, method }, response, } = ctx;
313
- if (!(typeof response.body === 'undefined') || response.status !== 404)
313
+ if (response.body !== undefined || response.status !== 404)
314
314
  return true;
315
315
  if (method !== 'HEAD' && method !== 'GET')
316
316
  return false;
317
- function _log_404() {
318
- let s = `${' '.repeat(11)} ${method.toLowerCase()} 404: ${path}`;
319
- if (_path !== path)
320
- s += ` ${_path.bracket()}`;
321
- console.log(s.red);
322
- }
323
317
  try {
324
- await this.fsend(ctx, fp, { root });
318
+ await this.fsend(ctx, fp, { root: fpd_root });
325
319
  return true;
326
320
  }
327
321
  catch (error) {
328
322
  if (error.status !== 404)
329
323
  throw error;
330
- if (log_404)
331
- _log_404();
324
+ if (log_404) {
325
+ let s = `${' '.repeat(11)} ${method.toLowerCase()} 404: ${path}`;
326
+ if (_path !== path)
327
+ s += ` ${_path.bracket()}`;
328
+ console.log(s.red);
329
+ }
332
330
  return false;
333
331
  }
334
332
  }
@@ -51,6 +51,8 @@ export declare class Lock<TResource = void> {
51
51
  export declare function pause(milliseconds?: number): Promise<void>;
52
52
  /** 字符串字典序比较 */
53
53
  export declare function strcmp(l: string, r: string): 0 | 1 | -1;
54
+ export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
55
+ export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
54
56
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
55
57
  export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
56
58
  /** 时间间隔 (milliseconds) 格式化 */
package/utils.browser.js CHANGED
@@ -126,6 +126,19 @@ export function strcmp(l, r) {
126
126
  return -1;
127
127
  return 1;
128
128
  }
129
+ export function get(obj, keypath) {
130
+ let obj_ = obj;
131
+ for (const key of keypath.split('.'))
132
+ obj_ = obj_[key];
133
+ return obj_;
134
+ }
135
+ export function invoke(obj, funcpath, args) {
136
+ const paths = funcpath.split('.');
137
+ let obj_ = obj;
138
+ for (let i = 0; i < paths.length - 1; i++)
139
+ obj_ = obj_[paths[i]];
140
+ return obj_[paths.at(-1)].call(obj_, ...args);
141
+ }
129
142
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
130
143
  export function concat(arrays) {
131
144
  let length = 0;
package/utils.d.ts CHANGED
@@ -22,6 +22,8 @@ export declare function unique<T>(iterable: T[] | Iterable<T>, selector?: string
22
22
  export declare function sort_keys<T>(obj: T): T;
23
23
  /** 字符串字典序比较 */
24
24
  export declare function strcmp(l: string, r: string): 0 | 1 | -1;
25
+ export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
26
+ export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
25
27
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
26
28
  export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
27
29
  export declare function typed_array_to_buffer(view: ArrayBufferView): Buffer;
package/utils.js CHANGED
@@ -86,6 +86,19 @@ export function strcmp(l, r) {
86
86
  return -1;
87
87
  return 1;
88
88
  }
89
+ export function get(obj, keypath) {
90
+ let obj_ = obj;
91
+ for (const key of keypath.split('.'))
92
+ obj_ = obj_[key];
93
+ return obj_;
94
+ }
95
+ export function invoke(obj, funcpath, args) {
96
+ const paths = funcpath.split('.');
97
+ let obj_ = obj;
98
+ for (let i = 0; i < paths.length - 1; i++)
99
+ obj_ = obj_[paths[i]];
100
+ return obj_[paths.at(-1)].call(obj_, ...args);
101
+ }
89
102
  /** 拼接 TypedArrays 生成一个完整的 Uint8Array */
90
103
  export function concat(arrays) {
91
104
  let length = 0;
@@ -405,8 +418,8 @@ export async function stream_to_buffer(stream) {
405
418
  export async function* stream_to_lines(stream) {
406
419
  let buf = '';
407
420
  for await (const chunk of stream) {
408
- let i = 0, j = 0;
409
- for (; (i = chunk.indexOf('\n', j)) >= 0;) {
421
+ let j = 0;
422
+ for (let i = 0; (i = chunk.indexOf('\n', j)) >= 0;) {
410
423
  let line = chunk.slice(j, i);
411
424
  if (buf) {
412
425
  line = buf + line;