picklock 0.1.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.
- picklock/__init__.py +31 -0
- picklock/__main__.py +10 -0
- picklock/addressing.py +259 -0
- picklock/cli.py +262 -0
- picklock/commands/__init__.py +601 -0
- picklock/commands/alias_commands.py +249 -0
- picklock/commands/memory_commands.py +789 -0
- picklock/commands/pointer_commands.py +582 -0
- picklock/commands/ps_commands.py +325 -0
- picklock/commands/scan_commands.py +1041 -0
- picklock/commands/session_commands.py +751 -0
- picklock/dependencies.py +67 -0
- picklock/errors.py +50 -0
- picklock/output.py +470 -0
- picklock/processes.py +107 -0
- picklock/py.typed +0 -0
- picklock/session.py +455 -0
- picklock/shell.py +417 -0
- picklock/store.py +106 -0
- picklock/valuetypes.py +303 -0
- picklock-0.1.0.dist-info/METADATA +118 -0
- picklock-0.1.0.dist-info/RECORD +25 -0
- picklock-0.1.0.dist-info/WHEEL +4 -0
- picklock-0.1.0.dist-info/entry_points.txt +2 -0
- picklock-0.1.0.dist-info/licenses/LICENSE +21 -0
picklock/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Picklock — a plain-text terminal client for PyMemoryEditor.
|
|
5
|
+
|
|
6
|
+
Picklock exposes PyMemoryEditor's process introspection, memory scanning and
|
|
7
|
+
read/write features through an interactive shell: ASCII result tables, a
|
|
8
|
+
one-line prompt, no curses, no GUI toolkit, no colour beyond a single
|
|
9
|
+
highlight for errors. It runs anywhere Python does — a desktop, a headless
|
|
10
|
+
server, an SSH session, a CI job.
|
|
11
|
+
|
|
12
|
+
The package is a *client*: every memory operation is performed by
|
|
13
|
+
PyMemoryEditor, which Picklock depends on but does not vendor.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__author__ = "Jean Loui Bernard Silva de Jesus"
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
from .errors import CommandError, NoProcessError, PicklockError
|
|
20
|
+
from .session import Session
|
|
21
|
+
from .shell import Shell
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
"CommandError",
|
|
25
|
+
"NoProcessError",
|
|
26
|
+
"PicklockError",
|
|
27
|
+
"Session",
|
|
28
|
+
"Shell",
|
|
29
|
+
"__author__",
|
|
30
|
+
"__version__",
|
|
31
|
+
)
|
picklock/__main__.py
ADDED
picklock/addressing.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The little address language every command shares.
|
|
5
|
+
|
|
6
|
+
Anywhere Picklock takes an address it takes an *expression*, so the workflows
|
|
7
|
+
that matter can be typed on one line instead of copied between commands:
|
|
8
|
+
|
|
9
|
+
=============================== =========================================
|
|
10
|
+
``0x7ffee3a01000`` / ``140...`` a literal, hex or decimal
|
|
11
|
+
``game.exe+0x1234`` a module base plus a static offset (ASLR-proof)
|
|
12
|
+
``"libfoo-1.so"+0x20`` the same, quoted when the name has a ``-``
|
|
13
|
+
``[game.exe+0x1234]+0x10`` read the pointer there, then add ``0x10``
|
|
14
|
+
``[[base+0x8]+0x20]+0x4`` a pointer chain, nested as deep as you like
|
|
15
|
+
``#3`` the address on row 3 of the last scan
|
|
16
|
+
=============================== =========================================
|
|
17
|
+
|
|
18
|
+
The grammar is deliberately tiny — brackets, ``+``, ``-`` and the three kinds
|
|
19
|
+
of term above — because an address expression that needs its own manual page
|
|
20
|
+
has stopped being a convenience.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING, Any, List, NamedTuple, Optional
|
|
24
|
+
|
|
25
|
+
from .errors import CommandError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
28
|
+
from .session import Session
|
|
29
|
+
|
|
30
|
+
_IDENT_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.")
|
|
31
|
+
|
|
32
|
+
#: Token kinds produced by :func:`_tokenize`.
|
|
33
|
+
_NUMBER, _IDENT, _RESULT, _OPEN, _CLOSE, _PLUS, _MINUS = range(7)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Token(NamedTuple):
|
|
37
|
+
"""One lexeme, with what it looked like and whether a space came first.
|
|
38
|
+
|
|
39
|
+
The source text and the spacing are what let a module name containing a
|
|
40
|
+
hyphen be told from a subtraction: ``_ssl.cpython-311-darwin.so`` is one
|
|
41
|
+
name, ``game.exe - 0x10`` is a sum.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
kind: int
|
|
45
|
+
value: Any
|
|
46
|
+
source: str
|
|
47
|
+
spaced_before: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _tokenize(text: str) -> List["_Token"]:
|
|
51
|
+
tokens: List[_Token] = []
|
|
52
|
+
index = 0
|
|
53
|
+
length = len(text)
|
|
54
|
+
spaced = False
|
|
55
|
+
|
|
56
|
+
def emit(kind: int, value: Any, source: str) -> None:
|
|
57
|
+
tokens.append(_Token(kind, value, source, spaced))
|
|
58
|
+
|
|
59
|
+
while index < length:
|
|
60
|
+
char = text[index]
|
|
61
|
+
|
|
62
|
+
if char.isspace():
|
|
63
|
+
index += 1
|
|
64
|
+
spaced = True
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if char == "[":
|
|
68
|
+
emit(_OPEN, "[", "[")
|
|
69
|
+
index += 1
|
|
70
|
+
spaced = False
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if char == "]":
|
|
74
|
+
emit(_CLOSE, "]", "]")
|
|
75
|
+
index += 1
|
|
76
|
+
spaced = False
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if char == "+":
|
|
80
|
+
emit(_PLUS, "+", "+")
|
|
81
|
+
index += 1
|
|
82
|
+
spaced = False
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if char == "-":
|
|
86
|
+
emit(_MINUS, "-", "-")
|
|
87
|
+
index += 1
|
|
88
|
+
spaced = False
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if char == "#":
|
|
92
|
+
index += 1
|
|
93
|
+
start = index
|
|
94
|
+
while index < length and text[index].isdigit():
|
|
95
|
+
index += 1
|
|
96
|
+
if start == index:
|
|
97
|
+
raise CommandError("'#' must be followed by a result number, e.g. #3.")
|
|
98
|
+
emit(_RESULT, int(text[start:index]), text[start - 1 : index])
|
|
99
|
+
spaced = False
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if char in ("'", '"'):
|
|
103
|
+
end = text.find(char, index + 1)
|
|
104
|
+
if end == -1:
|
|
105
|
+
raise CommandError(f"Unterminated {char} in address expression.")
|
|
106
|
+
emit(_IDENT, text[index + 1 : end], text[index : end + 1])
|
|
107
|
+
index = end + 1
|
|
108
|
+
spaced = False
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if char.lower() == "0" and text[index : index + 2].lower() == "0x":
|
|
112
|
+
start = index
|
|
113
|
+
index += 2
|
|
114
|
+
while index < length and text[index] in "0123456789abcdefABCDEF_":
|
|
115
|
+
index += 1
|
|
116
|
+
try:
|
|
117
|
+
emit(_NUMBER, int(text[start:index].replace("_", ""), 16), text[start:index])
|
|
118
|
+
spaced = False
|
|
119
|
+
except ValueError:
|
|
120
|
+
raise CommandError(f"{text[start:index]!r} is not a hex number.")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if char in _IDENT_CHARS:
|
|
124
|
+
start = index
|
|
125
|
+
while index < length and text[index] in _IDENT_CHARS:
|
|
126
|
+
index += 1
|
|
127
|
+
word = text[start:index]
|
|
128
|
+
# A run of digits is a decimal literal; anything else (including
|
|
129
|
+
# "game.exe" and "libc.so.6") is a module name.
|
|
130
|
+
if word.replace("_", "").isdigit():
|
|
131
|
+
emit(_NUMBER, int(word.replace("_", "")), word)
|
|
132
|
+
else:
|
|
133
|
+
emit(_IDENT, word, word)
|
|
134
|
+
spaced = False
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
raise CommandError(f"Unexpected character {char!r} in address expression.")
|
|
138
|
+
|
|
139
|
+
return tokens
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _Parser:
|
|
143
|
+
"""Recursive-descent parser over the token list. One expression per call."""
|
|
144
|
+
|
|
145
|
+
def __init__(self, tokens: List[_Token], session: "Session"):
|
|
146
|
+
self.tokens = tokens
|
|
147
|
+
self.session = session
|
|
148
|
+
self.position = 0
|
|
149
|
+
|
|
150
|
+
def peek(self, ahead: int = 0) -> Optional[_Token]:
|
|
151
|
+
index = self.position + ahead
|
|
152
|
+
if index < len(self.tokens):
|
|
153
|
+
return self.tokens[index]
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def next(self) -> _Token:
|
|
157
|
+
token = self.peek()
|
|
158
|
+
if token is None:
|
|
159
|
+
raise CommandError("Unexpected end of address expression.")
|
|
160
|
+
self.position += 1
|
|
161
|
+
return token
|
|
162
|
+
|
|
163
|
+
def parse_expression(self) -> int:
|
|
164
|
+
value = self.parse_term()
|
|
165
|
+
while True:
|
|
166
|
+
token = self.peek()
|
|
167
|
+
if token is None or token.kind not in (_PLUS, _MINUS):
|
|
168
|
+
return value
|
|
169
|
+
self.position += 1
|
|
170
|
+
operand = self.parse_term()
|
|
171
|
+
value = value + operand if token.kind == _PLUS else value - operand
|
|
172
|
+
|
|
173
|
+
def parse_term(self) -> int:
|
|
174
|
+
token = self.next()
|
|
175
|
+
|
|
176
|
+
if token.kind == _NUMBER:
|
|
177
|
+
return int(token.value)
|
|
178
|
+
|
|
179
|
+
if token.kind == _RESULT:
|
|
180
|
+
return self.session.result_address(int(token.value))
|
|
181
|
+
|
|
182
|
+
if token.kind == _IDENT:
|
|
183
|
+
return self.session.module_base(self._module_name(str(token.value)))
|
|
184
|
+
|
|
185
|
+
if token.kind == _OPEN:
|
|
186
|
+
inner = self.parse_expression()
|
|
187
|
+
closing = self.next()
|
|
188
|
+
if closing.kind != _CLOSE:
|
|
189
|
+
raise CommandError("Missing ']' in address expression.")
|
|
190
|
+
return self.session.read_pointer(inner)
|
|
191
|
+
|
|
192
|
+
raise CommandError("Expected an address, a module name or '[' here.")
|
|
193
|
+
|
|
194
|
+
def _module_name(self, name: str) -> str:
|
|
195
|
+
"""Rejoin a module name the tokenizer split on a hyphen.
|
|
196
|
+
|
|
197
|
+
A hyphen is a subtraction sign and also a perfectly ordinary character
|
|
198
|
+
in a library's name — ``_ssl.cpython-311-darwin.so`` is what every
|
|
199
|
+
Python process is full of. The two are told apart by asking: the pieces
|
|
200
|
+
are rejoined only while they are written without spaces *and* the
|
|
201
|
+
result names a module that is actually loaded. So
|
|
202
|
+
``game.exe-0x10`` stays a subtraction, because no such module exists,
|
|
203
|
+
and ``game.exe - 0x10`` never even gets here.
|
|
204
|
+
"""
|
|
205
|
+
best, consumed = name, 0
|
|
206
|
+
candidate = name
|
|
207
|
+
ahead = 0
|
|
208
|
+
|
|
209
|
+
while True:
|
|
210
|
+
minus, part = self.peek(ahead), self.peek(ahead + 1)
|
|
211
|
+
if minus is None or part is None:
|
|
212
|
+
break
|
|
213
|
+
if minus.kind != _MINUS or minus.spaced_before or part.spaced_before:
|
|
214
|
+
break
|
|
215
|
+
if part.kind not in (_IDENT, _NUMBER):
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
candidate += "-" + part.source
|
|
219
|
+
ahead += 2
|
|
220
|
+
if self.session.knows_module(candidate):
|
|
221
|
+
best, consumed = candidate, ahead
|
|
222
|
+
|
|
223
|
+
self.position += consumed
|
|
224
|
+
return best
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def parse_address(text: str, session: "Session") -> int:
|
|
228
|
+
"""Evaluate an address expression against ``session``.
|
|
229
|
+
|
|
230
|
+
:raises CommandError: on any syntax error, unknown module, out-of-range
|
|
231
|
+
result index or unreadable dereference — all of which are the user's
|
|
232
|
+
to correct, so the shell keeps running.
|
|
233
|
+
"""
|
|
234
|
+
tokens = _tokenize(text)
|
|
235
|
+
if not tokens:
|
|
236
|
+
raise CommandError("Empty address.")
|
|
237
|
+
|
|
238
|
+
parser = _Parser(tokens, session)
|
|
239
|
+
address = parser.parse_expression()
|
|
240
|
+
|
|
241
|
+
if parser.peek() is not None:
|
|
242
|
+
raise CommandError(f"Trailing characters in address {text!r}.")
|
|
243
|
+
if address < 0:
|
|
244
|
+
raise CommandError(f"Address expression {text!r} resolved below zero.")
|
|
245
|
+
return address
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_int(text: str, what: str = "value") -> int:
|
|
249
|
+
"""Parse a plain integer argument (hex or decimal), not an address."""
|
|
250
|
+
cleaned = text.strip().replace("_", "")
|
|
251
|
+
try:
|
|
252
|
+
if cleaned[:2].lower() == "0x":
|
|
253
|
+
return int(cleaned, 16)
|
|
254
|
+
return int(cleaned, 10)
|
|
255
|
+
except (ValueError, IndexError):
|
|
256
|
+
raise CommandError(f"{text!r} is not a valid {what}.")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = ("parse_address", "parse_int")
|
picklock/cli.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The ``picklock`` entry point.
|
|
5
|
+
|
|
6
|
+
Run bare, it opens the interactive shell. Given commands — with ``-e``, as a
|
|
7
|
+
trailing command line, in a file, or on standard input — it runs them and
|
|
8
|
+
exits with a status, so the same vocabulary works inside a script, an SSH
|
|
9
|
+
session or a CI job:
|
|
10
|
+
|
|
11
|
+
picklock # the shell
|
|
12
|
+
picklock ps:list chrome # one command, then exit
|
|
13
|
+
picklock -p 4242 -e "memory:read game.exe+0x10" # attach, read, exit
|
|
14
|
+
picklock -f setup.txt # a file of commands
|
|
15
|
+
echo "ps:list" | picklock # a pipe
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import shlex
|
|
20
|
+
import sys
|
|
21
|
+
from typing import List, Optional, Sequence
|
|
22
|
+
|
|
23
|
+
from . import dependencies
|
|
24
|
+
from .commands import top_level_listing
|
|
25
|
+
from .commands.alias_commands import restore as restore_aliases
|
|
26
|
+
from .commands.session_commands import restore as restore_settings
|
|
27
|
+
from .commands.session_commands import version_report
|
|
28
|
+
from .errors import CommandError, PicklockError
|
|
29
|
+
from .output import Printer
|
|
30
|
+
from .session import Session
|
|
31
|
+
from .shell import Shell
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _format_commands() -> str:
|
|
35
|
+
"""The same layered summary the shell's own ``help`` prints.
|
|
36
|
+
|
|
37
|
+
Every word you can type first, and nothing below it — rather than all
|
|
38
|
+
thirty-odd commands at once, which is a wall rather than an answer.
|
|
39
|
+
"""
|
|
40
|
+
rows = top_level_listing()
|
|
41
|
+
width = max(len(signature) for signature, _ in rows)
|
|
42
|
+
|
|
43
|
+
lines: List[str] = ["picklock commands:", ""]
|
|
44
|
+
# Four spaces between the columns, as the shell's own listings use.
|
|
45
|
+
lines += [f" {signature.ljust(width)} {summary}" for signature, summary in rows]
|
|
46
|
+
lines += [
|
|
47
|
+
"",
|
|
48
|
+
"Run 'picklock help <command>' for what a command takes, or",
|
|
49
|
+
"'picklock help' for the topics ('types', 'address', 'scanning').",
|
|
50
|
+
]
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="picklock",
|
|
57
|
+
description=(
|
|
58
|
+
"A terminal client for PyMemoryEditor: read, write and scan the "
|
|
59
|
+
"memory of a running process from any shell, on Windows, Linux or "
|
|
60
|
+
"macOS."
|
|
61
|
+
),
|
|
62
|
+
epilog=_format_commands(),
|
|
63
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
target = parser.add_argument_group("target")
|
|
67
|
+
target.add_argument("-p", "--pid", type=int, help="attach to this PID at startup")
|
|
68
|
+
target.add_argument("-n", "--name", help="attach to this process name at startup")
|
|
69
|
+
target.add_argument(
|
|
70
|
+
"-i",
|
|
71
|
+
"--ignore-case",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="match --name regardless of case",
|
|
74
|
+
)
|
|
75
|
+
target.add_argument(
|
|
76
|
+
"--partial",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="match --name as a substring ('chrome' finds 'chrome.exe')",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
running = parser.add_argument_group("commands")
|
|
82
|
+
running.add_argument(
|
|
83
|
+
"-e",
|
|
84
|
+
"--execute",
|
|
85
|
+
action="append",
|
|
86
|
+
default=[],
|
|
87
|
+
metavar="COMMAND",
|
|
88
|
+
help="run a command and exit; repeatable, run in order",
|
|
89
|
+
)
|
|
90
|
+
running.add_argument(
|
|
91
|
+
"-f",
|
|
92
|
+
"--file",
|
|
93
|
+
metavar="FILE",
|
|
94
|
+
help="run the commands in FILE and exit",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
output = parser.add_argument_group("output")
|
|
98
|
+
output.add_argument(
|
|
99
|
+
"--no-color", action="store_true", help="never emit ANSI colour"
|
|
100
|
+
)
|
|
101
|
+
output.add_argument(
|
|
102
|
+
"--no-timing", action="store_true", help="omit the elapsed-time footer"
|
|
103
|
+
)
|
|
104
|
+
output.add_argument(
|
|
105
|
+
"--limit",
|
|
106
|
+
type=int,
|
|
107
|
+
metavar="N",
|
|
108
|
+
help="rows printed per result table (0 for no limit)",
|
|
109
|
+
)
|
|
110
|
+
output.add_argument(
|
|
111
|
+
"-q", "--quiet", action="store_true", help="skip the welcome banner"
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"-v",
|
|
115
|
+
"--version",
|
|
116
|
+
action="version",
|
|
117
|
+
# The same block the 'version' command prints. Which PyMemoryEditor is
|
|
118
|
+
# underneath matters as much as which Picklock is on top, and someone
|
|
119
|
+
# pasting this into a bug report should not have to open the shell to
|
|
120
|
+
# get the useful half.
|
|
121
|
+
version=version_report(),
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"command",
|
|
125
|
+
nargs=argparse.REMAINDER,
|
|
126
|
+
help="a single command to run, e.g. 'picklock ps:list chrome'",
|
|
127
|
+
)
|
|
128
|
+
return parser
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _startup_lines(options: argparse.Namespace) -> List[str]:
|
|
132
|
+
"""The commands implied by the target flags, run before anything else."""
|
|
133
|
+
if options.pid is None and options.name is None:
|
|
134
|
+
return []
|
|
135
|
+
if options.pid is not None and options.name is not None:
|
|
136
|
+
raise CommandError("Give --pid or --name, not both.")
|
|
137
|
+
|
|
138
|
+
parts = ["ps:open"]
|
|
139
|
+
if options.pid is not None:
|
|
140
|
+
parts += ["--pid", str(options.pid)]
|
|
141
|
+
else:
|
|
142
|
+
parts += ["--name", shlex.quote(options.name)]
|
|
143
|
+
if options.ignore_case:
|
|
144
|
+
parts.append("-i")
|
|
145
|
+
if options.partial:
|
|
146
|
+
parts.append("--partial")
|
|
147
|
+
return [" ".join(parts)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _batch_lines(options: argparse.Namespace, stdin) -> Optional[List[str]]:
|
|
151
|
+
"""The commands to run non-interactively, or ``None`` for the shell.
|
|
152
|
+
|
|
153
|
+
Standard input counts only when it is *not* a terminal: a pipe or a
|
|
154
|
+
redirect is someone scripting Picklock, while a terminal is someone who
|
|
155
|
+
typed ``picklock`` and wants the prompt.
|
|
156
|
+
"""
|
|
157
|
+
lines: List[str] = []
|
|
158
|
+
|
|
159
|
+
lines.extend(options.execute)
|
|
160
|
+
|
|
161
|
+
if options.command:
|
|
162
|
+
lines.append(" ".join(shlex.quote(part) for part in options.command))
|
|
163
|
+
|
|
164
|
+
if options.file:
|
|
165
|
+
lines.append(f"source {shlex.quote(options.file)}")
|
|
166
|
+
|
|
167
|
+
if lines:
|
|
168
|
+
return lines
|
|
169
|
+
|
|
170
|
+
if not getattr(stdin, "isatty", lambda: True)():
|
|
171
|
+
return stdin.read().splitlines()
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
177
|
+
"""Run Picklock. Returns the process exit status."""
|
|
178
|
+
parser = build_parser()
|
|
179
|
+
options = parser.parse_args(argv)
|
|
180
|
+
|
|
181
|
+
printer = Printer(
|
|
182
|
+
color=False if options.no_color else None,
|
|
183
|
+
timing=not options.no_timing,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Before anything touches a process: a PyMemoryEditor below the declared
|
|
187
|
+
# floor fails later, deep inside a scan, with an error that names a Mach
|
|
188
|
+
# call rather than the cause.
|
|
189
|
+
outdated = dependencies.check()
|
|
190
|
+
if outdated is not None:
|
|
191
|
+
printer.error(outdated)
|
|
192
|
+
return 2
|
|
193
|
+
|
|
194
|
+
session = Session(printer)
|
|
195
|
+
shell = Shell(session, printer=printer)
|
|
196
|
+
|
|
197
|
+
# The aliases the user defined in an earlier run. Loaded here rather than
|
|
198
|
+
# in Session, so a Session built in a test or a script touches no files
|
|
199
|
+
# unless it asks to.
|
|
200
|
+
dropped = restore_aliases(session)
|
|
201
|
+
if dropped:
|
|
202
|
+
printer.note(
|
|
203
|
+
"Dropped %s, whose command no longer exists: %s."
|
|
204
|
+
% (
|
|
205
|
+
"an alias" if len(dropped) == 1 else "some aliases",
|
|
206
|
+
", ".join(sorted(dropped)),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Settings come back too. Restored before --limit is applied, so a flag
|
|
211
|
+
# given on this run still wins over what was stored on the last one.
|
|
212
|
+
forgotten = restore_settings(session)
|
|
213
|
+
if forgotten:
|
|
214
|
+
printer.note(
|
|
215
|
+
"Ignored %s no longer recognised: %s."
|
|
216
|
+
% (
|
|
217
|
+
"a stored setting" if len(forgotten) == 1 else "stored settings",
|
|
218
|
+
", ".join(sorted(forgotten)),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if options.limit is not None:
|
|
223
|
+
session.set_option("limit", str(options.limit))
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
startup = _startup_lines(options)
|
|
227
|
+
except CommandError as error:
|
|
228
|
+
printer.error(str(error))
|
|
229
|
+
return 2
|
|
230
|
+
|
|
231
|
+
batch = _batch_lines(options, sys.stdin)
|
|
232
|
+
interactive = batch is None
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# A failed --pid/--name is fatal either way: the commands that follow
|
|
236
|
+
# were written for a target that is not there.
|
|
237
|
+
for line in startup:
|
|
238
|
+
if not shell.run_line(line, raise_errors=False):
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
if interactive:
|
|
242
|
+
return shell.interact(banner=not options.quiet)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
return shell.run_lines(batch or [], raise_errors=True)
|
|
246
|
+
finally:
|
|
247
|
+
session.close()
|
|
248
|
+
|
|
249
|
+
except PicklockError as error:
|
|
250
|
+
printer.error(str(error))
|
|
251
|
+
return 1
|
|
252
|
+
except KeyboardInterrupt:
|
|
253
|
+
printer.clear_progress()
|
|
254
|
+
printer.write()
|
|
255
|
+
return 130
|
|
256
|
+
except BrokenPipeError: # pragma: no cover - depends on the consumer
|
|
257
|
+
# 'picklock ps | head' closes the pipe early; that is not an error.
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__": # pragma: no cover
|
|
262
|
+
sys.exit(main())
|