File: acl2s.py

package info (click to toggle)
acl2 8.6%2Bdfsg-3
  • links: PTS
  • area: main
  • in suites: forky, sid
  • size: 1,138,276 kB
  • sloc: lisp: 17,818,294; java: 125,359; python: 28,122; javascript: 23,458; cpp: 18,851; ansic: 11,569; perl: 7,678; xml: 5,591; sh: 3,978; makefile: 3,840; ruby: 2,633; yacc: 1,126; ml: 763; awk: 295; csh: 233; lex: 197; php: 178; tcl: 49; asm: 23; haskell: 17
file content (187 lines) | stat: -rw-r--r-- 6,983 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# SPDX-FileCopyrightText: Copyright (c) 2021 Andrew T. Walter <me@atwalter.com>
# SPDX-License-Identifier: MIT
import time
import subprocess
import os
import socket
import struct

ACL2S_COMMAND_RUN_TIMEOUT_S = 20 # seconds in which the ACL2S process should start and run our command

class Acl2sServiceException(Exception):
    pass

class InvalidHeaderException(Acl2sServiceException):
    def __init__(self, actual_header):
        self.message = f"Expected QK but got ${actual_header}."

class MessageSizeMismatchException(Acl2sServiceException):
    def __init__(self, expected, actual):
        self.message = f"Expected message length to be ${expected} bytes, but actually only recieved ${actual} bytes."

class ConnectionTestFailedException(Acl2sServiceException):
    def __init__(self):
        self.message = "Connection test failed - ensure that the service executes correctly and supports the E echo command"

class Acl2sService:
    """A class representing a an ACL2s service. Can either connect to an
    existing ACL2s service with connect(), or start an ACL2s service
    image in a subprocess and manage it.
    """
    def __init__(self, run_timeout=ACL2S_COMMAND_RUN_TIMEOUT_S):
        self.output_read_fd = None
        self.output_write_fd = None
        self.process = None
        self.sock = None
        self.run_timeout = run_timeout

    # 1382728371 is the default random seed for ACL2s
    # See books/acl2s/defdata/random-state-basis1.lisp.
    def start(self, executable, seed=1382728371, debug=False):
        """Start the ACL2s service and connect to it.
        Returns the port that the ACL2s service is listening on.
        
        Note that the optional seed parameter must be a 63-bit unsigned
        integer (per defdata).
        
        The ACL2s service image must output the port number that it is
        listening on to the file descriptor that it is passed in.
        """
        # Create pipes for receiving data from acl2s.
        self.output_read_fd, self.output_write_fd = os.pipe()
        # TODO: preexec_fn does not work on windows. But this is probably
        # true of most of the code in this file.
        # Pipe stdout and stderr to /dev/null unless debug is True.
        stdout = subprocess.DEVNULL
        stderr = subprocess.DEVNULL
        if debug:
            stdout = None
            stderr = None
        self.process = subprocess.Popen(
            [executable, str(self.output_write_fd), str(seed)],
            stdin=subprocess.DEVNULL,
            stdout=stdout,
            stderr=stderr,
            pass_fds=[self.output_write_fd],
            # Ignore CTRL-C
            preexec_fn=os.setpgrp)
        d = os.read(self.output_read_fd, 5)
        decoded = d.decode()
        port = int(decoded)
        self.connect(port, host="0.0.0.0")
        return port

    def connect(self, port, host="0.0.0.0"):
        """Connect to a running ACL2s service on the given port.
        The `host` argument is optional, and by default is 0.0.0.0.
        
        After connecting, this service will test whether the server
        responds as expected to an E (echo) command. If this fails, a
        `ConnectionTestFailedException `is raised but the socket remains
        connected.
        """
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port))
        self.sock.settimeout(self.run_timeout)
        if not self.run_command("E", "test") == ("OK", "test"):
            raise ConnectionTestFailedException()

    def disconnect(self):
        """Disconnect from a running ACL2s service.
        
        This will not stop the service, it will only close the TCP
        connection.
        """
        self.sock.close()

    def stop(self):
        """Stop the running ACL2s service and disconnect from it.
        
        If the ACL2s service was started in this Python instance using
        the `start` method of Acl2sService, the process will also be
        killed, and the file descriptors used to communicate with it at
        initialization time will be closed.
        
        The process kill and file descriptor cleanup will occur even if
        the stop command fails to execute for some reason.
        """
        try:
            self.run_command("Q")
            self.sock.close()
        finally:
            if self.process:
                self.process.kill()
            if self.output_write_fd:
                os.close(self.output_write_fd)
            if self.output_read_fd:
                os.close(self.output_read_fd)

    def write(self, body):
        """Send data to the ACL2s service."""
        body_en = body.encode(encoding="utf-8")
        nbytes = len(body_en)
        return self.sock.send(b''.join([b'QK', struct.pack('>I', nbytes), body_en]))

    def read(self):
        """Recieve data from the ACL2s service.

        This method will raise a `socket.timeout` exception if no
        message is received within the configured `run_timeout`
        duration.
        """
        header = self.sock.recv(6)
        if not header[0:2] == b'QK':
            raise InvalidHeaderException()
        nbytes = struct.unpack('>I', header[2:])[0]
        totbytes = 0
        body = []
        # Note that the message may come in as several TCP packets,
        # so we need to make sure we wait for them all here
        # TODO: add timeout or something
        while totbytes < nbytes:
            data = self.sock.recv(nbytes-totbytes)
            body.append(data)
            totbytes = totbytes + len(data)
        if not totbytes == nbytes:
            raise MessageSizeMismatchException(nbytes, totbytes)
        return b''.join(body).decode(encoding="utf-8")

    def run(self, data):
        """Send a data to the ACL2s service and wait for it to respond.

        This method will raise a `socket.timeout` exception if the Lisp
        command(s) take longer than the configured `run_timeout`
        duration to evaluate.
        """
        self.write(data)
        return self.read()

    def write_command(self, command, data=""):
        if len(command) != 1:
            raise ValueError("Command should be a single-character string.")
        return self.write(command + str(data))

    def read_command(self):
        output = self.read()
        return (output[0:2], output[2:])

    def run_command(self, command, data=""):
        """Send a command to the ACL2s service with (optionally) some
        data and wait for it to respond.

        This method will raise a `socket.timeout` exception if the Lisp
        command(s) take longer than the configured `run_timeout`
        duration to evaluate.
        """
        self.write_command(command, data)
        return self.read_command()

if __name__ == "__main__":
    # TODO: add better example
    #p = Acl2sService()
    #try:
    #    p.start()
    #    print(p.run_command("E", "foobar"))
    #finally:
    #    p.stop()
    pass