dktcli 1.0.0__py3-none-any.whl
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.
- devkittools/__init__.py +3 -0
- devkittools/__main__.py +6 -0
- devkittools/cli.py +179 -0
- devkittools/tools/__init__.py +1 -0
- devkittools/tools/bench.py +73 -0
- devkittools/tools/changeloggen.py +181 -0
- devkittools/tools/clean.py +110 -0
- devkittools/tools/colortool.py +126 -0
- devkittools/tools/commitwriter.py +199 -0
- devkittools/tools/contextpacker.py +226 -0
- devkittools/tools/cron.py +152 -0
- devkittools/tools/doctor.py +74 -0
- devkittools/tools/du.py +72 -0
- devkittools/tools/enc.py +131 -0
- devkittools/tools/envtool.py +141 -0
- devkittools/tools/findtool.py +119 -0
- devkittools/tools/gen.py +182 -0
- devkittools/tools/gx.py +184 -0
- devkittools/tools/hashtool.py +93 -0
- devkittools/tools/httptool.py +131 -0
- devkittools/tools/ignore.py +213 -0
- devkittools/tools/initproj.py +166 -0
- devkittools/tools/jsontool.py +200 -0
- devkittools/tools/net.py +114 -0
- devkittools/tools/portkiller.py +246 -0
- devkittools/tools/regextool.py +84 -0
- devkittools/tools/repostats.py +197 -0
- devkittools/tools/secrets_scan.py +108 -0
- devkittools/tools/serve.py +118 -0
- devkittools/tools/snip.py +94 -0
- devkittools/tools/system.py +157 -0
- devkittools/tools/timetool.py +130 -0
- devkittools/tools/todo.py +69 -0
- devkittools/tools/tree.py +86 -0
- devkittools/tools/watch.py +82 -0
- devkittools/utils.py +561 -0
- dktcli-1.0.0.dist-info/METADATA +148 -0
- dktcli-1.0.0.dist-info/RECORD +42 -0
- dktcli-1.0.0.dist-info/WHEEL +5 -0
- dktcli-1.0.0.dist-info/entry_points.txt +3 -0
- dktcli-1.0.0.dist-info/licenses/LICENSE +21 -0
- dktcli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""dkt json (js) - format, minify, query, validate, flatten and convert JSON."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from ..utils import DktError, c, color_enabled, info, ok, output_result, read_input, sym
|
|
12
|
+
|
|
13
|
+
META = ("Data & Text", "json", "js", "pretty-print, minify, query, flatten, JSON -> CSV")
|
|
14
|
+
|
|
15
|
+
EPILOG = """\
|
|
16
|
+
examples:
|
|
17
|
+
dkt js fmt data.json pretty-print (colored in a terminal)
|
|
18
|
+
cat resp.json | dkt js fmt --sort
|
|
19
|
+
dkt js min -f data.json --copy
|
|
20
|
+
dkt js get users[0].name -f data.json
|
|
21
|
+
dkt js keys -f data.json show the shape of the document
|
|
22
|
+
dkt js flat -f data.json a.b[0].c = value
|
|
23
|
+
dkt js csv -f users.json array of objects -> CSV
|
|
24
|
+
dkt js fmt --paste --copy fix whatever is on your clipboard
|
|
25
|
+
Trailing commas and // comments are tolerated and cleaned up.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_TOKEN = re.compile(r'("(?:\\.|[^"\\])*")(\s*:)?|(\btrue\b|\bfalse\b)|(\bnull\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def colorize(text: str) -> str:
|
|
32
|
+
if not color_enabled():
|
|
33
|
+
return text
|
|
34
|
+
|
|
35
|
+
def sub(m):
|
|
36
|
+
if m.group(1):
|
|
37
|
+
return (c(m.group(1), "bcyan") + m.group(2)) if m.group(2) else c(m.group(1), "bgreen")
|
|
38
|
+
if m.group(3):
|
|
39
|
+
return c(m.group(3), "byellow")
|
|
40
|
+
if m.group(4):
|
|
41
|
+
return c(m.group(4), "gray")
|
|
42
|
+
return c(m.group(5), "bmagenta")
|
|
43
|
+
return _TOKEN.sub(sub, text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_lenient(text: str) -> str:
|
|
47
|
+
"""Remove // and /* */ comments (outside strings) and trailing commas."""
|
|
48
|
+
out, i, n, in_str = [], 0, len(text), False
|
|
49
|
+
while i < n:
|
|
50
|
+
ch = text[i]
|
|
51
|
+
if in_str:
|
|
52
|
+
out.append(ch)
|
|
53
|
+
if ch == "\\" and i + 1 < n:
|
|
54
|
+
out.append(text[i + 1])
|
|
55
|
+
i += 1
|
|
56
|
+
elif ch == '"':
|
|
57
|
+
in_str = False
|
|
58
|
+
elif ch == '"':
|
|
59
|
+
in_str = True
|
|
60
|
+
out.append(ch)
|
|
61
|
+
elif text.startswith("//", i):
|
|
62
|
+
while i < n and text[i] != "\n":
|
|
63
|
+
i += 1
|
|
64
|
+
continue
|
|
65
|
+
elif text.startswith("/*", i):
|
|
66
|
+
end = text.find("*/", i + 2)
|
|
67
|
+
i = n if end == -1 else end + 2
|
|
68
|
+
continue
|
|
69
|
+
else:
|
|
70
|
+
out.append(ch)
|
|
71
|
+
i += 1
|
|
72
|
+
return re.sub(r",(\s*[}\]])", r"\1", "".join(out))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def loads(text: str):
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(text)
|
|
78
|
+
except json.JSONDecodeError as first:
|
|
79
|
+
try:
|
|
80
|
+
data = json.loads(_strip_lenient(text))
|
|
81
|
+
info(c("note: fixed comments / trailing commas", "gray"))
|
|
82
|
+
return data
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
lines = text.splitlines()
|
|
85
|
+
ctx = lines[first.lineno - 1] if 0 < first.lineno <= len(lines) else ""
|
|
86
|
+
raise DktError("Invalid JSON at line %d, column %d: %s\n %s\n %s^" % (
|
|
87
|
+
first.lineno, first.colno, first.msg, ctx.strip()[:100], " " * min(first.colno, 100)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_path(data, path: str):
|
|
91
|
+
cur = data
|
|
92
|
+
for tok in re.findall(r"[^.\[\]]+|\[\d+\]", path):
|
|
93
|
+
try:
|
|
94
|
+
if tok.startswith("["):
|
|
95
|
+
cur = cur[int(tok[1:-1])]
|
|
96
|
+
elif isinstance(cur, list) and tok.isdigit():
|
|
97
|
+
cur = cur[int(tok)]
|
|
98
|
+
else:
|
|
99
|
+
cur = cur[tok]
|
|
100
|
+
except (KeyError, IndexError, TypeError):
|
|
101
|
+
raise DktError("Path not found at '%s'" % tok)
|
|
102
|
+
return cur
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def flatten(data, prefix=""):
|
|
106
|
+
items = []
|
|
107
|
+
if isinstance(data, dict):
|
|
108
|
+
for k, v in data.items():
|
|
109
|
+
items += flatten(v, "%s.%s" % (prefix, k) if prefix else str(k))
|
|
110
|
+
elif isinstance(data, list):
|
|
111
|
+
for i, v in enumerate(data):
|
|
112
|
+
items += flatten(v, "%s[%d]" % (prefix, i))
|
|
113
|
+
else:
|
|
114
|
+
items.append((prefix, data))
|
|
115
|
+
return items
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def shape(data, indent=0, name="$", out=None):
|
|
119
|
+
out = [] if out is None else out
|
|
120
|
+
pad = " " * indent
|
|
121
|
+
if isinstance(data, dict):
|
|
122
|
+
out.append("%s%s: object (%d keys)" % (pad, name, len(data)))
|
|
123
|
+
for k, v in data.items():
|
|
124
|
+
shape(v, indent + 1, k, out)
|
|
125
|
+
elif isinstance(data, list):
|
|
126
|
+
out.append("%s%s: array (%d items)" % (pad, name, len(data)))
|
|
127
|
+
if data:
|
|
128
|
+
shape(data[0], indent + 1, "[0]", out)
|
|
129
|
+
else:
|
|
130
|
+
out.append("%s%s: %s" % (pad, name, type(data).__name__ if data is not None else "null"))
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run(args) -> int:
|
|
135
|
+
text = read_input(args.file_pos, args.file, args.paste, "JSON")
|
|
136
|
+
data = loads(text)
|
|
137
|
+
op = args.op
|
|
138
|
+
if op == "validate":
|
|
139
|
+
ok("valid JSON")
|
|
140
|
+
return 0
|
|
141
|
+
if op == "fmt":
|
|
142
|
+
out = json.dumps(data, indent=args.indent, sort_keys=args.sort, ensure_ascii=False)
|
|
143
|
+
output_result(out, copy=True) if args.copy else print(colorize(out))
|
|
144
|
+
elif op == "min":
|
|
145
|
+
output_result(json.dumps(data, separators=(",", ":"), sort_keys=args.sort, ensure_ascii=False), copy=args.copy)
|
|
146
|
+
elif op == "get":
|
|
147
|
+
val = get_path(data, args.path)
|
|
148
|
+
out = json.dumps(val, indent=2, ensure_ascii=False) if isinstance(val, (dict, list)) else (
|
|
149
|
+
val if isinstance(val, str) else json.dumps(val))
|
|
150
|
+
print(colorize(out) if isinstance(val, (dict, list)) else out)
|
|
151
|
+
elif op == "keys":
|
|
152
|
+
print("\n".join(shape(data)))
|
|
153
|
+
elif op == "flat":
|
|
154
|
+
for k, v in flatten(data):
|
|
155
|
+
print("%s = %s" % (c(k, "bcyan"), json.dumps(v, ensure_ascii=False)))
|
|
156
|
+
elif op == "csv":
|
|
157
|
+
if not isinstance(data, list) or not data or not all(isinstance(x, dict) for x in data):
|
|
158
|
+
raise DktError("CSV conversion needs a JSON array of objects.")
|
|
159
|
+
cols = []
|
|
160
|
+
for row in data:
|
|
161
|
+
for k in row:
|
|
162
|
+
if k not in cols:
|
|
163
|
+
cols.append(k)
|
|
164
|
+
buf = io.StringIO()
|
|
165
|
+
w = csv.writer(buf, lineterminator="\n")
|
|
166
|
+
w.writerow(cols)
|
|
167
|
+
for row in data:
|
|
168
|
+
w.writerow([json.dumps(row.get(k), ensure_ascii=False) if isinstance(row.get(k), (dict, list))
|
|
169
|
+
else ("" if row.get(k) is None else row.get(k)) for k in cols])
|
|
170
|
+
output_result(buf.getvalue().rstrip("\n"), copy=args.copy)
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register(sub, common) -> None:
|
|
175
|
+
p = sub.add_parser("json", aliases=["js"], parents=[common], help=META[3],
|
|
176
|
+
description="Swiss army knife for JSON.", epilog=EPILOG,
|
|
177
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
178
|
+
p.add_argument("op", choices=["fmt", "min", "get", "keys", "flat", "csv", "validate"])
|
|
179
|
+
p.add_argument("path", nargs="?", help="for 'get': a.b[0].c")
|
|
180
|
+
p.add_argument("file_pos", nargs="?", metavar="TEXT", help=argparse.SUPPRESS)
|
|
181
|
+
p.add_argument("-f", "--file", metavar="FILE", help="read JSON from a file")
|
|
182
|
+
p.add_argument("--paste", action="store_true", help="read JSON from clipboard")
|
|
183
|
+
p.add_argument("--copy", action="store_true", help="copy result to clipboard")
|
|
184
|
+
p.add_argument("--indent", type=int, default=2)
|
|
185
|
+
p.add_argument("--sort", action="store_true", help="sort keys")
|
|
186
|
+
p.set_defaults(func=_dispatch)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _dispatch(args) -> int:
|
|
190
|
+
import os
|
|
191
|
+
if args.file_pos and not args.file and os.path.isfile(args.file_pos):
|
|
192
|
+
args.file, args.file_pos = args.file_pos, None
|
|
193
|
+
# 'fmt data.json' style: positional after op is the file for every op except 'get'.
|
|
194
|
+
if args.op != "get":
|
|
195
|
+
if args.path and not args.file:
|
|
196
|
+
args.file = args.path
|
|
197
|
+
args.path = None
|
|
198
|
+
elif not args.path:
|
|
199
|
+
raise DktError("'get' needs a path, e.g. dkt js get users[0].name -f data.json")
|
|
200
|
+
return run(args)
|
devkittools/tools/net.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""dkt net - IP addresses, DNS lookups and TCP reachability checks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import socket
|
|
6
|
+
import time
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
from ..utils import DktError, c, fail, kv, ok, sym, table
|
|
10
|
+
|
|
11
|
+
META = ("Servers & Network", "net", "", "ip addresses, DNS lookup, is-this-port-open checks")
|
|
12
|
+
|
|
13
|
+
EPILOG = """\
|
|
14
|
+
examples:
|
|
15
|
+
dkt net ip local IPs
|
|
16
|
+
dkt net ip --public also your public IP (asks api.ipify.org)
|
|
17
|
+
dkt net dns github.com
|
|
18
|
+
dkt net check github.com:443 localhost:5432 192.168.1.10:22
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def local_ips():
|
|
23
|
+
ips = set()
|
|
24
|
+
try:
|
|
25
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
26
|
+
s.connect(("10.255.255.255", 1))
|
|
27
|
+
ips.add(s.getsockname()[0])
|
|
28
|
+
s.close()
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
try:
|
|
32
|
+
for info_ in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
|
33
|
+
ips.add(info_[4][0])
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return sorted(i for i in ips if not i.startswith("127."))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_ip(args) -> int:
|
|
40
|
+
rows = [("hostname", socket.gethostname())]
|
|
41
|
+
for i, ip in enumerate(local_ips()):
|
|
42
|
+
rows.append(("local ip" if i == 0 else "", c(ip, "bold")))
|
|
43
|
+
if args.public:
|
|
44
|
+
try:
|
|
45
|
+
req = urllib.request.Request("https://api.ipify.org", headers={"User-Agent": "dkt"})
|
|
46
|
+
rows.append(("public ip", c(urllib.request.urlopen(req, timeout=6).read().decode().strip(), "bold bcyan")))
|
|
47
|
+
except Exception:
|
|
48
|
+
rows.append(("public ip", c("unavailable (offline?)", "red")))
|
|
49
|
+
kv(rows)
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_dns(args) -> int:
|
|
54
|
+
for host in args.hosts:
|
|
55
|
+
try:
|
|
56
|
+
t0 = time.perf_counter()
|
|
57
|
+
res = socket.getaddrinfo(host, None)
|
|
58
|
+
dt = (time.perf_counter() - t0) * 1000
|
|
59
|
+
except socket.gaierror as e:
|
|
60
|
+
fail("%s %s" % (host, e))
|
|
61
|
+
continue
|
|
62
|
+
addrs = []
|
|
63
|
+
for r in res:
|
|
64
|
+
a = r[4][0]
|
|
65
|
+
if a not in addrs:
|
|
66
|
+
addrs.append(a)
|
|
67
|
+
print("%s %s %s" % (c(sym("ok"), "bgreen"), c(host, "bold"), c("(%.0f ms)" % dt, "gray")))
|
|
68
|
+
for a in addrs:
|
|
69
|
+
print(" %s %s" % (c("IPv6" if ":" in a else "IPv4", "gray"), a))
|
|
70
|
+
try:
|
|
71
|
+
print(" %s %s" % (c("rdns", "gray"), socket.gethostbyaddr(addrs[0])[0]))
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_check(args) -> int:
|
|
78
|
+
rows = []
|
|
79
|
+
bad = 0
|
|
80
|
+
for target in args.targets:
|
|
81
|
+
host, _, port = target.rpartition(":")
|
|
82
|
+
if not host or not port.isdigit():
|
|
83
|
+
raise DktError("Use host:port, e.g. github.com:443 (got '%s')" % target)
|
|
84
|
+
t0 = time.perf_counter()
|
|
85
|
+
try:
|
|
86
|
+
with socket.create_connection((host, int(port)), timeout=args.timeout):
|
|
87
|
+
dt = (time.perf_counter() - t0) * 1000
|
|
88
|
+
rows.append((c(sym("ok"), "bgreen"), target, c("open", "green"), "%.0f ms" % dt))
|
|
89
|
+
except socket.timeout:
|
|
90
|
+
rows.append((c(sym("fail"), "bred"), target, c("timed out", "red"), ""))
|
|
91
|
+
bad += 1
|
|
92
|
+
except OSError as e:
|
|
93
|
+
rows.append((c(sym("fail"), "bred"), target, c("closed / unreachable", "red"), c(str(e.strerror or e)[:30], "gray")))
|
|
94
|
+
bad += 1
|
|
95
|
+
table(["", "TARGET", "STATUS", "LATENCY"], rows)
|
|
96
|
+
return 1 if bad else 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def register(sub, common) -> None:
|
|
100
|
+
p = sub.add_parser("net", parents=[common], help=META[3],
|
|
101
|
+
description="Network helpers.", epilog=EPILOG,
|
|
102
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
103
|
+
g = p.add_subparsers(dest="sub", metavar="<ip|dns|check>")
|
|
104
|
+
p.set_defaults(func=lambda a: (p.print_help() or 0))
|
|
105
|
+
ip = g.add_parser("ip", help="show local (and public) IP addresses")
|
|
106
|
+
ip.add_argument("--public", action="store_true")
|
|
107
|
+
ip.set_defaults(func=cmd_ip)
|
|
108
|
+
dns = g.add_parser("dns", help="resolve host names")
|
|
109
|
+
dns.add_argument("hosts", nargs="+")
|
|
110
|
+
dns.set_defaults(func=cmd_dns)
|
|
111
|
+
ck = g.add_parser("check", help="test if host:port accepts TCP connections")
|
|
112
|
+
ck.add_argument("targets", nargs="+", metavar="HOST:PORT")
|
|
113
|
+
ck.add_argument("--timeout", type=float, default=3.0)
|
|
114
|
+
ck.set_defaults(func=cmd_check)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""dkt portkiller (pk) - see what is using a port and optionally kill it."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import csv
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import signal
|
|
10
|
+
import time
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from ..utils import DktError, c, info, print_table, run_capture, str2bool
|
|
14
|
+
|
|
15
|
+
PROTECTED_PIDS = {0, 4}
|
|
16
|
+
|
|
17
|
+
EPILOG = """\
|
|
18
|
+
examples:
|
|
19
|
+
dkt pk list everything that is listening
|
|
20
|
+
dkt pk --show 3000 who is using port 3000?
|
|
21
|
+
dkt pk --show 3000 --kill true ...and kill it
|
|
22
|
+
dkt pk --show 3000 8080 --kill several ports at once
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------ listing
|
|
27
|
+
|
|
28
|
+
def _windows_names() -> Dict[int, str]:
|
|
29
|
+
names = {} # type: Dict[int, str]
|
|
30
|
+
_, out, _ = run_capture(["tasklist", "/FO", "CSV", "/NH"])
|
|
31
|
+
for row in csv.reader(out.splitlines()):
|
|
32
|
+
if len(row) >= 2 and row[1].isdigit():
|
|
33
|
+
names[int(row[1])] = row[0]
|
|
34
|
+
return names
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _listeners_windows() -> List[dict]:
|
|
38
|
+
code, out, err = run_capture(["netstat", "-ano"])
|
|
39
|
+
if code != 0:
|
|
40
|
+
raise DktError("netstat failed: %s" % err.strip())
|
|
41
|
+
names = _windows_names()
|
|
42
|
+
entries = []
|
|
43
|
+
for line in out.splitlines():
|
|
44
|
+
parts = line.split()
|
|
45
|
+
if len(parts) < 5 or parts[0].upper() != "TCP" or parts[3].upper() != "LISTENING":
|
|
46
|
+
continue
|
|
47
|
+
local, pid_s = parts[1], parts[4]
|
|
48
|
+
addr, _, port_s = local.rpartition(":")
|
|
49
|
+
if not port_s.isdigit() or not pid_s.isdigit():
|
|
50
|
+
continue
|
|
51
|
+
pid = int(pid_s)
|
|
52
|
+
entries.append({"port": int(port_s), "address": addr, "pid": pid, "name": names.get(pid, "?")})
|
|
53
|
+
return entries
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _listeners_posix() -> List[dict]:
|
|
57
|
+
entries = []
|
|
58
|
+
if shutil.which("ss"):
|
|
59
|
+
_, out, _ = run_capture(["ss", "-ltnp"])
|
|
60
|
+
for line in out.splitlines():
|
|
61
|
+
parts = line.split()
|
|
62
|
+
if len(parts) < 4 or parts[0] == "State":
|
|
63
|
+
continue
|
|
64
|
+
local = parts[3]
|
|
65
|
+
addr, _, port_s = local.rpartition(":")
|
|
66
|
+
if not port_s.isdigit():
|
|
67
|
+
continue
|
|
68
|
+
m = re.search(r'\("([^"]*)",pid=(\d+)', line)
|
|
69
|
+
entries.append({"port": int(port_s), "address": addr,
|
|
70
|
+
"pid": int(m.group(2)) if m else None,
|
|
71
|
+
"name": m.group(1) if m else "? (try sudo)"})
|
|
72
|
+
return entries
|
|
73
|
+
if shutil.which("lsof"):
|
|
74
|
+
_, out, _ = run_capture(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN"])
|
|
75
|
+
for line in out.splitlines()[1:]:
|
|
76
|
+
parts = line.split()
|
|
77
|
+
if len(parts) < 9:
|
|
78
|
+
continue
|
|
79
|
+
addr, _, port_s = parts[8].rpartition(":")
|
|
80
|
+
if not port_s.isdigit() or not parts[1].isdigit():
|
|
81
|
+
continue
|
|
82
|
+
entries.append({"port": int(port_s), "address": addr, "pid": int(parts[1]), "name": parts[0]})
|
|
83
|
+
return entries
|
|
84
|
+
if os.path.exists("/proc/net/tcp"):
|
|
85
|
+
return _listeners_proc()
|
|
86
|
+
raise DktError("Neither 'ss' nor 'lsof' is available on this system.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _listeners_proc() -> List[dict]:
|
|
90
|
+
"""Fallback for minimal Linux systems: read /proc directly."""
|
|
91
|
+
sockets = {} # inode -> (port, address)
|
|
92
|
+
for fname in ("/proc/net/tcp", "/proc/net/tcp6"):
|
|
93
|
+
try:
|
|
94
|
+
with open(fname) as f:
|
|
95
|
+
next(f, None)
|
|
96
|
+
for line in f:
|
|
97
|
+
parts = line.split()
|
|
98
|
+
if len(parts) < 10 or parts[3] != "0A": # 0A = LISTEN
|
|
99
|
+
continue
|
|
100
|
+
addr_hex, port_hex = parts[1].rsplit(":", 1)
|
|
101
|
+
sockets[parts[9]] = (int(port_hex, 16), addr_hex)
|
|
102
|
+
except OSError:
|
|
103
|
+
continue
|
|
104
|
+
owners = {} # inode -> pid
|
|
105
|
+
for pid_s in os.listdir("/proc"):
|
|
106
|
+
if not pid_s.isdigit():
|
|
107
|
+
continue
|
|
108
|
+
fd_dir = "/proc/%s/fd" % pid_s
|
|
109
|
+
try:
|
|
110
|
+
for fd in os.listdir(fd_dir):
|
|
111
|
+
target = os.readlink("%s/%s" % (fd_dir, fd))
|
|
112
|
+
if target.startswith("socket:["):
|
|
113
|
+
inode = target[8:-1]
|
|
114
|
+
if inode in sockets:
|
|
115
|
+
owners[inode] = int(pid_s)
|
|
116
|
+
except OSError:
|
|
117
|
+
continue
|
|
118
|
+
entries = []
|
|
119
|
+
for inode, (port, addr_hex) in sockets.items():
|
|
120
|
+
pid = owners.get(inode)
|
|
121
|
+
name = "? (try sudo)"
|
|
122
|
+
if pid is not None:
|
|
123
|
+
try:
|
|
124
|
+
with open("/proc/%d/comm" % pid) as f:
|
|
125
|
+
name = f.read().strip()
|
|
126
|
+
except OSError:
|
|
127
|
+
name = "?"
|
|
128
|
+
address = "::" if len(addr_hex) > 8 else ("0.0.0.0" if int(addr_hex, 16) == 0 else "127.0.0.1")
|
|
129
|
+
entries.append({"port": port, "address": address, "pid": pid, "name": name})
|
|
130
|
+
return entries
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_listeners() -> List[dict]:
|
|
134
|
+
raw = _listeners_windows() if os.name == "nt" else _listeners_posix()
|
|
135
|
+
seen = set()
|
|
136
|
+
unique = []
|
|
137
|
+
for e in sorted(raw, key=lambda e: (e["port"], e["pid"] or 0, e["address"])):
|
|
138
|
+
key = (e["port"], e["pid"], e["address"])
|
|
139
|
+
if key not in seen:
|
|
140
|
+
seen.add(key)
|
|
141
|
+
unique.append(e)
|
|
142
|
+
return unique
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------ killing
|
|
146
|
+
|
|
147
|
+
def _alive(pid: int) -> bool:
|
|
148
|
+
try:
|
|
149
|
+
os.kill(pid, 0)
|
|
150
|
+
except ProcessLookupError:
|
|
151
|
+
return False
|
|
152
|
+
except PermissionError:
|
|
153
|
+
return True
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def kill_pid(pid: int) -> Tuple[bool, str]:
|
|
158
|
+
if pid in PROTECTED_PIDS or pid == os.getpid():
|
|
159
|
+
return False, "refusing to kill a protected system process"
|
|
160
|
+
if os.name == "nt":
|
|
161
|
+
code, out, err = run_capture(["taskkill", "/PID", str(pid), "/F", "/T"])
|
|
162
|
+
if code == 0:
|
|
163
|
+
return True, "killed"
|
|
164
|
+
msg = (err or out).strip().splitlines()
|
|
165
|
+
text = msg[0] if msg else "taskkill failed"
|
|
166
|
+
if "denied" in text.lower():
|
|
167
|
+
text += " (try an Administrator PowerShell)"
|
|
168
|
+
return False, text
|
|
169
|
+
try:
|
|
170
|
+
os.kill(pid, signal.SIGTERM)
|
|
171
|
+
except ProcessLookupError:
|
|
172
|
+
return True, "already gone"
|
|
173
|
+
except PermissionError:
|
|
174
|
+
return False, "permission denied (try sudo)"
|
|
175
|
+
for _ in range(20):
|
|
176
|
+
time.sleep(0.1)
|
|
177
|
+
if not _alive(pid):
|
|
178
|
+
return True, "killed"
|
|
179
|
+
try:
|
|
180
|
+
os.kill(pid, signal.SIGKILL)
|
|
181
|
+
except (ProcessLookupError, PermissionError) as e:
|
|
182
|
+
return False, str(e)
|
|
183
|
+
return True, "killed (SIGKILL)"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------ command
|
|
187
|
+
|
|
188
|
+
def run(args: argparse.Namespace) -> int:
|
|
189
|
+
kill = str2bool(args.kill)
|
|
190
|
+
listeners = get_listeners()
|
|
191
|
+
|
|
192
|
+
if args.show is None:
|
|
193
|
+
if kill:
|
|
194
|
+
raise DktError("--kill needs a port: dkt pk --show 3000 --kill true")
|
|
195
|
+
if not listeners:
|
|
196
|
+
print("Nothing is listening on any TCP port.")
|
|
197
|
+
return 0
|
|
198
|
+
print(c("Listening TCP ports", "bold"))
|
|
199
|
+
print_table(["PORT", "PID", "PROCESS", "ADDRESS"],
|
|
200
|
+
[(e["port"], e["pid"] if e["pid"] is not None else "-", e["name"], e["address"])
|
|
201
|
+
for e in listeners])
|
|
202
|
+
info("\nTip: dkt pk --show <port> --kill true")
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
for port in args.show:
|
|
206
|
+
if not 1 <= port <= 65535:
|
|
207
|
+
raise DktError("Invalid port: %d (must be 1-65535)" % port)
|
|
208
|
+
|
|
209
|
+
for port in args.show:
|
|
210
|
+
matches = [e for e in listeners if e["port"] == port]
|
|
211
|
+
if not matches:
|
|
212
|
+
print("Port %d: %s" % (port, c("free - nothing is listening", "green")))
|
|
213
|
+
continue
|
|
214
|
+
print("Port %d: %s" % (port, c("in use", "yellow")))
|
|
215
|
+
print_table(["PID", "PROCESS", "ADDRESS"],
|
|
216
|
+
[(e["pid"] if e["pid"] is not None else "-", e["name"], e["address"]) for e in matches])
|
|
217
|
+
if not kill:
|
|
218
|
+
info(" (add --kill true to stop it)")
|
|
219
|
+
continue
|
|
220
|
+
pids = sorted({e["pid"] for e in matches if e["pid"] is not None})
|
|
221
|
+
if not pids:
|
|
222
|
+
print(c(" Can't see the owning process - try running with sudo/Administrator.", "red"))
|
|
223
|
+
continue
|
|
224
|
+
for pid in pids:
|
|
225
|
+
name = next((e["name"] for e in matches if e["pid"] == pid), "?")
|
|
226
|
+
ok, msg = kill_pid(pid)
|
|
227
|
+
mark = c("✔", "green") if ok else c("✘", "red")
|
|
228
|
+
print(" %s PID %d (%s): %s" % (mark, pid, name, msg))
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def register(sub, common) -> None:
|
|
233
|
+
p = sub.add_parser(
|
|
234
|
+
"portkiller", aliases=["pk"], parents=[common],
|
|
235
|
+
help="see what is using a port, optionally kill it",
|
|
236
|
+
description="Show which process is listening on a TCP port and optionally kill it.\n"
|
|
237
|
+
"With no options, lists every listening port.",
|
|
238
|
+
epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
239
|
+
p.add_argument("-s", "--show", nargs="+", type=int, metavar="PORT",
|
|
240
|
+
help="port(s) to inspect")
|
|
241
|
+
p.add_argument("-k", "--kill", nargs="?", const="true", default="false", metavar="true|false",
|
|
242
|
+
help="kill the process(es) using the port (use with --show)")
|
|
243
|
+
p.set_defaults(func=run)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
META = ("Servers & Network", "portkiller", "pk", "see what's using a port, kill it")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""dkt regex (rx) - test a regular expression against text or a file with highlighted matches."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ..utils import DktError, c, color_enabled, kv, read_input, warn
|
|
8
|
+
|
|
9
|
+
META = ("Data & Text", "regex", "rx", "test regexes with highlighted matches, groups, replace")
|
|
10
|
+
|
|
11
|
+
EPILOG = """\
|
|
12
|
+
examples:
|
|
13
|
+
dkt rx "\\d{3}-\\d{4}" "call 555-1234 now"
|
|
14
|
+
dkt rx "(?P<user>\\w+)@(?P<host>[\\w.]+)" -f contacts.txt
|
|
15
|
+
dkt rx "foo" "foo Foo FOO" -i
|
|
16
|
+
dkt rx "(\\w+) (\\w+)" "hello world" --replace "\\2 \\1"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def highlight(text: str, matches) -> str:
|
|
21
|
+
if not color_enabled():
|
|
22
|
+
return text
|
|
23
|
+
out, last = [], 0
|
|
24
|
+
for m in matches:
|
|
25
|
+
if m.end() == m.start():
|
|
26
|
+
continue
|
|
27
|
+
out.append(text[last:m.start()])
|
|
28
|
+
out.append(c(text[m.start():m.end()], "reverse bgreen"))
|
|
29
|
+
last = m.end()
|
|
30
|
+
out.append(text[last:])
|
|
31
|
+
return "".join(out)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run(args) -> int:
|
|
35
|
+
flags = 0
|
|
36
|
+
if args.i:
|
|
37
|
+
flags |= re.I
|
|
38
|
+
if args.m:
|
|
39
|
+
flags |= re.M
|
|
40
|
+
if args.s:
|
|
41
|
+
flags |= re.S
|
|
42
|
+
try:
|
|
43
|
+
rx = re.compile(args.pattern, flags)
|
|
44
|
+
except re.error as e:
|
|
45
|
+
raise DktError("Invalid regex: %s" % e)
|
|
46
|
+
text = read_input(args.text, args.file, False, "text")
|
|
47
|
+
matches = list(rx.finditer(text))
|
|
48
|
+
if args.replace is not None:
|
|
49
|
+
try:
|
|
50
|
+
print(rx.sub(args.replace, text))
|
|
51
|
+
except (re.error, IndexError) as e:
|
|
52
|
+
raise DktError("Bad replacement: %s" % e)
|
|
53
|
+
return 0
|
|
54
|
+
if not matches:
|
|
55
|
+
warn("no matches")
|
|
56
|
+
return 1
|
|
57
|
+
print(highlight(text.rstrip("\n"), matches))
|
|
58
|
+
print()
|
|
59
|
+
print(" %s match%s" % (c(len(matches), "bold bgreen"), "" if len(matches) == 1 else "es"))
|
|
60
|
+
for n, m in enumerate(matches[:args.limit], 1):
|
|
61
|
+
loc = c("@%d-%d" % (m.start(), m.end()), "gray")
|
|
62
|
+
print(" %s %s %s" % (c("#%d" % n, "bcyan"), repr(m.group(0)), loc))
|
|
63
|
+
if m.groups():
|
|
64
|
+
names = {v: k for k, v in rx.groupindex.items()}
|
|
65
|
+
for gi, val in enumerate(m.groups(), 1):
|
|
66
|
+
print(" %s %s" % (c("$%s" % names.get(gi, gi), "bmagenta"), repr(val)))
|
|
67
|
+
if len(matches) > args.limit:
|
|
68
|
+
print(c(" ... %d more (use --limit)" % (len(matches) - args.limit), "gray"))
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def register(sub, common) -> None:
|
|
73
|
+
p = sub.add_parser("regex", aliases=["rx"], parents=[common], help=META[3],
|
|
74
|
+
description="Test a Python regular expression.", epilog=EPILOG,
|
|
75
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
76
|
+
p.add_argument("pattern")
|
|
77
|
+
p.add_argument("text", nargs="?", help="text to test (or -f FILE / pipe stdin)")
|
|
78
|
+
p.add_argument("-f", "--file", metavar="FILE")
|
|
79
|
+
p.add_argument("-i", action="store_true", help="ignore case")
|
|
80
|
+
p.add_argument("-m", action="store_true", help="multiline (^ $ per line)")
|
|
81
|
+
p.add_argument("-s", action="store_true", help="dot matches newline")
|
|
82
|
+
p.add_argument("--replace", metavar="REPL", help="show the result of re.sub instead")
|
|
83
|
+
p.add_argument("--limit", type=int, default=20, help="max matches to detail (default 20)")
|
|
84
|
+
p.set_defaults(func=run)
|