├── CHANGELOG.md ├── lib ├── spinner.mjs ├── utils.mjs ├── host.mjs ├── stdout.mjs ├── ping.mjs ├── server.mjs ├── constant.mjs ├── ip.mjs └── painter.mjs ├── package.json ├── LICENSE ├── bin └── cmd.js ├── README.md ├── .gitignore └── index.mjs /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 (2023-02-20) 2 | 3 | ### feature 4 | 5 | - system host setting 6 | 7 | ## 1.1.0 (2023-02-13) 8 | 9 | ### feature 10 | 11 | - support DNS resolve time 12 | - support node19.x 13 | - more elegant IP ping latency style 14 | 15 | ### fix 16 | 17 | - IO stream cache flush 18 | -------------------------------------------------------------------------------- /lib/spinner.mjs: -------------------------------------------------------------------------------- 1 | import { SPINNER_FRAME } from "./constant.mjs" 2 | 3 | export class Spinner { 4 | constructor(type) { 5 | this.tick = 0 6 | this.frames = SPINNER_FRAME[type] 7 | } 8 | 9 | text() { 10 | if (this.tick >= this.frames.length) { 11 | this.tick = 0 12 | } 13 | return this.frames[this.tick++] 14 | } 15 | } -------------------------------------------------------------------------------- /lib/utils.mjs: -------------------------------------------------------------------------------- 1 | export function getRealTime(time) { 2 | return time === 'timeout' ? Infinity : time 3 | } 4 | 5 | export function interval(callback, time) { 6 | return new Promise(async r => { 7 | await callback() 8 | await sleep(time) 9 | r() 10 | }).then(() => interval(callback, time)) 11 | } 12 | 13 | export async function sleep(time) { 14 | return new Promise(r => { 15 | setTimeout(r, time) 16 | }) 17 | } -------------------------------------------------------------------------------- /lib/host.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | 3 | 4 | export class HostSetting { 5 | constructor(host) { 6 | this.host = host 7 | this.hostFile = '/etc/hosts' 8 | } 9 | 10 | async getContent() { 11 | const content = await readFile(this.hostFile); 12 | 13 | this.content = content.toString().split('\n') 14 | } 15 | 16 | setHostIP(ip) { 17 | this.ip = ip; 18 | this.line = this.content.findIndex(str => !/^\s*#/.test(str) && str.includes(this.host)) 19 | const settingStr = `${ip.addr} ${this.host}` 20 | if (this.line > -1) { 21 | this.content[this.line] = settingStr 22 | } else { 23 | this.content.push(settingStr) 24 | } 25 | 26 | return writeFile(this.hostFile, this.content.join('\n'), { mode: 0o777 }) 27 | } 28 | } -------------------------------------------------------------------------------- /lib/stdout.mjs: -------------------------------------------------------------------------------- 1 | import { stdout } from "node:process" 2 | import { Readline } from 'node:readline/promises' 3 | import { emitKeypressEvents } from 'node:readline' 4 | import { COLORS } from "./constant.mjs" 5 | 6 | const rl = new Readline(stdout) 7 | 8 | stdout.clearAll = () => stdout.write('\x1Bc') 9 | stdout.showCursor = () => stdout.write('\u001B[?25h') 10 | stdout.hideCursor = () => stdout.write('\u001B[?25l') 11 | stdout.clearLines = (dy) => { 12 | rl.cursorTo(0) 13 | rl.moveCursor(0, -dy) 14 | // rl.clearScreenDown(); 15 | return rl.commit() 16 | } 17 | 18 | stdout.error = (err) => stdout.write('dns-detector: ' + COLORS.red + err) 19 | 20 | export const watchKeypress = (callback) => { 21 | emitKeypressEvents(process.stdin) 22 | process.stdin.setRawMode(true) 23 | process.stdin.on('keypress', callback) 24 | } 25 | 26 | export { stdout } -------------------------------------------------------------------------------- /lib/ping.mjs: -------------------------------------------------------------------------------- 1 | import { platform } from 'node:os' 2 | import { spawn } from 'node:child_process' 3 | 4 | export class Ping { 5 | constructor(ip) { 6 | this.ping = spawn('ping', [ip]) 7 | 8 | this.ping.stderr.on('data', data => { 9 | this.exit() 10 | }) 11 | } 12 | 13 | onResponse(callback) { 14 | this.ping.stdout.on('data', data => { 15 | callback(this.parse(data.toString())) 16 | }) 17 | } 18 | 19 | parse(out) { 20 | let reg = platform() === 'win32' ? /bytes=(\d+).+time=(\d+)/ : /(\d+)\s*bytes.+time\=(.+)\s*ms/ 21 | const [, received, time] = out.match(reg) || [, '0', 'timeout'] 22 | return { received, time } 23 | } 24 | 25 | exit() { 26 | this.ping.kill('SIGINT') 27 | } 28 | 29 | } 30 | 31 | export class PingQueue extends Map { 32 | exit() { 33 | this.forEach(ping => ping.exit()) 34 | } 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dns-detector", 3 | "version": "1.2.0", 4 | "description": "A nodejs cli tool to resolve host's IPs", 5 | "type": "module", 6 | "module": "index.mjs", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sun0day/dns-detector.git" 13 | }, 14 | "engines": { 15 | "node": ">= 18.0.0" 16 | }, 17 | "files": [ 18 | "bin", 19 | "lib", 20 | "index.mjs" 21 | ], 22 | "keywords": [ 23 | "dns", 24 | "ping", 25 | "cli", 26 | "ip" 27 | ], 28 | "bin": { 29 | "dns": "./bin/cmd.js" 30 | }, 31 | "author": "sun0day", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/sun0day/dns-detector/issues" 35 | }, 36 | "homepage": "https://github.com/sun0day/dns-detector#readme" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sun0day 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/server.mjs: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { Resolver } from 'node:dns/promises' 3 | import { DNS_SERVER, RESOLVE_EVENT } from './constant.mjs' 4 | 5 | export class DnsServer extends EventEmitter { 6 | constructor({ server, timeout = 2000, tries } = {}) { 7 | super(); 8 | this.timeout = timeout 9 | this.tries = tries 10 | this.servers = server ? DNS_SERVER.concat(server) : DNS_SERVER 11 | 12 | this.emit(RESOLVE_EVENT.INIT) 13 | } 14 | 15 | async resolve(host) { 16 | const ips = await Promise.allSettled(this.servers.map(async server => { 17 | const baseParams = { server, host } 18 | 19 | this.emit(RESOLVE_EVENT.PENDING,) 20 | 21 | const resolver = new Resolver({ timeout: this.timeout, tries: this.tries }) 22 | resolver.setServers([server]) 23 | 24 | try { 25 | const ips = await resolver.resolve(host) 26 | const data = { ...baseParams, ips, error: null } 27 | 28 | this.emit(RESOLVE_EVENT.FULFILLED, data) 29 | 30 | return data 31 | } catch (error) { 32 | const data = { ...baseParams, ips: [], error } 33 | 34 | this.emit(RESOLVE_EVENT.REJECT, data) 35 | return data 36 | } 37 | })) 38 | 39 | const data = ips.map(({ value }) => value) 40 | 41 | this.emit(RESOLVE_EVENT.FINISHED, data) 42 | 43 | return data 44 | } 45 | } -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { argv as args } from "node:process"; 4 | import { resolve, stdout } from "../index.mjs"; 5 | 6 | const optionSchema = { 7 | server: { 8 | type: String, 9 | }, 10 | host: { 11 | required: true, 12 | type: String, 13 | }, 14 | timeout: { 15 | default: 2000, 16 | type: Number, 17 | }, 18 | tries: { 19 | default: 4, 20 | type: Number, 21 | }, 22 | }; 23 | 24 | function resolveOptions(args) { 25 | const options = {}; 26 | let optKey = ""; 27 | args.forEach((arg, index) => { 28 | if (index < 2) { 29 | return; 30 | } 31 | 32 | const [key, value] = arg.replace(/^--/, "").split("="); 33 | 34 | if (!optKey && !value && !optionSchema[key]) { 35 | stdout.error(`unknown option ${arg}\n`); 36 | process.exit(1); 37 | } 38 | 39 | if (value) { 40 | options[key] = value; 41 | } else { 42 | if (optKey) { 43 | options[optKey] = key; 44 | optKey = ""; 45 | } else { 46 | optKey = key; 47 | } 48 | } 49 | }); 50 | 51 | Object.keys(optionSchema).forEach((key) => { 52 | const { required, type, default: dv } = optionSchema[key]; 53 | if (!options[key]) { 54 | if (required) { 55 | stdout.error(`${key} option is required\n`); 56 | process.exit(1); 57 | } else { 58 | options[key] = dv; 59 | } 60 | } 61 | options[key] = type(options[key]); 62 | }); 63 | 64 | return options; 65 | } 66 | 67 | resolve(resolveOptions(args)); 68 | -------------------------------------------------------------------------------- /lib/constant.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const DNS_SERVER = [ 3 | '1.1.1.1', 4 | '8.8.8.8', 5 | '199.85.126.10', 6 | '208.67.222.222', 7 | '84.200.69.80', 8 | '8.26.56.26', 9 | '64.6.64.6', 10 | '192.95.54.3', 11 | '81.218.119.11', 12 | '114.114.114.114', 13 | '119.29.29.29', 14 | '223.5.5.5' 15 | ] 16 | 17 | export const RESOLVE_STATUS = { 18 | PENDING: 'pending', 19 | SUCCESS: 'success', 20 | FAIL: 'fail' 21 | } 22 | 23 | export const RESOLVE_EVENT = { 24 | INIT: 'init', 25 | PENDING: 'pending', 26 | FULFILLED: 'fulfilled', 27 | REJECT: 'reject', 28 | FINISHED: 'finished' 29 | } 30 | 31 | export const SPINNER_FRAME = { 32 | bouncingBall: [ 33 | "( ● )", 34 | "( ● )", 35 | "( ●)", 36 | "( ● )", 37 | "( ● )", 38 | "(● )", 39 | ], 40 | bouncingBar: [ 41 | "[ ]", 42 | "[= ]", 43 | "[== ]", 44 | "[=== ]", 45 | "[ ===]", 46 | "[ ==]", 47 | "[ =]", 48 | "[ ]", 49 | "[ =]", 50 | "[ ==]", 51 | "[ ===]", 52 | "[====]", 53 | "[=== ]", 54 | "[== ]", 55 | "[= ]" 56 | ] 57 | } 58 | 59 | export const COLORS = { 60 | reset: '\x1b[0m', 61 | reverse: '\x1b[7m', 62 | 63 | red: '\x1b[31m', 64 | yellow: '\x1b[33m', 65 | green: '\x1b[32m', 66 | cyan: '\x1b[36m', 67 | bright: '\x1b[1m', 68 | magenta: '\x1b[35m', 69 | } 70 | 71 | export const ACTION = { 72 | next: 'next', 73 | prev: 'prev', 74 | setting: 'setting' 75 | } 76 | 77 | export const CHAR = { 78 | check: '✔', 79 | close: '×', 80 | block: '◼', 81 | star: '★' 82 | } -------------------------------------------------------------------------------- /lib/ip.mjs: -------------------------------------------------------------------------------- 1 | import { ACTION } from './constant.mjs' 2 | import { getRealTime } from './utils.mjs' 3 | 4 | export class IP { 5 | constructor({ server, time, addr, received = 0, resolveTime, selected }) { 6 | this.addr = addr 7 | this.server = server 8 | this.time = time 9 | this.received = received 10 | this.resolveTime = resolveTime 11 | this.selected = selected 12 | } 13 | } 14 | 15 | export class IPQueue extends Map { 16 | getAllIPs() { 17 | return Array.from(this.values()) 18 | } 19 | 20 | sortByTime() { 21 | return this.getAllIPs().filter(ip => !!ip.time).sort((next, cur) => getRealTime(next.time) - getRealTime(cur.time)) 22 | } 23 | 24 | sortByResolveTime() { 25 | return this.getAllIPs().sort((next, cur) => getRealTime(next.resolveTime) - getRealTime(cur.resolveTime)) 26 | } 27 | 28 | groupByServer() { 29 | const group = {} 30 | 31 | this.sortByResolveTime().forEach(ip => { 32 | group[ip.server] ??= [] 33 | group[ip.server].push(ip) 34 | }) 35 | 36 | return group 37 | } 38 | 39 | selectIP(action) { 40 | const ips = this.sortByResolveTime() 41 | 42 | let index = ips.findIndex(ip => ip.selected) 43 | 44 | switch (action) { 45 | case ACTION.next: 46 | ips[(index + 1) % ips.length].selected = true 47 | 48 | if (index > -1) { 49 | ips[index].selected = false 50 | } 51 | break 52 | case ACTION.prev: 53 | ips[(index ? index : ips.length) - 1].selected = true 54 | 55 | if (index > -1) { 56 | ips[index].selected = false 57 | } 58 | } 59 | } 60 | 61 | getSelectedIP() { 62 | return this.getAllIPs().find(ip => ip.selected) 63 | } 64 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | image 3 | 4 |
5 | A tiny nodejs cli tool to resolve host IPs and find the fastest IP 6 |
7 |
8 | 9 | node-current 10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 | ## Feature 18 | 19 | - :mag: resolve host IPs 20 | - :rocket: IP latency test 21 | - :potted_plant: super tiny, no 3rd-party dependencies 22 | - :whale: cross-platform support 23 | - :hammer: system host IP setting(Enter) 24 | 25 | 26 | ## Screenshot 27 | 28 | 29 | ![dns](https://user-images.githubusercontent.com/102238922/220001043-b5584e16-c43b-49e3-a9ae-55200b61d043.gif) 30 | 31 | 32 | 33 | 34 | ## Install 35 | 36 | ```shell 37 | $ npm i -g dns-detector 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```shell 43 | $ dns --host {your host} --server {DNS server} --timeout {query timeout} --tries {query retry times} 44 | ``` 45 | 46 | ## Options 47 | 48 | |option|required|default|description 49 | |-----|-----|-----|------| 50 | |--host|:heavy_check_mark:||host you want to resolve| 51 | |--server|||DNS server IP to resolve host 52 | |--timeout||2000|query timeout, same as [nodejs dns resolveroptions.timeout](https://nodejs.org/dist/latest-v18.x/docs/api/dns.html#resolveroptions) 53 | |--tries||4|query timeout, same as [nodejs dns resolveroptions.tries](https://nodejs.org/dist/latest-v18.x/docs/api/dns.html#resolveroptions) 54 | 55 | ## Embedded DNS Server 56 | 57 | `dns-detector` also embeds some famous DNS servers to help resolve IP address. 58 | 59 | - 1.1.1.1 60 | - 8.8.8.8 61 | - 199.85.126.10 62 | - 208.67.222.222 63 | - 84.200.69.80 64 | - 8.26.56.26 65 | - 64.6.64.6 66 | - 192.95.54.3 67 | - 81.218.119.11 68 | - 114.114.114.114 69 | - 119.29.29.29 70 | - 223.5.5.5 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | ACTION, 3 | COLORS, 4 | RESOLVE_EVENT, 5 | RESOLVE_STATUS, 6 | } from "./lib/constant.mjs"; 7 | import { DnsServer } from "./lib/server.mjs"; 8 | import { IP, IPQueue } from "./lib/ip.mjs"; 9 | import { Ping, PingQueue } from "./lib/ping.mjs"; 10 | import { Painter } from "./lib/painter.mjs"; 11 | import { interval, sleep } from "./lib/utils.mjs"; 12 | import { stdout, watchKeypress } from "./lib/stdout.mjs"; 13 | import { HostSetting } from "./lib/host.mjs"; 14 | 15 | export { COLORS }; 16 | 17 | export { stdout }; 18 | 19 | export async function resolve(options) { 20 | const { host } = options; 21 | const hostSetting = new HostSetting(host); 22 | const startTime = new Date(); 23 | const ipQueue = new IPQueue(); 24 | const pingQueue = new PingQueue(); 25 | let resolveStatus = RESOLVE_STATUS.PENDING; 26 | 27 | const server = new DnsServer(options); 28 | const painter = new Painter(host); 29 | 30 | await hostSetting.getContent(); 31 | server.resolve(host); 32 | painter.print(ipQueue, resolveStatus, hostSetting); 33 | 34 | interval(() => painter.print(ipQueue, resolveStatus, hostSetting), 100); 35 | 36 | let isFirstIp = false; 37 | server.on(RESOLVE_EVENT.FULFILLED, (data) => { 38 | data.ips.forEach((addr) => { 39 | let ip = ipQueue.get(addr); 40 | 41 | if (ip) { 42 | return; 43 | } 44 | 45 | ip = new IP({ 46 | server: data.server, 47 | addr, 48 | resolveTime: new Date() - startTime, 49 | selected: !isFirstIp, 50 | }); 51 | isFirstIp = true; 52 | 53 | ipQueue.set(addr, ip); 54 | 55 | const ping = new Ping(addr); 56 | 57 | ping.onResponse((res) => { 58 | ip.received ||= 0; 59 | ip.received += +res.received; 60 | ip.time = res.time; 61 | }); 62 | 63 | pingQueue.set(addr, ping); 64 | }); 65 | }); 66 | 67 | server.on(RESOLVE_EVENT.FINISHED, async (data) => { 68 | resolveStatus = RESOLVE_STATUS.SUCCESS; 69 | 70 | if (!ipQueue?.size) { 71 | resolveStatus = RESOLVE_STATUS.FAIL; 72 | await sleep(100); 73 | stdout.error( 74 | `can not resolve ${host}, please make sure host exists and is reachable\n` 75 | ); 76 | process.exit(1); 77 | } 78 | }); 79 | 80 | function onExit(code) { 81 | stdout.showCursor(); 82 | pingQueue.exit(); 83 | 84 | process.exit(typeof code === "number" ? code : 0); 85 | } 86 | 87 | process.on("exit", onExit); 88 | process.on("SIGINT", onExit); 89 | process.on("uncaughtException", (err) => { 90 | console.error( 91 | painter.color( 92 | err + (err.code === "EACCES" ? "\nAre you running as root?" : ""), 93 | COLORS.red 94 | ) 95 | ); 96 | onExit(1); 97 | }); 98 | watchKeypress((str, key) => { 99 | if (key.ctrl && ["c", "d"].includes(key.name)) { 100 | onExit(0); 101 | } 102 | 103 | const move = { 104 | left: ACTION.prev, 105 | right: ACTION.next, 106 | }[key.name]; 107 | 108 | move && ipQueue.selectIP(move); 109 | 110 | const selectedIP = ipQueue.getSelectedIP(); 111 | if (key.name === "return" && selectedIP) { 112 | hostSetting.setHostIP(selectedIP); 113 | } 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /lib/painter.mjs: -------------------------------------------------------------------------------- 1 | import { getRealTime, sleep } from './utils.mjs' 2 | import { stdout } from './stdout.mjs' 3 | import { Spinner } from './spinner.mjs' 4 | import { COLORS, RESOLVE_STATUS, CHAR } from './constant.mjs' 5 | 6 | export class Painter { 7 | constructor(host) { 8 | this.host = host 9 | this.lines = 0 10 | this.divider = new Array(stdout.columns).fill(' ').join('') 11 | this.columns = [{ 12 | title: 'IP', 13 | dataIndex: 'addr', 14 | render: (addr, ip) => this.color(addr, COLORS.cyan) + (ip.selected ? this.color(`${CHAR.star}`, COLORS.green) : '') 15 | }, { 16 | title: 'DNS Server', 17 | dataIndex: 'server', 18 | }, { 19 | title: 'Received(B)', 20 | dataIndex: 'received', 21 | }, { 22 | title: 'Time(ms)↑', 23 | dataIndex: 'time', 24 | }, { 25 | title: 'Latency↑', 26 | dataIndex: 'time' 27 | }] 28 | 29 | this.resolveTitle = `${this.color('Resolving <')}${this.color(host, COLORS.yellow)}${this.color('> IP...')}` 30 | this.resolveSpinner = new Spinner('bouncingBar') 31 | 32 | this.pingTitle = `${this.color('Querying <')}${this.color(host, COLORS.yellow)}${this.color('> IP...')}` 33 | this.pingSpinner = new Spinner('bouncingBall') 34 | 35 | this.setHeader() 36 | 37 | stdout.on('resize', () => { 38 | stdout.clearAll() 39 | this.divider = new Array(stdout.columns).fill(' ').join('') 40 | this.setHeader() 41 | }) 42 | } 43 | 44 | async print(ipQueue, resolveStatus, hostSetting) { 45 | stdout.hideCursor() 46 | 47 | if (this.lines > 0) { 48 | await stdout.clearLines(this.lines) 49 | } 50 | 51 | const output = ipQueue.size ? this.renderResolve(ipQueue, resolveStatus).concat(this.renderPing(ipQueue)).concat(this.renderHostSetting(hostSetting)) : this.renderResolve(ipQueue, resolveStatus) 52 | this.lines = output.length 53 | 54 | stdout.write(output.map(out => this.pad(out, stdout.columns)).join('\n') + '\n' + COLORS.cyan) 55 | } 56 | 57 | setHeader() { 58 | this.cellWidth = parseInt(stdout.columns / this.columns.length) 59 | this.tHeader = this.color(this.columns.reduce((str, col) => str + this.renderCell(col.title), '')) 60 | this.tDivider = this.color(this.columns.reduce((acc, col) => acc + new Array(this.cellWidth).fill('─').join(''), '')) 61 | } 62 | 63 | renderResolve(ipQueue, resolveStatus) { 64 | const serverGroup = ipQueue.groupByServer() 65 | let titlePrefix = this.color(this.resolveSpinner.text()) 66 | 67 | switch (resolveStatus) { 68 | case RESOLVE_STATUS.SUCCESS: 69 | titlePrefix = this.color(`[${new Array(4).fill(CHAR.check).join('')}]`, COLORS.green) 70 | break; 71 | case RESOLVE_STATUS.FAIL: 72 | titlePrefix = this.color(`[${new Array(4).fill(CHAR.close).join('')}]`, COLORS.red) 73 | } 74 | 75 | return [this.divider, `${titlePrefix} ${this.resolveTitle}`, this.divider, ...Object.keys(serverGroup).reduce((acc, server) => { 76 | const { resolveTime } = serverGroup[server][0] 77 | const prefix = this.color(`+ ${server} > ${this.color(resolveTime + 'ms', this.getTimeColor(resolveTime))}${this.color(' >>')}`) 78 | serverGroup[server].reduce((prev, ip, index) => { 79 | const addr = ip.selected ? this.highlight(ip.addr) : this.color(ip.addr) 80 | let str = prev + ' ' + addr 81 | if (this.removeColor(str).length <= stdout.columns) { 82 | index === serverGroup[server].length - 1 && acc.push(str) 83 | return str 84 | } else { 85 | acc.push(prev) 86 | return prefix + ' ' + addr 87 | } 88 | }, prefix) 89 | 90 | return acc 91 | }, [])] 92 | } 93 | 94 | renderPing(ipQueue) { 95 | this.columns[4].render = (time) => { 96 | const len = Math.min(Math.round(getRealTime(time) / 10), this.cellWidth - 2) 97 | return this.color(this.pad('[' + new Array(len).fill(CHAR.block).join(''), this.cellWidth - 1) + ']', this.getTimeColor(time)) 98 | } 99 | 100 | return [this.divider, `${this.color(this.pingSpinner.text())} ${this.pingTitle}`, this.divider, this.tHeader, this.tDivider, ...ipQueue.sortByTime().map((ip, index) => this.renderRow(ip, index))] 101 | } 102 | 103 | renderHostSetting(hostSetting) { 104 | return hostSetting.ip ? ['', `${this.color(CHAR.check + ' <')}${this.color(hostSetting.host, COLORS.yellow)}${this.color(`> IP is set to `)}${this.color(hostSetting.ip.addr, COLORS.yellow)} ${this.color('now')}`] : [] 105 | } 106 | 107 | renderRow(ip, index) { 108 | return this.columns.reduce((str, col) => { 109 | const value = ip[col.dataIndex] 110 | return str + this.renderCell(col.render ? col.render(value, ip, index) : this.color(value, COLORS.cyan)) 111 | }, '') 112 | } 113 | 114 | renderCell(str) { 115 | return this.pad(str, this.cellWidth) 116 | } 117 | 118 | pad(str, width, char = ' ') { 119 | const plainStr = this.removeColor(str.toString()) 120 | 121 | return `${str}${new Array(Math.abs(width - plainStr.length)).fill(char).join('')}` 122 | } 123 | 124 | color(text, colors = COLORS.cyan) { 125 | const realColors = colors instanceof Array ? colors : [colors] 126 | realColors.push(COLORS.bright) 127 | 128 | return `${realColors.join('')}${text}${COLORS.reset}` 129 | } 130 | 131 | removeColor(text) { 132 | function escape(s) { 133 | return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') 134 | }; 135 | 136 | function replaceAll(str, pattern, replacement) { 137 | return str.replace(new RegExp(escape(pattern), "g"), replacement); 138 | } 139 | 140 | return Object.values(COLORS).reduce((text, color) => { 141 | return replaceAll(text, color, '') 142 | }, text) 143 | } 144 | 145 | getTimeColor(time) { 146 | let color = COLORS.green 147 | const realTime = getRealTime(time) 148 | 149 | if (realTime > 100 && realTime < 200) { 150 | color = COLORS.yellow 151 | } else if (realTime > 200) { 152 | color = COLORS.magenta 153 | } 154 | 155 | return color 156 | } 157 | 158 | highlight(text) { 159 | return this.color(text, [COLORS.reverse, COLORS.cyan]) 160 | } 161 | } --------------------------------------------------------------------------------