apdb 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.
- apdb/__init__.py +5 -0
- apdb/cli.py +57 -0
- apdb/debugger.py +194 -0
- apdb/protocol.py +45 -0
- apdb/server.py +24 -0
- apdb-0.1.0.dist-info/METADATA +103 -0
- apdb-0.1.0.dist-info/RECORD +11 -0
- apdb-0.1.0.dist-info/WHEEL +5 -0
- apdb-0.1.0.dist-info/entry_points.txt +2 -0
- apdb-0.1.0.dist-info/licenses/LICENSE +24 -0
- apdb-0.1.0.dist-info/top_level.txt +1 -0
apdb/__init__.py
ADDED
apdb/cli.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .protocol import dumps_response, loads_json_line
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
10
|
+
DEFAULT_TIMEOUT = 2.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser():
|
|
14
|
+
shared = argparse.ArgumentParser(add_help=False)
|
|
15
|
+
shared.add_argument("--host", default=DEFAULT_HOST)
|
|
16
|
+
shared.add_argument("--port", type=int, required=True)
|
|
17
|
+
shared.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
|
|
18
|
+
|
|
19
|
+
parser = argparse.ArgumentParser(prog="apdb_cli")
|
|
20
|
+
subparsers = parser.add_subparsers(dest="cmd", required=True)
|
|
21
|
+
for name in ("ping", "state", "where", "locals", "next", "step", "continue", "quit"):
|
|
22
|
+
subparsers.add_parser(name, parents=[shared])
|
|
23
|
+
|
|
24
|
+
eval_parser = subparsers.add_parser("eval", parents=[shared])
|
|
25
|
+
eval_parser.add_argument("expr")
|
|
26
|
+
return parser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_request(args):
|
|
30
|
+
request = {"id": 1, "cmd": args.cmd}
|
|
31
|
+
if args.cmd == "eval":
|
|
32
|
+
request["expr"] = args.expr
|
|
33
|
+
return request
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def send_command(host, port, request, timeout=DEFAULT_TIMEOUT):
|
|
37
|
+
with socket.create_connection((host, port), timeout=timeout) as sock:
|
|
38
|
+
sock.sendall(dumps_response(request))
|
|
39
|
+
with sock.makefile("rb") as reader:
|
|
40
|
+
return loads_json_line(reader.readline())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv=None):
|
|
44
|
+
parser = build_parser()
|
|
45
|
+
args = parser.parse_args(argv)
|
|
46
|
+
try:
|
|
47
|
+
response = send_command(args.host, args.port, make_request(args), args.timeout)
|
|
48
|
+
except OSError as exc:
|
|
49
|
+
print(f"connection failed: {exc}", file=sys.stderr)
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
print(json.dumps(response, sort_keys=True))
|
|
53
|
+
return 0 if response.get("ok") else 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
apdb/debugger.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import bdb
|
|
2
|
+
import linecache
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from .protocol import error_response, ok_response
|
|
7
|
+
from .server import APDBTCPServer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RELEASE_COMMANDS = {"continue", "next", "step", "quit"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APDBError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentPdbSession:
|
|
18
|
+
def __init__(self, host, port, header=None):
|
|
19
|
+
self.host = host
|
|
20
|
+
self.port = port
|
|
21
|
+
self.header = header
|
|
22
|
+
self._condition = threading.Condition()
|
|
23
|
+
self._release_event = threading.Event()
|
|
24
|
+
self._server = None
|
|
25
|
+
self._server_thread = None
|
|
26
|
+
self._current_frame = None
|
|
27
|
+
self._current_action = None
|
|
28
|
+
self._done = False
|
|
29
|
+
self._paused = False
|
|
30
|
+
self._trace_mode = None
|
|
31
|
+
self._next_frame = None
|
|
32
|
+
|
|
33
|
+
def start(self):
|
|
34
|
+
try:
|
|
35
|
+
self._server = APDBTCPServer((self.host, self.port), self)
|
|
36
|
+
except OSError as exc:
|
|
37
|
+
raise APDBError(f"could not start apdb server on {self.host}:{self.port}: {exc}") from exc
|
|
38
|
+
|
|
39
|
+
self._server_thread = threading.Thread(
|
|
40
|
+
target=self._server.serve_forever,
|
|
41
|
+
name=f"apdb:{self.host}:{self.port}",
|
|
42
|
+
daemon=True,
|
|
43
|
+
)
|
|
44
|
+
self._server_thread.start()
|
|
45
|
+
|
|
46
|
+
def close(self):
|
|
47
|
+
self._done = True
|
|
48
|
+
if self._server is not None:
|
|
49
|
+
self._server.shutdown()
|
|
50
|
+
self._server.server_close()
|
|
51
|
+
self._server = None
|
|
52
|
+
|
|
53
|
+
def pause(self, frame):
|
|
54
|
+
with self._condition:
|
|
55
|
+
self._current_frame = frame
|
|
56
|
+
self._current_action = None
|
|
57
|
+
self._paused = True
|
|
58
|
+
self._release_event.clear()
|
|
59
|
+
self._condition.notify_all()
|
|
60
|
+
|
|
61
|
+
self._release_event.wait()
|
|
62
|
+
|
|
63
|
+
with self._condition:
|
|
64
|
+
self._paused = False
|
|
65
|
+
return self._current_action
|
|
66
|
+
|
|
67
|
+
def handle_request(self, request):
|
|
68
|
+
cmd = request["cmd"]
|
|
69
|
+
try:
|
|
70
|
+
if cmd == "ping":
|
|
71
|
+
return ok_response(request, {"status": "online"})
|
|
72
|
+
if cmd == "state":
|
|
73
|
+
return ok_response(request, self._state())
|
|
74
|
+
if cmd == "where":
|
|
75
|
+
return ok_response(request, {"frames": self._stack()})
|
|
76
|
+
if cmd == "locals":
|
|
77
|
+
return ok_response(request, self._locals())
|
|
78
|
+
if cmd == "eval":
|
|
79
|
+
return ok_response(request, self._eval(request.get("expr", "")))
|
|
80
|
+
if cmd in RELEASE_COMMANDS:
|
|
81
|
+
return self._release(request, cmd)
|
|
82
|
+
return error_response(request, "unknown_command", f"unknown command: {cmd}")
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
return error_response(request, "command_error", f"{type(exc).__name__}: {exc}")
|
|
85
|
+
|
|
86
|
+
def trace_dispatch(self, frame, event, arg):
|
|
87
|
+
if self._done:
|
|
88
|
+
return None
|
|
89
|
+
if self._should_pause_for_trace(frame, event):
|
|
90
|
+
action = self.pause(frame)
|
|
91
|
+
return self._after_action(action, frame)
|
|
92
|
+
return self.trace_dispatch
|
|
93
|
+
|
|
94
|
+
def _release(self, request, action):
|
|
95
|
+
with self._condition:
|
|
96
|
+
self._current_action = action
|
|
97
|
+
self._release_event.set()
|
|
98
|
+
status = {
|
|
99
|
+
"continue": "continuing",
|
|
100
|
+
"next": "next",
|
|
101
|
+
"step": "step",
|
|
102
|
+
"quit": "quitting",
|
|
103
|
+
}[action]
|
|
104
|
+
return ok_response(request, {"status": status})
|
|
105
|
+
|
|
106
|
+
def _state(self):
|
|
107
|
+
frame = self._current_frame
|
|
108
|
+
result = {"status": "paused" if self._paused else "running"}
|
|
109
|
+
if frame is not None:
|
|
110
|
+
result.update(self._frame_info(frame, index=0))
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def _stack(self):
|
|
114
|
+
frames = []
|
|
115
|
+
frame = self._current_frame
|
|
116
|
+
index = 0
|
|
117
|
+
while frame is not None:
|
|
118
|
+
frames.append(self._frame_info(frame, index=index))
|
|
119
|
+
frame = frame.f_back
|
|
120
|
+
index += 1
|
|
121
|
+
return frames
|
|
122
|
+
|
|
123
|
+
def _locals(self):
|
|
124
|
+
frame = self._require_frame()
|
|
125
|
+
return {name: safe_repr(value) for name, value in sorted(frame.f_locals.items())}
|
|
126
|
+
|
|
127
|
+
def _eval(self, expr):
|
|
128
|
+
if not expr:
|
|
129
|
+
raise ValueError("eval command requires expr")
|
|
130
|
+
frame = self._require_frame()
|
|
131
|
+
value = eval(expr, frame.f_globals, frame.f_locals)
|
|
132
|
+
return {"repr": safe_repr(value), "type": type(value).__name__}
|
|
133
|
+
|
|
134
|
+
def _require_frame(self):
|
|
135
|
+
if self._current_frame is None:
|
|
136
|
+
raise RuntimeError("debugger is not paused at a frame")
|
|
137
|
+
return self._current_frame
|
|
138
|
+
|
|
139
|
+
def _frame_info(self, frame, index):
|
|
140
|
+
filename = frame.f_code.co_filename
|
|
141
|
+
line = linecache.getline(filename, frame.f_lineno).strip()
|
|
142
|
+
return {
|
|
143
|
+
"index": index,
|
|
144
|
+
"file": filename,
|
|
145
|
+
"line": frame.f_lineno,
|
|
146
|
+
"function": frame.f_code.co_name,
|
|
147
|
+
"code": line,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def _should_pause_for_trace(self, frame, event):
|
|
151
|
+
if event != "line":
|
|
152
|
+
return False
|
|
153
|
+
if self._trace_mode == "step":
|
|
154
|
+
return True
|
|
155
|
+
if self._trace_mode == "next":
|
|
156
|
+
return frame is self._next_frame
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def _after_action(self, action, frame):
|
|
160
|
+
if action == "continue":
|
|
161
|
+
self.close()
|
|
162
|
+
sys.settrace(None)
|
|
163
|
+
return None
|
|
164
|
+
if action == "quit":
|
|
165
|
+
self.close()
|
|
166
|
+
sys.settrace(None)
|
|
167
|
+
raise bdb.BdbQuit()
|
|
168
|
+
if action in {"next", "step"}:
|
|
169
|
+
self._trace_mode = action
|
|
170
|
+
self._next_frame = frame
|
|
171
|
+
frame.f_trace = self.trace_dispatch
|
|
172
|
+
sys.settrace(self.trace_dispatch)
|
|
173
|
+
return self.trace_dispatch
|
|
174
|
+
self.close()
|
|
175
|
+
sys.settrace(None)
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def safe_repr(value):
|
|
180
|
+
try:
|
|
181
|
+
return repr(value)
|
|
182
|
+
except Exception:
|
|
183
|
+
return f"<unrepresentable {type(value).__name__}>"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def set_trace(host="127.0.0.1", port=None, header=None):
|
|
187
|
+
if port is None:
|
|
188
|
+
raise TypeError("apdb.set_trace() missing required keyword argument: 'port'")
|
|
189
|
+
|
|
190
|
+
frame = sys._getframe().f_back
|
|
191
|
+
session = AgentPdbSession(host=host, port=port, header=header)
|
|
192
|
+
session.start()
|
|
193
|
+
action = session.pause(frame)
|
|
194
|
+
return session._after_action(action, frame)
|
apdb/protocol.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProtocolError(ValueError):
|
|
5
|
+
def __init__(self, code, message):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.code = code
|
|
8
|
+
self.message = message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def loads_request(line):
|
|
12
|
+
request = loads_json_line(line)
|
|
13
|
+
if not isinstance(request, dict):
|
|
14
|
+
raise ProtocolError("invalid_request", "request must be a JSON object")
|
|
15
|
+
if not request.get("cmd"):
|
|
16
|
+
raise ProtocolError("missing_command", "request must include cmd")
|
|
17
|
+
if not isinstance(request["cmd"], str):
|
|
18
|
+
raise ProtocolError("invalid_command", "cmd must be a string")
|
|
19
|
+
return request
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def loads_json_line(line):
|
|
23
|
+
try:
|
|
24
|
+
payload = json.loads(line.decode("utf-8"))
|
|
25
|
+
except UnicodeDecodeError as exc:
|
|
26
|
+
raise ProtocolError("invalid_encoding", str(exc)) from exc
|
|
27
|
+
except json.JSONDecodeError as exc:
|
|
28
|
+
raise ProtocolError("invalid_json", str(exc)) from exc
|
|
29
|
+
return payload
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def dumps_response(response):
|
|
33
|
+
return (json.dumps(response, sort_keys=True) + "\n").encode("utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ok_response(request, result):
|
|
37
|
+
return {"id": request.get("id"), "ok": True, "result": result}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def error_response(request, code, message):
|
|
41
|
+
return {
|
|
42
|
+
"id": request.get("id"),
|
|
43
|
+
"ok": False,
|
|
44
|
+
"error": {"code": code, "message": message},
|
|
45
|
+
}
|
apdb/server.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import socketserver
|
|
2
|
+
|
|
3
|
+
from .protocol import ProtocolError, dumps_response, error_response, loads_request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class APDBRequestHandler(socketserver.StreamRequestHandler):
|
|
7
|
+
def handle(self):
|
|
8
|
+
line = self.rfile.readline()
|
|
9
|
+
try:
|
|
10
|
+
request = loads_request(line)
|
|
11
|
+
except ProtocolError as exc:
|
|
12
|
+
response = error_response({"id": None}, exc.code, exc.message)
|
|
13
|
+
else:
|
|
14
|
+
response = self.server.session.handle_request(request)
|
|
15
|
+
self.wfile.write(dumps_response(response))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APDBTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
19
|
+
allow_reuse_address = True
|
|
20
|
+
daemon_threads = True
|
|
21
|
+
|
|
22
|
+
def __init__(self, server_address, session):
|
|
23
|
+
self.session = session
|
|
24
|
+
super().__init__(server_address, APDBRequestHandler)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apdb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-friendly remote Python debugger over a zero-dependency TCP API
|
|
5
|
+
Author: apdb contributors
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Keywords: debugger,pdb,agent,tcp,remote-debugger
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# apdb
|
|
25
|
+
|
|
26
|
+
`apdb` is an agent-friendly Python debugger inspired by `remote-pdb`.
|
|
27
|
+
It keeps the Python call site close to `pdb`, but exposes debugger control over
|
|
28
|
+
a newline-delimited JSON TCP API instead of a human interactive shell.
|
|
29
|
+
|
|
30
|
+
The runtime and CLI use only the Python standard library.
|
|
31
|
+
|
|
32
|
+
## Python API
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import apdb
|
|
36
|
+
|
|
37
|
+
answer = 41
|
|
38
|
+
apdb.set_trace(port=4444)
|
|
39
|
+
print(answer + 1)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`set_trace()` blocks the target process whether or not a client is connected.
|
|
43
|
+
The process resumes only after the TCP API receives a release command such as
|
|
44
|
+
`continue`, `next`, `step`, or `quit`.
|
|
45
|
+
|
|
46
|
+
By default, `apdb` binds to `127.0.0.1` and uses no authentication:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
apdb.set_trace(port=4444, host="127.0.0.1")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
The package installs `apdb_cli`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
apdb_cli ping --port 4444
|
|
58
|
+
apdb_cli state --port 4444
|
|
59
|
+
apdb_cli where --port 4444
|
|
60
|
+
apdb_cli locals --port 4444
|
|
61
|
+
apdb_cli eval 'answer + 1' --port 4444
|
|
62
|
+
apdb_cli next --port 4444
|
|
63
|
+
apdb_cli step --port 4444
|
|
64
|
+
apdb_cli continue --port 4444
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The CLI prints one JSON response to stdout and exits nonzero for connection
|
|
68
|
+
failures or API errors.
|
|
69
|
+
|
|
70
|
+
## TCP API
|
|
71
|
+
|
|
72
|
+
The TCP API speaks newline-delimited JSON. Each request and response is one JSON
|
|
73
|
+
object followed by `\n`.
|
|
74
|
+
|
|
75
|
+
Request:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"id": 1, "cmd": "state"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Response:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{"id": 1, "ok": true, "result": {"status": "paused"}}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Supported v0 commands:
|
|
88
|
+
|
|
89
|
+
- `ping`
|
|
90
|
+
- `state`
|
|
91
|
+
- `where`
|
|
92
|
+
- `locals`
|
|
93
|
+
- `eval`
|
|
94
|
+
- `next`
|
|
95
|
+
- `step`
|
|
96
|
+
- `continue`
|
|
97
|
+
- `quit`
|
|
98
|
+
|
|
99
|
+
## Security
|
|
100
|
+
|
|
101
|
+
`apdb` has no authentication in v0. Debugger access can inspect and evaluate
|
|
102
|
+
code inside the target process. Bind to `127.0.0.1` unless you explicitly want
|
|
103
|
+
to expose that control surface.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
apdb/__init__.py,sha256=xLXWQawoqb5qFQnFuULx9kd2v1FoQfOLg905iMN6Eew,155
|
|
2
|
+
apdb/cli.py,sha256=maNWSdMO4FMKOtlGrvkp5y-P5DO8l2DyG1ah7fIciOk,1679
|
|
3
|
+
apdb/debugger.py,sha256=LFbPCKWOuU5Z_cuZ_QiGN6398nT8ifohxBmsyDslxSA,6153
|
|
4
|
+
apdb/protocol.py,sha256=etPnmmVPaIn64C1hF4HxTTUNMn3yOgAW-k4s522xSzo,1293
|
|
5
|
+
apdb/server.py,sha256=-NmcEBHChmfEUs1EbI9GHax94vSmZ4pdfDxB-YHjKcQ,800
|
|
6
|
+
apdb-0.1.0.dist-info/licenses/LICENSE,sha256=lvGLyG5yWbZsUmKNvQoD1wcjE1lpKc2rNTP1L25V0MI,1306
|
|
7
|
+
apdb-0.1.0.dist-info/METADATA,sha256=wkiX5VyhdbPNGw6e56e_Hhiu7yBClQC1f4xRHJ04kz4,2535
|
|
8
|
+
apdb-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
apdb-0.1.0.dist-info/entry_points.txt,sha256=DZTiyDDhf5HZdZcEWKzw-nY2SNm_NUH6bY3BumeHARc,43
|
|
10
|
+
apdb-0.1.0.dist-info/top_level.txt,sha256=FcE0oU1p-OxPclBZ9-Ux3fedckbLNW1-FCK7UB-JRkU,5
|
|
11
|
+
apdb-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, apdb contributors
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
apdb
|