-
Notifications
You must be signed in to change notification settings - Fork 0
/
cl-to-python.lisp
416 lines (360 loc) · 15.9 KB
/
cl-to-python.lisp
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
;(ql:quickload :flexi-streams)
;(ql:quickload :bordeaux-threads)
(in-package :cl-to-python)
;(defparameter *python-cmd* "python -u")
(defparameter *python-cmd* "python")
(defparameter *python* nil)
(defparameter *start-isnt-alive-p* t)
;(defparameter *force-restart-always* t)
(defparameter *force-restart-always* nil)
(defparameter *custom-random-state* (make-random-state t))
(defparameter *error-prefix* "_<error_")
;; TODO Binding with python (using this constant also python)
(defparameter *unknown-uuid* "<unknown_uuid_----------------------->")
(defparameter *results-for-unknown-commands* nil)
(defparameter *None* 'None)
(defparameter *wait-result-period* 0.1)
;(defparameter *wait-result-period* 1)
(defparameter *wait-output-period* 0.1)
;(defparameter *wait-output-period* 1)
(defparameter *last-error* nil)
;(defparameter *debug-python-worker* t)
(defparameter *debug-python-worker* nil)
(defstruct python-conn proc-info worker results outputs (worker-exit-p nil))
(defun results-hash ()
(python-conn-results *python*))
(defun outputs-hash ()
(python-conn-outputs *python*))
(defun worker-exit-p ()
(python-conn-worker-exit-p *python*))
(defun unknown-uuid-p (uuid) (string= uuid *unknown-uuid*))
(defun gen-uid ()
(let ((*random-state* *custom-random-state*))
(format nil "[~a]" (princ-to-string (uuid:make-v4-uuid)))))
(defparameter *uuid-len* (length (gen-uid)))
;;;;;;;;;;; Python output ;;;;;;;;;;;;
(defstruct python-output str-output)
(defmethod print-object ((obj python-output) out)
(print-unreadable-object (obj out :type t)
(format out "~s" (python-output-str-output obj))))
;(make-python-output :str-output "Some data")
#|(make-python-output :str-output "Some oute
Newline blabla
Newline 2")
|#
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun get-py-script ()
(uiop:native-namestring
(truename (asdf:component-pathname
(asdf:find-component
:cl-to-python "python-code")))))
;(get-py-script)
(defun prepare-cmd (op cmd &aux #|prepared-cmd|#)
;(setf prepared-cmd (concatenate 'string op (princ-to-string (length cmd)) (string #\Newline) cmd))
#|(if (and (string= "e"op)
(string= "" cmd))
(safing-replace-special prepared-cmd))|#
(concatenate 'string op (princ-to-string (length cmd)) (string #\Newline) cmd)
)
(defun parse-cmd (in-cmd &aux op npos len cmd)
(setf op (subseq in-cmd 0 1))
(setf npos (position #\Newline in-cmd))
(setf len (read-from-string (subseq in-cmd 1 npos)))
(setf cmd (subseq in-cmd (1+ npos) (+ npos 1 len)))
`(:op ,op :cmd ,cmd)
)
(defun get-op (spec)
;(setf spec (parse-cmd in-cmd))
(getf spec :op))
(defun get-cmd (spec)
(getf spec :cmd))
(defun safing-replace-special (str)
(if (search "ee00\\\\nn" str)
(error "Found special string subsequence"))
(ppcre:regex-replace-all "e0\\n" str "ee00\\\\nn"))
;(safing-replace-special (prepare-cmd "e" ""))
(assert (string= (safing-replace-special (uiop:strcat "___e0" (string #\Newline) "___"))
"___ee00\\nn___"))
;; For info
(defun disable-safing-replace (str)
(ppcre:regex-replace-all "ee00\\\\nn" str (uiop:strcat "e0" (string #\Newline))))
(assert (string= (disable-safing-replace "___ee00\\nn___")
(uiop:strcat "___e0" (string #\Newline) "___")))
(defun process-op-cmd (op cmd &aux prepared-cmd)
(setf prepared-cmd (prepare-cmd op cmd)))
(defun write-to-python (op cmd &aux prepared-cmd)
;(setf prepared-cmd (process-op-cmd op cmd))
(setf prepared-cmd (prepare-cmd op cmd))
(log-info prepared-cmd)
(write-sequence (string-to-octets prepared-cmd)
(uiop:process-info-input (python-conn-proc-info *python*)))
(finish-output (uiop:process-info-input (python-conn-proc-info *python*))))
;(write-to-python "e" "")
(defun maybe-restart-python ()
(if *force-restart-always*
(py-start)
(if (and *start-isnt-alive-p* *python* (not (uiop/launch-program:process-alive-p (python-conn-proc-info *python*))))
(py-start))))
(defun deserialize-python-value (seq bytes-count)
#|python_to_lisp_type = {
bool: "BOOLEAN",
type(None): "NULL",
int: "INTEGER",
float: "FLOAT",
complex: "COMPLEX",
list: "VECTOR",
dict: "HASH-TABLE",
str: "STRING",
}
lisp_type_to_sym = {
"BOOLEAN": 'b',
"NULL": 'n',
"INTEGER": 'i',
"FLOAT": 'f',
"COMPLEX": 'c',
"VECTOR": 'v',
"HASH-TABLE": 'h',
"STRING": 's',
}
|#
(let* ((sval (octets-to-string seq))
;; TODO Using view array, using bytes (rid of translate to string)
(uuid (subseq sval 0 *uuid-len*))
(type (elt sval *uuid-len*))
(val (subseq sval (1+ *uuid-len*) bytes-count)))
(log-info "deserialize-python-value: uuid = ~s, type = ~s, val = ~s" uuid type val)
(list (handler-case (case type
(#\s val)
(#\i (parse-integer val))
(#\n *None*)
(#\v (parse-array val))
(#\b (cond
((string= "True" val) t)
((string= "False" val) nil)
(t (error "Bad string for boolean type"))))
(#\_ (progn
(if (search *error-prefix* val)
(error val)
(progn
(warn "Returned native data presentation: ~s" val)
val))))
(#\p (values))
(#\e (error 'python-error :text (format nil "Error from python: ~s" val)))
(#\o (make-python-output :str-output val))
(t (error "Not impelemented for type: ~s" type)))
;(python-error ())
(python-error (err) err))
uuid)
)
)
(defparameter *slime-output* *standard-output*)
(defparameter *slime-error-output* *error-output*)
(defun log-info (msg &rest args)
(format *slime-output* "~%~a" (apply #'format nil msg args))
(finish-output *slime-output*))
(defun log-error (msg &rest args)
(format *slime-error-output* "~%~%[ERROR]: ~a~%" (apply #'format nil msg args))
(finish-output *error-output*))
(defun set-wait-res (key-uuid out-hash)
(setf (gethash key-uuid out-hash) (list nil nil)))
(defun set-res (key-uuid res out-hash)
(if (unknown-uuid-p key-uuid)
(progn
(warn "Added result for unknown uuid to *results-for-unknown-commands*, result = ~s" res)
(push res *results-for-unknown-commands*))
(multiple-value-bind (val present-p)
(gethash key-uuid out-hash)
(if (not present-p)
(error "Not waiting result for uid = ~s" key-uuid))
(destructuring-bind (saved &rest rest)
(coerce val 'list)
(declare (ignore rest))
(if saved (error "Result already saved"))
(setf (gethash key-uuid out-hash) (vector t res))))))
(defun get-res (key-uuid out-hash)
(log-info "results-hash = ~s" out-hash)
(multiple-value-bind
(val present-p)
(gethash key-uuid out-hash)
(if (not present-p)
(values nil nil)
(destructuring-bind (has-come-p res)
(coerce val 'list)
(values res has-come-p)))))
#|
(defparameter *test-py* nil)
(defun test-run ()
(setf *test-py* (uiop:launch-program (uiop:strcat *python-cmd* " " (get-py-script)) :output :stream :input :stream :error-output :stream)))
(defun raise-err (&aux op-seq proc)
(setf op-seq (make-sequence '(vector (unsigned-byte 8)) 1))
(read-sequence op-seq (uiop:process-info-output proc) :end 1)
(log-info (format nil "=== OK ===: ~s" (octets-to-string op-seq))))
|#
;(worker-python)
(define-condition python-error (error)
((text :initarg :text :reader text)))
(defun %worker-python (&aux slen len)
(loop with python-output = (uiop:process-info-output (python-conn-proc-info *python*))
with payload-seq
with payload-count = 0
;; For describe of stages
with read-op = 'read-op
with read-len = 'read-len
with read-payload = 'read-payload
with stage = read-op
:while (not (worker-exit-p))
do
(log-info "=== Current stage = ~s" stage)
(case stage
(read-op (let (op-code)
(log-info "=== before read op")
(handler-case (setf op-code (read-byte python-output))
(error (e)
(format t "Error on ~s: ~s" stage e)
(loop-finish)))
(log-info "=== after read op, op-code = ~s, op = ~a" op-code (code-char op-code))
(if (not op-code)
(progn
;; TODO 0.1 to variable
(sleep 0.1))
(let ((op (code-char op-code)))
(log-info "=== Analysing op: ~s" op op-code)
(if (not (or (char= #\e op)
(char= #\r op)))
(error "OP `~s` dont supported (code: ~s)" op op-code))
;; TODO 0.1 to variable
(setf stage read-len)))))
(read-len (progn
(log-info "=== before read slen")
(handler-case (setf slen (read-line python-output))
(error (e)
(format t "Error on ~s: ~s" stage e)
(loop-finish)))
(log-info "=== after read slen, slen = ~s" slen)
(setf len (ignore-errors (parse-integer slen)))
(when (not len)
(progn (error "Bad parsing len: ~s" slen)
(loop-finish)))
(log-info "=== before set stage: " read-payload)
(setf stage read-payload)
(log-info "=== after set stage, stage = ~s" stage)))
(read-payload (let (bytes-count)
(log-info "=== before read payload")
(if (zerop payload-count)
;; Start read payload
(setf payload-seq (make-sequence '(vector (unsigned-byte 8)) len)))
;(setf payload-view (make-array (- len payload-count) :element-type '(unsigned-byte 8) :displaced-to payload-seq :displaced-index-offset payload-count))
(handler-case (setf bytes-count (read-sequence payload-seq python-output :start payload-count :end len))
(error (e)
(format t "Error on ~s: ~s" stage e)
(loop-finish)))
(log-info "=== after read payload, bytes-count = ~s, payload-seq as string = ~s, payload-view as string = ~s"
bytes-count (octets-to-string payload-seq) (octets-to-string (subseq payload-seq payload-count len)))
(if (> (+ payload-count bytes-count) len)
(error "Bad algorithm for reading payload"))
(let ((readed-bytes (+ payload-count bytes-count)))
(if (= readed-bytes len)
(progn
(destructuring-bind (val uuid)
(deserialize-python-value payload-seq readed-bytes)
(set-res uuid val (if (typep val 'python-output)
(outputs-hash)
(results-hash)))
(log-info (format nil "After set-res: res = ~s, uuid = ~s" val uuid)))
(setf payload-count 0
payload-seq nil
stage read-op)))))))))
(defun worker-python ()
(if (not (python-alive-p))
(progn
(error "Python don't running (maybe required to call (py-start)?)"))
(handler-case (%worker-python)
(error (err)
(log-error "in worker-python: ~a " err)
(setf *last-error* err)
(if *debug-python-worker* (error err))))))
(defun run-python-worker (&optional (python *python*))
(log-info "Before run worker")
(setf (python-conn-worker python)
(make-thread 'worker-python :name "Python thread" :initial-bindings (cons (list '*python* 'identity *python*) *default-special-bindings*))
)
(log-info "After run worker: ~s" (python-conn-worker python)))
(defun py-start (&aux python-proc-info)
(if (python-alive-p) (py-stop))
(setf python-proc-info (uiop:launch-program (uiop:strcat *python-cmd* " " (get-py-script)) :output :stream :input :stream :error-output :stream))
(setf *python* (make-python-conn :proc-info python-proc-info
;; TODO Move to constructor
:results (make-hash-table :test 'equal)
:outputs (make-hash-table :test 'equal)))
(run-python-worker *python*))
(defun python-alive-p ()
#|(break "in python-alive-p, wait result = ~s" (if (and *python* (python-conn-proc-info *python*))
(uiop:process-alive-p (python-conn-proc-info *python*))))|#
(if (and *python* (python-conn-proc-info *python*))
(uiop:process-alive-p (python-conn-proc-info *python*))))
(defun py-kill (&aux process)
(setf process (slot-value (python-conn-proc-info *python*) 'uiop/launch-program::process))
(when (sb-ext:process-alive-p process)
;; Note: uiop:terminate-process - don't working this
(sb-ext:process-kill process 15)))
(defun py-stop (&optional (kill-python-cmd-p t))
(when (python-alive-p)
(write-to-python "f" "")
;; TODO 0.1 to variable
(sleep 0.1)
(let ((thread-python (python-conn-worker *python*)))
(if (and thread-python (thread-alive-p thread-python))
(destroy-thread thread-python))
(if (and kill-python-cmd-p)
;; TODO kill python child
(py-kill)))
nil))
;; TODO move to top
;(py-start)
(defparameter *continue-sent-empty-command* t)
;(defparameter *continue-sent-empty-command* nil)
(defun wait-value-loop (uuid out-hash)
(loop
with res
while t
finally (return res)
do
(multiple-value-bind (val has-come-p)
(progn
(log-info "trying getting res, is output wait: ~a" (eql out-hash (outputs-hash)))
(let ((val-and-has-come-p (multiple-value-list (get-res uuid out-hash))))
(log-info "got results: ~s" val-and-has-come-p)
(values-list val-and-has-come-p)))
(if has-come-p
(progn
(setf res val)
(remhash uuid out-hash)
(loop-finish))
;; Note: It required than wakeup python (it hanging on read input on my system (windows))
(progn
(log-info "res don't got")
(if *continue-sent-empty-command*
;; TODO Rid of using uuid for stub message
(let ((uuid-stub (gen-uid)))
(write-to-python "e" uuid-stub)))
;; TODO 1 to variable
(sleep *wait-result-period*))
))))
(defun send-to-python (cmd &optional (op "e") &aux #|seq bytes-count slen th|# uuid)
(maybe-restart-python)
(if (not (python-alive-p))
(error "Python isn't running"))
(setf uuid (gen-uid))
(setf cmd (uiop:strcat uuid cmd))
(set-wait-res uuid (results-hash))
(set-wait-res uuid (outputs-hash))
(write-to-python op cmd)
(values (wait-value-loop uuid (results-hash))
(wait-value-loop uuid (outputs-hash))))
(defun py-eval (cmd)
(send-to-python cmd "e"))
(defun py-peval (cmd)
(send-to-python cmd "p"))
(defun py-exec (cmd)
(send-to-python cmd "x"))
;;;;;;;;;;;;;;