@luminati-io/luminati-proxy
Version:
A configurable local proxy for luminati.io
693 lines (654 loc) • 22.2 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*zlint node*/
require('./config.js');
const fs = require('fs');
const string = require('./string.js');
const zerr = require('./zerr.js');
const file = require('./file.js');
const etask = require('./etask.js');
const efile = require('./efile.js');
const array = require('./array.js');
const zutil = require('./util.js');
const assert = require('assert');
const cli = require('./cli.js');
const exec = require('./exec.js');
const os = require('os');
const E = exports;
const env = process.env, qw = string.qw, KB = 1024, MB = KB*KB;
var ffi_napi;
try { ffi_napi = require('ffi-napi'); } catch(e){}
const libc = ffi_napi && !file.is_darwin ? ffi_napi.Library('libc',
{fallocate: ['int', ['int', 'int', 'long', 'long']]}) : undefined;
const BIN_IP = '/bin/ip';
var distro_release;
var procfs_fmt = {
cpu: qw`cpu user nice system idle iowait irq softirq steal guest
guest_nice`,
pstat: qw`pid tcomm state ppid pgrp sid tty_nr tty_pgrp flags min_flt
cmin_flt maj_flt cmaj_flt utime stime cutime cstime priority nice
num_threads it_real_value start_time vsize rss rsslim start_code
end_code start_stack esp eip pending blocked sigign sigcatch notused
notused notused exit_signal task_cpu rt_priority policy blkio_ticks
gtime cgtime start_data end_data start_brk arg_start arg_end env_start
env_end exit_code`,
filenr: qw`open unused max`,
diskstats: qw`major minor reads reads_merged reads_sector reads_ms writes
writes_merged writes_sector writes_ms io_current io_ms io_weighted_ms`,
};
let cyg_readdir = dir=>
file.is_win ? exec.get_lines(`ls -1 ${dir}`) : file.readdir(dir);
let cyg_read_lines = path=>
file.is_win ? exec.get_lines(`cat ${path}`) : file.read_lines(path);
let cyg_read = path=>file.is_win ?
exec.get(`cat ${path}`, {out: 'stdout', stdio: 'pipe'}) : file.read(path);
// XXX vadim: all procfs-related funcs should use this and not magic numbers
function read_procfs_line(filepath, type){
var str;
if (!(str = cyg_read(filepath)))
return;
var res = {}, parts = str.split(/ +/);
procfs_fmt[type].forEach((name, idx)=>res[name] = parts[idx]||0);
return res;
}
// on some machines the lines are in a different order
E.meminfo_parse = function(info){
var n = string.split_nl(info), mem = {};
for (var i=0; i<n.length; i++)
{
if (!n[i])
continue;
var m = /^([A-Za-z0-9_()]+):\s+([0-9]+)( kB)?$/.exec(n[i]);
if (!m)
zerr(`meminfo_parse: can't parse line "${n[i]}"`);
assert(m);
switch (m[1])
{
case 'MemTotal': mem.memtotal = m[2]*KB; break;
case 'MemFree': mem.memfree = m[2]*KB; break;
case 'Buffers': mem.buffers = m[2]*KB; break;
case 'Cached': mem.cached = m[2]*KB; break;
case 'MemAvailable': mem.memfree_all = m[2]*KB; break;
}
}
return mem;
};
// os.freemem does not include buffers and cached as freemem
E.meminfo = function(){
if (file.is_darwin)
return {memtotal: os.totalmem(), memfree_all: os.freemem()};
var info = cyg_read('/proc/meminfo');
var mem = E.meminfo_parse(info);
mem.buffers = mem.buffers||0; // openvz does not always have Buffers
mem.cached = mem.cached||0; // cygwin does not have Cached
mem.memfree_all = mem.memfree_all||mem.memfree+mem.buffers+mem.cached;
return mem;
};
E.freemem = function(){ return E.meminfo().memfree_all; };
E.totalmem = function(){ return E.meminfo().memtotal; };
E.mem_usage = function(){
var info = E.meminfo();
return (info.memtotal-info.memfree_all)/info.memtotal;
};
E.freemem_percent = function(){ return 100*(1-E.mem_usage()); };
E.get_release = function(no_cache){
if (distro_release && !no_cache)
return distro_release;
distro_release = {};
if (file.is_win)
{
const lines = exec.get_lines('systeminfo.exe');
const values = {id: /OS Name:\s+(.+)/, version: /OS Version:\s+(.+)/};
for (let l of lines)
{
let v;
for (let k in values)
{
if (v = values[k].exec(l))
distro_release[k] = v[1];
}
}
}
else if (!file.is_darwin)
{
let info = exec.get_lines(['lsb_release', '-i', '-r', '-c', '-s']);
if (!info.length)
{
const os_info = exec.get_lines('cat /etc/os-release');
const get = k=>{
const start = `${k.toUpperCase()}=`;
const data = os_info.find(l=>l.startsWith(start));
return data &&
data.slice(start.length).replace(/^"|"$/g, '') || '';
};
info = [get('id'), get('version_id'), get('version_codename')];
}
distro_release = {
id: info[0].toLowerCase(),
version: info[1],
codename: info[2].toLowerCase(),
};
}
return distro_release;
};
E.is_release = function(releases, no_cache){
releases = array.to_array(releases);
E.get_release(no_cache);
return releases.some(function(e){
var m = e.toLowerCase().match(/^(i|v|c):(.*)$/);
switch (m[1])
{
case 'i':
return distro_release.id==m[2];
case 'v':
return distro_release.version==m[2];
case 'c':
return distro_release.codename==m[2];
}
});
};
E.fallocate = function(fd, len, offset, mode){
if (libc.fallocate(fd, mode||0, offset||0, len)!=0)
{
let errno = ffi_napi.errno();
if (errno==95) // EOPNOTSUPP
fs.ftruncateSync(fd, len);
else
throw new Error(`fallocate failed: ${errno}`);
}
};
var swapfile = '/tmp/zapt.swap';
E.swapon = function(){
if (!file.is_file(swapfile) || file.size(swapfile)<512*MB)
{
cli.exec_rt_e('rm -f '+swapfile);
// XXX sergey: we cannot use it anyway, it disabled by hoster
if (cli.exec_rt('fallocate -l 512M '+swapfile))
return;
cli.exec_rt_e('mkswap '+swapfile);
}
// XXX sergey: some openvz does not support swapon, ignore it
cli.exec_rt('swapon '+swapfile);
};
E.swapoff = function(){ cli.exec_rt('swapoff '+swapfile); };
E.swap_usage = function(){
if (file.is_darwin)
return {count: 0, usage: 0};
let swaps = cyg_read_lines('/proc/swaps').slice(1);
let ret = {count: swaps.length, usage: 0};
if (!swaps.length)
return ret;
let data = swaps.reduce((p, line)=>{
let splitted = line.split('\t');
p.total += +splitted[splitted.length-3];
p.used += +splitted[splitted.length-2];
return p;
}, {total: 0, used: 0});
ret.usage = data.total ? data.used/data.total*100 : 0;
return ret;
};
E.check_space = function(req){
return +exec.get_line('df --output=avail -k / | grep -iv avail')>req;
};
function cpu_diff(prev, curr){
var d = {};
for (var i in curr)
d[i] = curr[i]-prev[i];
// vitaly: ignoring steal diff<0 to mitigate decreasing steal
// https://0xstubs.org/debugging-a-flaky-cpu-steal-time-counter-on-a-paravirtualized-xen-guest/
d.busy = d.user+d.nice+d.system+d.irq+d.softirq+Math.max(d.steal, 0)
+d.guest+d.guest_nice;
d.total = d.busy+d.idle+d.iowait;
if (d.total>0)
d.busy_ratio = d.busy/d.total;
return d;
}
function cpus_diff(prev, curr){
var diff = [];
for (var i = 0; i<curr.length; i++)
diff.push(cpu_diff(prev[i], curr[i]));
diff.all = cpu_diff(prev.all, curr.all);
return diff;
}
// XXX vadim: cleanup
E.cpus = function(){
var cpus = [];
var items = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq',
'steal', 'guest', 'guest_nice'];
if (file.is_darwin)
{
const names = {system: 'sys'};
const res = os.cpus();
for (let i=0; i<res.length; i++)
{
let c = cpus[i] = {};
for (let item of items)
c[item] = res[i].times[names[item]||item]|0;
}
cpus.all = {};
for (let item of items)
cpus.all[item] = cpus.reduce((p, c)=>p+c[item], 0)/res.length;
return cpus;
}
var ll = cyg_read_lines('/proc/stat');
ll.forEach(l=>{
if (!/^cpu\d* /.test(l))
return;
l = l.split(/ +/);
var c = {}, name = l[0]=='cpu' ? 'all' : +l[0].slice(3), i;
for (i=0; i<items.length; i++)
c[items[i]] = +(l[i+1]||0); // guest/guest_nice not on old kernels
cpus[name] = c;
});
return cpus;
};
E.cpu_threads = function(){
let res = {};
let read_stat = (dir, tid)=>{
let str;
if (!(str = file.read(`${dir}/${tid}/stat`)))
return;
let parts = str.split(' ');
// stat.utime + stat.stime
res[tid] = +parts[13]+(+parts[14]);
};
E.ps().forEach(pid=>{
if (file.is_win)
return read_stat('/proc', pid);
let taskdir = `/proc/${pid}/task`;
cyg_readdir(taskdir).forEach(tid=>read_stat(taskdir, tid));
});
return res;
};
E.cpu_threads_prev = {};
E.cpu_threads_usage = function(){
const curr = E.cpu_threads();
let res = [];
for (let tid in curr)
res.push(curr[tid]-E.cpu_threads_prev[tid]||0);
E.cpu_threads_prev = curr;
return res;
};
E.cpus_prev = [null, null];
E.cpu_usage = function(cpus_curr, cpus_prev){
var p = cpus_prev||E.cpus_prev;
cpus_curr = cpus_curr||E.cpus();
var zero = {all: 0, single: 0};
if (!p[0])
{
p[1] = p[0] = cpus_curr;
return zero;
}
var d = cpus_diff(p[0], cpus_curr);
if (!d.all.total)
{
d = cpus_diff(p[1], cpus_curr);
if (!d.all.total)
return zero;
}
else
{
p[1] = p[0];
p[0] = cpus_curr;
}
const total_per_cpu = d.all.total/d.length;
const threads = E.cpu_threads_usage();
return {all: d.all.busy_ratio,
single: Math.max.apply(null, d.map(e=>e.busy_ratio)),
thread_single: Math.max.apply(null, threads.map(x=>x/total_per_cpu)),
diff: d};
};
if (!file.is_darwin)
E.cpu_usage(); // init
E.iface_list = ()=>{
if (file.is_win || file.is_darwin)
return Object.keys(os.networkInterfaces());
// https://github.com/nodejs/node/issues/498
let ifaces = cli.exec_get_lines(`${BIN_IP} link show`);
return ifaces.map(str=>{
const match = /^\d+:\s([\w@]+):.*/.exec(str);
return match && match[1].replace(/@\w+$/, '');
}).filter(i=>i);
};
E.eth_dev = ()=>{
let is_ether = ifname=>/^(bond|en|wl|eth)/.test(ifname);
let ifaces = E.iface_list().filter(is_ether);
// https://cgit.freedesktop.org/systemd/systemd/tree/src/udev/udev-builtin-net_id.c#n20
if (!ifaces.length)
throw new Error('No ethernet interfaces found');
if (ifaces.length==1)
return {eth_dev: ifaces[0], ifaces};
let routes = cli.exec_get_lines(`${BIN_IP} -4 route`);
let default_route = array.grep(routes, /^default/)[0];
if (!default_route)
throw new Error('Default route not found');
let m = default_route.match(/dev (\w+)/);
if (!m)
throw new Error('Cannot determine interface for default route');
if (!is_ether(m[1]))
throw new Error('None of multiple ethernet interfaces is default');
let eth_dev = m[1];
let udptunnel_dev = ifaces.filter(s=>s!=eth_dev)[0];
return {eth_dev, udptunnel_dev, ifaces, routes};
};
E.net_dev = undefined;
function set_net_dev(){
if (E.net_dev)
return;
// XXX vadim: rename to NET_DEV_STAT?
if (env.NET_DEV)
{
E.net_dev = qw(env.NET_DEV);
E.net_dev.forEach(dev=>{
if (!file.exists('/sys/class/net/'+dev))
{
zerr.perr('err_assert_bad_conf', 'Device '+dev+' configured '+
'in NET_DEV env but it does not exist');
}
});
return;
}
let eth_dev = 'ens3';
try { eth_dev = E.eth_dev(); } catch(e){ zerr(e); }
var search = [eth_dev.eth_dev, 'eth0', 'venet0', 'em1'];
for (var i in search)
{
if (file.exists('/sys/class/net/'+search[i]))
return void(E.net_dev = [search[i]]);
}
}
E.net_dev_stats = function(net_dev, opt={}){
var o = {};
if (!E.net_dev)
set_net_dev();
net_dev = array.to_array(net_dev||E.net_dev);
if (!net_dev.length)
return;
net_dev.forEach((dev, index)=>{
var ifname = index ? '_'+dev : '';
var stats = ['rx_bytes', 'tx_bytes'];
if (opt.err_stat)
{
stats.push('rx_packets', 'tx_packets', 'rx_errors', 'rx_dropped',
'rx_fifo_errors', 'rx_frame_errors', 'tx_errors', 'tx_dropped',
'tx_fifo_errors', 'tx_carrier_errors', 'collisions');
}
for (var i in stats)
{
try {
o[stats[i]+ifname] = +file.read_e(
'/sys/class/net/'+dev+'/statistics/'+stats[i]);
} catch(e){}
}
});
return o;
};
function beancounter_value(value){
return value=='9223372036854775807' ? null : +value; }
E.beancounters = function(){
try {
var info = file.read_lines_e('/proc/user_beancounters')
.slice(2).map(line=>line.slice(12).split(/[^\w]+/g));
var data = {total_failcnt: 0};
info.forEach(line=>{
data[line[0]] = {
held: +line[1],
maxheld: +line[2],
barrier: beancounter_value(line[3]),
limit: beancounter_value(line[4]),
failcnt: +line[5]
};
data.total_failcnt += +line[5];
});
return data;
} catch(e){ return; }
};
E.TCP = { // net/tcp_states.h
1: 'ESTABLISHED',
2: 'SYN_SENT',
3: 'SYN_RECV',
4: 'FIN_WAIT1',
5: 'FIN_WAIT2',
6: 'TIME_WAIT',
7: 'CLOSE',
8: 'CLOSE_WAIT',
9: 'LAST_ACK',
10: 'LISTEN',
11: 'CLOSING',
};
E.sockets_count = proto=>etask(function*(){
let line_idx = -1, v = {total: 0, lo: 0, ext: 0, err: 0};
zutil.forEach(E.TCP, id=>v[id] = 0);
yield efile.read_line_cb_e('/proc/net/'+proto, conn=>{
line_idx++;
if (!conn || !line_idx)
return;
let start;
if ((start = conn.indexOf(':'))==-1)
return void(v.err++);
v.total++;
if (conn.substr(start+2, 8)=='0100007F')
v.lo++;
else
v.ext++;
let state = E.TCP[+('0x'+conn.substr(start+30, 2))];
if (state)
v[state]++;
});
return v;
});
E.tcp_stats = ()=>{
let ll = file.read_lines_e('/proc/net/snmp'), i = 0;
for (; i<ll.length && !ll[i].startsWith('Tcp'); i++);
if (i<ll.length-1)
{
let l = ll[i+1].split(/\s+/);
return {in_segs: +l[10], out_segs: +l[11], retrans_segs: +l[12],
in_errs: +l[13], in_csum_errs: +l[15]};
}
};
E.udp_stats = ()=>{
let ll = file.read_lines_e('/proc/net/snmp'), i = 0;
for (; i<ll.length && !ll[i].startsWith('Udp'); i++);
if (i<ll.length-1)
{
let l = ll[i+1].split(/\s+/);
return {in_datagrams: +l[1], no_ports: +l[2], in_errors: +l[3],
out_datagrams: +l[4], rcvbuf_errors: +l[5], sndbuf_errors: +l[6],
in_csum_errors: +l[7]};
}
};
E.vmstat = function(){
var vmstat = file.read_lines_e('/proc/vmstat');
var ret = {};
for (var i=0; i<vmstat.length; i++)
{
var n = qw(vmstat[i]);
if (!n[0])
continue;
ret[n[0]] = +n[1];
}
return ret;
};
E.disk_page_io = function(){
var vmstat = E.vmstat();
// pgpgin/pgpgout are reported in KB; nr_dirty is reported in 4K pages
return {
read: vmstat.pgpgin*KB,
write: vmstat.pgpgout*KB,
dirty: vmstat.nr_dirty*4*KB,
dirty_max: vmstat.nr_dirty_threshold*4*KB,
dirty_bg_max: vmstat.nr_dirty_background_threshold*4*KB,
};
};
function calc_diskstat(cur, prev){
cur.rw_ms = cur.reads_ms+cur.writes_ms;
cur.rw_ios = cur.reads+cur.writes;
if (!prev)
return;
let d_ts = cur.ts-prev.ts;
let d_ios = cur.rw_ios-prev.rw_ios;
cur.await = d_ios ? (cur.rw_ms-prev.rw_ms)/d_ios : 0;
cur.util = d_ts ? (cur.io_ms-prev.io_ms)/d_ts*100 : 0;
cur.util_weighted = d_ts
? (cur.io_weighted_ms-prev.io_weighted_ms)/d_ts : 0;
}
E.diskstats_prev = {};
E.diskstats_sys = function(dev, inflight){
let diskstats;
if (!(diskstats = file.read_line(`/sys/block/${dev}/stat`)))
return;
let n = diskstats.trim().split(/\s+/);
let cur = {reads: +n[0], reads_merged: +n[1], reads_sector: +n[2],
reads_ms: +n[3], writes: +n[4], writes_merged: +n[5],
writes_sector: +n[6], writes_ms: +n[7], io_current: +n[8],
io_ms: +n[9], io_weighted_ms: +n[10], await: 0, util: 0,
util_weighted: 0, ts: Date.now()};
let prev = E.diskstats_prev[dev];
calc_diskstat(cur, prev);
if (prev)
{
cur.major = prev.major||0;
cur.minor = prev.minor||0;
}
inflight = inflight && file.read_line(`/sys/block/${dev}/inflight`);
if (inflight)
{
inflight = inflight.trim().split(/\s+/);
[cur.inflight_r, cur.inflight_w] = [+inflight[0], +inflight[1]];
}
E.diskstats_prev[dev] = cur;
return cur;
};
E.diskstats = function(){
// https://www.kernel.org/doc/Documentation/iostats.txt
let diskstats;
if (!(diskstats = cyg_read_lines('/proc/diskstats')))
return;
let ret = {};
for (let i=0; i<diskstats.length; i++)
{
let n = diskstats[i].trim().split(/\s+/), dev = n[2];
if (/\d+$/.test(dev) && !/nvme\dn\d$/.test(dev)) // ignore paritions
continue;
let cur = ret[dev] = {major: +n[0], minor: +n[1], reads: +n[3],
reads_merged: +n[4], reads_sector: +n[5], reads_ms: +n[6],
writes: +n[7], writes_merged: +n[8], writes_sector: +n[9],
writes_ms: +n[10], io_current: +n[11], io_ms: +n[12],
io_weighted_ms: +n[13], await: 0, util: 0, util_weighted: 0,
ts: Date.now()};
calc_diskstat(cur, E.diskstats_prev[dev]);
}
E.diskstats_prev = ret;
return ret;
};
E.disk_io_time = function(stats){
var diskstats = stats||E.diskstats();
if (!diskstats)
return;
var io = {read: 0, write: 0, total: 0, max_await: 0, max_util: 0,
max_util_weighted: 0};
for (var i in diskstats)
{
io.read += diskstats[i].reads_ms;
io.write += diskstats[i].writes_ms;
io.total += diskstats[i].io_ms;
io.max_await = Math.max(io.max_await, diskstats[i].await);
io.max_util = Math.max(io.max_util, diskstats[i].util);
io.max_util_weighted = Math.max(io.max_util_weighted,
diskstats[i].util_weighted);
}
return io;
};
E.get_kernel = function(){
const uname = exec.get_line('uname -snrv');
const res = /^(\S+) (\S+) (\S+) (.+)/.exec(uname);
if (!res)
return {};
return {name: res[1], node: res[2], release: res[3], version: res[4]};
};
E.info = function(){
return {type: os.type(), endianness: os.endianness(),
hostname: os.hostname(), arch: os.arch(),
release: E.get_release(), kernel: E.get_kernel()};
};
E.node = function(){
const dst = file.is_dir(file.normalize('/var/hola_agent')) ?
'hola_agent' : 'hola_server';
let host = exec.get_line('/usr/local/bin/node -v').replace(/^v/, '');
let hola_server = file.read_line(
file.normalize(`/var/${dst}/node_version`));
return {host, hola_server};
};
E.ps = function(){
return cyg_readdir('/proc').filter(p=>/^\d+$/.test(p)).map(p=>+p)
.sort((a, b)=>a-b);
};
E.fd_use = opt=>etask(function*(){
opt = opt||{};
let res = {use: -1, pid: -1, pids: {}, glob: {open: -1, max: -1, use: 0}};
if (file.is_win||file.is_darwin)
return res;
let pids = opt.pids||E.ps();
if (!pids.length)
return res;
let calc = (o, m)=>o<0 ? 0 : 100*o/m;
let ln = file.read_line('/proc/sys/fs/file-nr').split('\t');
if (ln.length==3)
{
res.glob.open = +ln[0];
res.glob.max = +ln[2];
}
res.use = res.glob.use = calc(res.glob.open, res.glob.max);
for (let pid of pids)
{
pid = ''+pid;
let dir = `/proc/${pid}/fd`;
let open = yield efile.readdir(dir);
let nopen = efile.error ? -1 : open.length;
let ln_re = cyg_read('/proc/'+pid+'/limits'), nmax = -1, m;
if (m = /Max open files\s+([0-9]+)/g.exec(ln_re))
nmax = +m[1];
if (!nmax)
continue;
let use = calc(nopen, nmax);
if (res.use<use)
{
res.use = use;
res.pid = +pid;
let status = cyg_read(`/proc/${pid}/status`);
if (m = /^Name:\t(.*)/.exec(status))
res.name = m[1];
}
if (nopen>0 && opt.match)
{
res.match = res.match||0;
for (let f of open)
{
let link = yield efile.readlink(`${dir}/${f}`);
if (opt.match.test(link))
res.match++;
}
}
res.pids[pid] = {open: nopen, max: nmax, use: use};
}
if (res.match)
res.match = calc(res.match, res.glob.max);
return res;
});
E.systemd_analyze = (type, args=[])=>{
args = [].concat(args);
return exec.get(`systemd-analyze ${type} ${args.join(' ')}`,
{out: 'stdout', stdio: 'pipe'});
};
const PAGESIZE = 4096;
const parse_pid_stat = stat=>{
if (!stat)
return {};
let res = stat.match(/^(\d+)\s+\(([^)]*)\)\s+(.*)\n?$/);
if (!res)
return {};
let fields = res[3].split(' ');
return {pid: +res[1], name: res[2], rss: +(fields[21]||0)*PAGESIZE};
};
const proc_mem = pid=>parse_pid_stat(cyg_read('/proc/'+pid+'/stat'));
E.ps_mem = ()=>E.ps().map(p=>proc_mem(p));