mirror of https://github.com/bobwen-dev/htpdate
Initial commit
This commit is contained in:
parent
b8c67fd69b
commit
135536db2c
|
@ -0,0 +1,57 @@
|
||||||
|
util = require 'util'
|
||||||
|
{ spawn } = require 'child_process'
|
||||||
|
dayjs = require 'dayjs'
|
||||||
|
dayjs.extend require './dayjs_format_ms'
|
||||||
|
platform = require('os').platform()
|
||||||
|
|
||||||
|
|
||||||
|
adjust_time = (delta) ->
|
||||||
|
console.log 'Adjust time...'
|
||||||
|
shell = spawn spawn_args...
|
||||||
|
shell.stderr.on 'data', (data) -> console.error "> " + "#{data}".trim()
|
||||||
|
shell.stdout.on 'data', (data) -> console.debug "> " + "#{data}".trim()
|
||||||
|
shell.on 'close', (code) ->
|
||||||
|
shell.stdin.end()
|
||||||
|
console.log 'Done'
|
||||||
|
process.exit 0
|
||||||
|
wait_data = util.promisify (cb) ->
|
||||||
|
shell.stdout.once 'data', (chunk) ->
|
||||||
|
setTimeout ->
|
||||||
|
cb null, chunk
|
||||||
|
, 1
|
||||||
|
input_line = util.promisify (cmd, cb) ->
|
||||||
|
console.debug "$ #{cmd}"
|
||||||
|
shell.stdin.write "#{cmd}\n", (p...) ->
|
||||||
|
await wait_data() if platform in ['win32']
|
||||||
|
cb p...
|
||||||
|
await wait_data() if platform in ['win32']
|
||||||
|
cmd = dayjs().add(delta, 'ms').format adjust_time.command
|
||||||
|
await input_line cmd
|
||||||
|
await input_line 'exit'
|
||||||
|
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
win32: '[time ]HH:mm:ss.SS[ && date ]YYYY-MM-DD'
|
||||||
|
linux: '[date -s ]YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
|
}
|
||||||
|
adjust_time.command = COMMANDS[platform] or COMMANDS.linux
|
||||||
|
|
||||||
|
|
||||||
|
SHELL_ARGS = {
|
||||||
|
win32: [
|
||||||
|
'cmd'
|
||||||
|
['/q', '/k', 'prompt $H && chcp 65001 > nul']
|
||||||
|
{
|
||||||
|
windowsHide: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
linux: [
|
||||||
|
'sh'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
spawn_args = SHELL_ARGS[platform] or SHELL_ARGS.linux
|
||||||
|
if not /UTF\-8/i.test (process.env.LANGUAGE or '')
|
||||||
|
process.env.LANGUAGE = 'C.UTF-8'
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = adjust_time
|
|
@ -0,0 +1,122 @@
|
||||||
|
path = require 'path'
|
||||||
|
util = require 'util'
|
||||||
|
minimist = require 'minimist'
|
||||||
|
minimist_opt = require 'minimist-options'
|
||||||
|
{ orderBy } = require 'natural-orderby'
|
||||||
|
info = require './package.json'
|
||||||
|
platform = require('os').platform()
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_OPTIONS = {
|
||||||
|
help: {
|
||||||
|
describe: 'This help text'
|
||||||
|
type: 'boolean'
|
||||||
|
alias: 'h'
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
version: {
|
||||||
|
describe: "display the version of #{info.name} and exit"
|
||||||
|
type: 'boolean'
|
||||||
|
alias: 'v'
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
print_version = ->
|
||||||
|
console.log """
|
||||||
|
#{info.name} #{info.version}, #{info.description}
|
||||||
|
|
||||||
|
Copyright (C) 2021 Bob Wen. All rights reserved.
|
||||||
|
|
||||||
|
License: AGPL-3.0
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation; either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Affero General Public License for more details.
|
||||||
|
|
||||||
|
Libraries:
|
||||||
|
"""
|
||||||
|
for lib_name, version of info.dependencies
|
||||||
|
lib_info = require "./node_modules/#{lib_name}/package.json"
|
||||||
|
console.log """
|
||||||
|
#{lib_info.name} #{lib_info.version}
|
||||||
|
License: #{lib_info.license}
|
||||||
|
Homepage: #{lib_info.homepage.split('#')[0]}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print_usage = (options) ->
|
||||||
|
items = []
|
||||||
|
for k, v of options
|
||||||
|
items.push {
|
||||||
|
key: k
|
||||||
|
v...
|
||||||
|
}
|
||||||
|
items = orderBy items, (v) -> v.alias ? v.key
|
||||||
|
console.log """
|
||||||
|
#{info.name} #{info.version}, #{info.description}
|
||||||
|
Usage: #{info.name} [options...] urls...
|
||||||
|
|
||||||
|
Options
|
||||||
|
"""
|
||||||
|
for o in items
|
||||||
|
if o.alias?
|
||||||
|
line = " -#{o.alias}, --#{o.key}".padEnd 24
|
||||||
|
else
|
||||||
|
line = " --#{o.key}".padEnd 24
|
||||||
|
line += "#{o.describe or ''}\n"
|
||||||
|
if o.default?
|
||||||
|
line += "".padStart(24) + "Default: #{util.inspect o.default}"
|
||||||
|
else if o.type?
|
||||||
|
line += "".padStart(24) + "Type: #{o.type}"
|
||||||
|
console.log line + '\n'
|
||||||
|
|
||||||
|
|
||||||
|
print_examples = (exam = []) ->
|
||||||
|
console.log """
|
||||||
|
Examples
|
||||||
|
Get time from multiple URLs
|
||||||
|
#{info.name} www.pool.ntp.org www.openssl.org nodejs.org
|
||||||
|
|
||||||
|
Change default protocol to 'http'
|
||||||
|
#{info.name} --protocol http www.pool.ntp.org
|
||||||
|
|
||||||
|
Mix http url and https url
|
||||||
|
#{info.name} http://www.pool.ntp.org https://www.openssl.org
|
||||||
|
|
||||||
|
Access through a http proxy
|
||||||
|
#{if platform is 'win32' then 'set' else 'export'} http_proxy=http://127.0.0.1:8118
|
||||||
|
#{info.name} www.pool.ntp.org
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = (opt, exam) ->
|
||||||
|
cli_options = {
|
||||||
|
DEFAULT_OPTIONS...
|
||||||
|
opt...
|
||||||
|
}
|
||||||
|
argv = minimist process.argv.slice(2), minimist_opt cli_options
|
||||||
|
if argv.version
|
||||||
|
print_version()
|
||||||
|
process.exit 0
|
||||||
|
if argv.help or argv._.length is 0
|
||||||
|
print_usage cli_options
|
||||||
|
print_examples exam
|
||||||
|
console.log()
|
||||||
|
if argv.help
|
||||||
|
process.exit 0
|
||||||
|
console.error "\nError: Missing server url, at least one url should be specified"
|
||||||
|
process.exit 0
|
||||||
|
if argv.count < 1
|
||||||
|
argv.count = 1
|
||||||
|
argv
|
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = (o, c, d) ->
|
||||||
|
proto = c.prototype
|
||||||
|
old_format = proto.format
|
||||||
|
proto.format = (fmt_str = 'YYYY-MM-DDTHH:mm:ssZ') ->
|
||||||
|
locale = @$locale()
|
||||||
|
utils = @$utils()
|
||||||
|
r = fmt_str.replace /\[([^\]]+)]|SSS|S{2,2}|S/g, (word) =>
|
||||||
|
switch word
|
||||||
|
when 'S'
|
||||||
|
"#{@$ms}"
|
||||||
|
when 'SS'
|
||||||
|
ms = Math.round(@$ms / 10)
|
||||||
|
"#{ms}".padStart(2, '0')
|
||||||
|
else
|
||||||
|
word
|
||||||
|
old_format.bind(this) r
|
|
@ -0,0 +1,130 @@
|
||||||
|
util = require 'util'
|
||||||
|
got = require 'got'
|
||||||
|
dayjs = require 'dayjs'
|
||||||
|
info = require './package.json'
|
||||||
|
median = require './median'
|
||||||
|
adjust_time = require './adjust_time'
|
||||||
|
|
||||||
|
|
||||||
|
argv = require('./argv') {
|
||||||
|
threshold: {
|
||||||
|
alias: 't'
|
||||||
|
describe: 'At least how many milliseconds are considered to adjust system time'
|
||||||
|
default: 1500
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
set: {
|
||||||
|
describe: 'Adjust system time if necessary'
|
||||||
|
alias: 's'
|
||||||
|
default: false
|
||||||
|
type: 'boolean'
|
||||||
|
}
|
||||||
|
protocol: {
|
||||||
|
describe: 'Use this protocol when no protocol is specified in the url'
|
||||||
|
alias: 'p'
|
||||||
|
default: 'https'
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
method: {
|
||||||
|
describe: 'HTTP method'
|
||||||
|
alias: 'm'
|
||||||
|
default: 'HEAD'
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
describe: 'The number of requests for each url'
|
||||||
|
alias: 'c'
|
||||||
|
default: 4
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
redirect: {
|
||||||
|
describe: 'If redirect responses should be followed'
|
||||||
|
alias: 'r'
|
||||||
|
default: false
|
||||||
|
type: 'boolean'
|
||||||
|
}
|
||||||
|
timeout: {
|
||||||
|
alias: 'T'
|
||||||
|
default: 6000
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
command: {
|
||||||
|
describe: 'Command to adjust system time, in https://day.js.org/ display format'
|
||||||
|
alias: 'C'
|
||||||
|
default: adjust_time.command
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
interval: {
|
||||||
|
describe: 'The minimum milliseconds between requests'
|
||||||
|
alias: 'i'
|
||||||
|
default: 500
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
'user-agent': {
|
||||||
|
alias: 'u'
|
||||||
|
default: "#{info.name}/#{info.version}"
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjust_time.command = argv.command
|
||||||
|
|
||||||
|
|
||||||
|
req_opt = {
|
||||||
|
method: argv.method.trim().toUpperCase()
|
||||||
|
followRedirect: argv.redirect
|
||||||
|
timeout: argv.timeout
|
||||||
|
retry: 0
|
||||||
|
dnsCache: true
|
||||||
|
cache: false
|
||||||
|
headers: {
|
||||||
|
'user-agent': argv['user-agent']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get_time_delta = (url) ->
|
||||||
|
if not /^https?:\/\//.test url
|
||||||
|
url = "#{argv.protocol}://#{url}"
|
||||||
|
console.log "#{argv.method} #{url}"
|
||||||
|
for i in [1 .. argv.count]
|
||||||
|
step = "\##{i}: ".padStart 8
|
||||||
|
start_at = Date.now()
|
||||||
|
try
|
||||||
|
r = await got url, req_opt
|
||||||
|
catch e
|
||||||
|
if not e.response?
|
||||||
|
console.log "#{step}#{e}"
|
||||||
|
wait_ms = argv.interval + start_at - Date.now()
|
||||||
|
if wait_ms > 0
|
||||||
|
await delay wait_ms
|
||||||
|
continue
|
||||||
|
r = e.response
|
||||||
|
wait_ms = argv.interval + start_at - Date.now()
|
||||||
|
if wait_ms > 0
|
||||||
|
await delay wait_ms
|
||||||
|
duration = r.timings.end - r.timings.upload
|
||||||
|
server_moment = dayjs r.headers.date
|
||||||
|
delta = Math.round(server_moment - r.timings.end - duration / 2 + 500)
|
||||||
|
console.log "#{step}" + "#{delta} ms".padStart 10
|
||||||
|
delta
|
||||||
|
|
||||||
|
|
||||||
|
delay = util.promisify (ms, cb) ->
|
||||||
|
setTimeout cb, ms
|
||||||
|
|
||||||
|
|
||||||
|
do ->
|
||||||
|
values = []
|
||||||
|
for url in argv._
|
||||||
|
values.push (await get_time_delta url)...
|
||||||
|
if values.length is 0
|
||||||
|
console.log "Network failure"
|
||||||
|
process.exit 2
|
||||||
|
return
|
||||||
|
delta = median values
|
||||||
|
console.log "Median: " + "#{delta} ms".padStart 10
|
||||||
|
return if not argv.set
|
||||||
|
if Math.abs(delta) < argv.threshold
|
||||||
|
console.log "There is no need to adjust the time"
|
||||||
|
return
|
||||||
|
await adjust_time delta
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Select the kth element in arr
|
||||||
|
quickselect = (arr, k) ->
|
||||||
|
return arr[0] if arr.length is 1
|
||||||
|
pivot = arr[0]
|
||||||
|
lows = arr.filter (e) -> e < pivot
|
||||||
|
highs = arr.filter (e) -> e > pivot
|
||||||
|
pivots = arr.filter (e) -> e is pivot
|
||||||
|
if k < lows.length
|
||||||
|
# the pivot is too high
|
||||||
|
quickselect lows, k
|
||||||
|
else if k < lows.length + pivots.length
|
||||||
|
# We got lucky and guessed the median
|
||||||
|
pivot
|
||||||
|
else
|
||||||
|
# the pivot is too low
|
||||||
|
quickselect highs, k - lows.length - pivots.length
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = (arr) ->
|
||||||
|
L = arr.length
|
||||||
|
halfL = L / 2
|
||||||
|
if (L % 2) is 1
|
||||||
|
quickselect arr, halfL
|
||||||
|
else
|
||||||
|
0.5 * (quickselect(arr, halfL - 1) + quickselect(arr, halfL))
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"name": "htpdate",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "a tool to synchronize system time with web servers",
|
||||||
|
"keywords": [
|
||||||
|
"htp",
|
||||||
|
"htpdate",
|
||||||
|
"ntp",
|
||||||
|
"ntp-client"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Bob Wen",
|
||||||
|
"email": "bobwen@tutanota.com",
|
||||||
|
"url": "https://github.com/bobwen-dev"
|
||||||
|
},
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"homepage": "https://github.com/bobwen-dev/htpdate",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/bobwen-dev/htpdate/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/bobwen-dev/htpdate.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"compile": "npx coffee -bc --no-header ./",
|
||||||
|
"clean": "rm *.js",
|
||||||
|
"b": "npx nexe -i index.js -r node_modules/**/package.json",
|
||||||
|
"b-win-x86": "npm run b -- -t windows-x86-14.15.3 -o dists/htpdate-x86.exe",
|
||||||
|
"b-win-x64": "npm run b -- -t windows-x64-14.15.4 -o dists/htpdate-x64.exe",
|
||||||
|
"b-linux-x64": "npm run b -- -t linux-x64-14.15.3 -o dists/htpdate-linux-x64",
|
||||||
|
"b-linux-arm64": "npm run b -- -t linux-arm64-14.15.4 -o dists/htpdate-linux-arm64",
|
||||||
|
"b-mac-x64": "npm run b -- -t mac-x64-14.15.3 -o dists/htpdate-mac-x64",
|
||||||
|
"b-all": "npm run b-win-x86 && npm run b-win-x64 && npm run b-linux-x64 && npm run b-linux-arm64 && npm run b-mac-x64",
|
||||||
|
"build": "npm run compile && npm run b-all && npm run clean",
|
||||||
|
"test": "npx coffee index.coffee www.pool.ntp.org"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.10.3",
|
||||||
|
"got": "^11.8.0",
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"minimist-options": "^4.1.0",
|
||||||
|
"natural-orderby": "^2.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"coffeescript": "^2.5.1",
|
||||||
|
"nexe": "^4.0.0-beta.16"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue