virtuoso-cli 0.3.13

CLI tool to control Cadence Virtuoso from anywhere, locally or remotely
Documentation
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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
;RB = RAMIC Bridge
;RBD = RB Daemon
;RBM = RB Monitor

; Daemon binary path: resolved at load-time, never hardcoded.
; sh() returns t/nil (not stdout), so `which` cannot be used for path detection.
; Priority: (1) RB_DAEMON_PATH env var, (2) ~/.cargo/bin/virtuoso-daemon.
; Re-resolved on reload if previously empty or the file no longer exists.
when(!boundp('RBDPath) || RBDPath == "" || RBDPath == nil || !isFile(RBDPath)
    RBDPath = let((fromEnv cargoPath)
        fromEnv = getShellEnvVar("RB_DAEMON_PATH")
        if(fromEnv then
            fromEnv
        else
            cargoPath = strcat(getShellEnvVar("HOME") "/.cargo/bin/virtuoso-daemon")
            if(isFile(cargoPath) then
                cargoPath
            else
                ""
            )
        )
    )
)
when(RBDPath == "" || RBDPath == nil
    printf("[RAMIC Bridge] ERROR: virtuoso-daemon not found.\n")
    printf("[RAMIC Bridge] Set RB_DAEMON_PATH or run: cargo install --path <repo> --bin virtuoso-daemon --features daemon\n")
)
; RBPython is no longer used (daemon is a standalone binary), kept for compatibility
unless(boundp('RBPython) RBPython = "")
; All state vars preserved across reloads — only initialized if not yet bound.
; RBPort must NOT be reset on reload: the running daemon's actual port would be lost.
unless(boundp('RBPort)       RBPort       = 0)   ; 0 = OS assigns; updated by RBIpcErrHandler
unless(boundp('RBLocal)      RBLocal      = nil)
unless(boundp('RBEcho)       RBEcho       = nil)
unless(boundp('RBDLog)       RBDLog       = nil)
unless(boundp('RBShowBanner) RBShowBanner = nil) ; t = print banner when next PORT: arrives
unless(boundp('RBDVersion)   RBDVersion   = "")  ; set from VERSION: line before PORT:

; IPC process handle — must survive reload so we don't spawn a second daemon.
unless(boundp('RBIpc) RBIpc = 'unbound)

; Session management — persist across reloads so session_id stays stable
unless(boundp('RBSessionId)   RBSessionId   = "")
unless(boundp('RBSessionSeq)  RBSessionSeq  = 0)
; SSH port for tunnel connection — read from env or default to 2222
unless(boundp('RBsshPort)
    RBsshPort = let((envPort)
        envPort = getShellEnvVar("RB_SSH_PORT")
        if(envPort then
            atoi(envPort)
        else
            2222
        )
    )
)

procedure(RBSendCallback(msg)
  ; Write result to a temp file pair instead of ipcWriteProcess — works around
  ; the IC23.1/RHEL8 bug where ipcWriteProcess stops firing after the first call.
  ; Daemon polls for the .done marker, reads data file, then deletes both.
  let((cbPort dataFile doneFile port)
    cbPort = RBPort + 1
    dataFile = sprintf(nil "/tmp/.ramic_cb_%d" cbPort)
    doneFile = sprintf(nil "/tmp/.ramic_cb_%d.done" cbPort)
    port = outfile(dataFile "w")
    when(port fprintf(port "%s" msg) close(port))
    port = outfile(doneFile "w")
    when(port close(port))
  )
)

procedure(RBIpcDataHandler(ipcId data)
  ; IPC data handler: eval SKILL expression, send STX (success) or NAK (error) via callback file.
  ; evalstring() bypasses CIW's interactive loop — printf output is line-buffered
  ; and won't appear until a "\n" is received or hiFlush() is called explicitly.
  ; We call hiFlush() after every evalstring to force CIW UI refresh.
  let((result resultStr)
    when(RBEcho printf("[RAMIC Bridge (%L)] receive:%L\n" ipcId data))
    if(errset(result=evalstring(data)) then
        resultStr = sprintf(nil "%c%L%c" intToChar(2) result intToChar(30))
        when(RBEcho printf("[RAMIC Bridge (%L)] return:%L\n" ipcId result))
    else
        resultStr = sprintf(nil "%c%L%c" intToChar(21) errset.errset intToChar(30))
    )
    RBSendCallback(resultStr)
    hiFlush()  ; force CIW UI refresh — evalstring is line-buffered unlike interactive CIW
  )
)

procedure(RBIpcErrHandler(ipcId data)
    ; Parse VERSION:x.x.x and PORT:XXXXX lines emitted by daemon on startup.
    ; VERSION arrives first so it is stored before the banner fires on PORT.
    rexCompile("VERSION:\\([^ \n]*\\)")
    when(rexExecute(data)
        RBDVersion = rexSubstitute("\\1")
    )
    rexCompile("PORT:\\([0-9][0-9]*\\)")
    when(rexExecute(data)
        let((actualPort)
            actualPort = atoi(rexSubstitute("\\1"))
            when(actualPort > 0
                RBPort = actualPort
                RBWriteSession(RBSessionId RBPort)
                printf("[RAMIC Bridge] Session '%s' on port %d\n" RBSessionId RBPort)
                ; Print banner here so the port is always known-correct.
                when(RBShowBanner
                    RBShowBanner = nil
                    RBPrintBanner()
                )
            )
        )
    )
)

procedure(RBIpcFinishHandler(ipcId data)
  ; Handler called when the Python daemon process exits
  let((exitStatus)
	exitStatus = ipcGetExitStatus(ipcId)
    printf("[RAMIC Bridge (%L)] exit at (%s) with state = %L\n" ipcId getCurrentTime() exitStatus)
  )
)

procedure(RBStart()
    ; Start the RAMIC Bridge daemon process.
    ; Passes port=0 so the OS assigns a free port automatically — supports multiple
    ; Virtuoso instances on the same host without port conflicts.
    ; The daemon prints "PORT:XXXXX" to stderr; RBIpcErrHandler captures it and
    ; writes the session registration file.
	if(boundp('RBIpc) && ipcIsAliveProcess(RBIpc) then
		printf("[RAMIC Bridge (%L)] is already running\n", RBIpc)
	else
		prog((host logpath hostname username)
			; Determine host binding based on RBLocal setting
			if(RBLocal then
				host = "127.0.0.1"  ; Local-only connections
			else
				host = "0.0.0.0"   ; Accept connections from any IP
			)
			; Set log path if logging is enabled
			if(RBDLog then
				logpath = "/tmp/RB.log"
			else
				logpath = ""
			)
			; Generate stable session_id: hostname-username-N (increments per RBStart)
			hostname = getShellEnvVar("HOSTNAME")
			unless(hostname hostname = "localhost")
			hostname = car(parseString(hostname "."))  ; strip domain
			username = getShellEnvVar("USER")
			unless(username username = "user")
			RBSessionSeq = RBSessionSeq + 1
			RBSessionId = sprintf(nil "%s-%s-%d" hostname username RBSessionSeq)
			; Start the daemon with port=0 (OS assigns free port).
			; RBIpcErrHandler will parse PORT:N from stderr and write session file.
			RBIpc = ipcBeginProcess(sprintf(nil "%s %s 0" RBDPath host) "" 'RBIpcDataHandler 'RBIpcErrHandler 'RBIpcFinishHandler logpath)
		)
		printf("[RAMIC Bridge (%L)] start at (%s)\n" RBIpc getCurrentTime())
	)
)

procedure(RBStop()
    ; Stop the RAMIC Bridge daemon process and remove session registration file.
	if(boundp('RBIpc) && ipcIsAliveProcess(RBIpc) then
		ipcKillProcess(RBIpc)
		RBDeleteSession(RBSessionId)
		RBPort = 0   ; clear stale port so next start shows correct value
	else
		printf("[RAMIC Bridge] is already down\n" )
	)
)

procedure(RBStopAll()
    ; Emergency function to kill all RAMIC Bridge daemon processes
    ; 1. Try graceful SIGTERM first, then force SIGKILL after 2s
    ; 2. Also kill any process holding the configured RBPort
    ; 3. Reset Skill-side IPC state
    ; 4. Remove this session's registration file
    sh(sprintf(nil "pgrep -f 'virtuoso-daemon' | xargs -r kill 2>/dev/null; sleep 2; pgrep -f 'virtuoso-daemon' | xargs -r kill -9 2>/dev/null; fuser -k %d/tcp 2>/dev/null" RBPort))
    RBDeleteSession(RBSessionId)
    RBIpc = 'unbound
    printf("[RAMIC Bridge] RBStopAll: all daemon processes killed, port %d released\n" RBPort)
)

procedure(RBWriteSession(sessionId port)
    ; Write session registration file using sh() to avoid SKILL fprintf buffering issues.
    ; File: ~/.cache/virtuoso_bridge/sessions/<session_id>.json
    let((dir filename hostname username)
        when(sessionId != ""
            hostname = getShellEnvVar("HOSTNAME")
            unless(hostname hostname = "localhost")
            username = getShellEnvVar("USER")
            unless(username username = "user")
            dir = strcat(getShellEnvVar("HOME") "/.cache/virtuoso_bridge/sessions")
            filename = strcat(dir "/" sessionId ".json")
            ; Use printf to write JSON (avoids sh() output-capture limitation)
            sh(sprintf(nil "mkdir -p \"%s\"" dir))
            sh(sprintf(nil "printf '{\"id\":\"%s\",\"port\":%d,\"pid\":0,\"host\":\"%s\",\"user\":\"%s\",\"created\":\"%s\"}' > \"%s\""
                sessionId port hostname username getCurrentTime() filename))
        )
    )
)

procedure(RBDeleteSession(sessionId)
    ; Remove session registration file on bridge stop.
    let((filename)
        when(sessionId != ""
            filename = strcat(getShellEnvVar("HOME") "/.cache/virtuoso_bridge/sessions/" sessionId ".json")
            when(isFile(filename)
                deleteFile(filename)
            )
        )
    )
)

; ============================================================================
; GUI Components for the RAMIC Bridge Monitor (once per session)
; ============================================================================
; Re-running hiCreateAppForm / hiInsertBannerMenu on reload causes form/menu
; conflicts; build GUI and banner menu only the first time.
unless(boundp('RBMonInstalled)
progn(
; Status label showing current bridge state
RBMState = hiCreateLabel(
	?name 		'RBMState
	?labelText 	"RAMIC Bridge State: ?"
	?justification CDS_JUSTIFY_LEFT
)

; Refresh button to update status display
RBMBtnRefresh = hiCreateButton(
	?name		'RBMBtnRefresh
	?buttonText	"Refresh"
	?callback	"RBMRefresh()"
)

; Start bridge button
RBMBtnStart = hiCreateButton(
	?name		'RBMBtnStart
	?buttonText	"Start"
	?callback	"RBStart() RBMRefresh()"
)

; Stop bridge button
RBMBtnStop = hiCreateButton(
	?name		'RBMBtnStop
	?buttonText	"Stop"
	?callback	"RBStop() RBMRefresh()"
)

; Port number input field
RBMIntPort = hiCreateIntField(
	?name		'RBMIntPort
	?prompt		"Port"
	?value		RBPort
	?defValue	RBPort
	?callback	nil
)

; Local-only connection checkbox
RBMBolLocal = hiCreateBooleanButton(
	?name		'RBMBolLocal
	?buttonText	"Local connect only"
	?value		RBLocal
	?defValue	RBLocal
	?callback	nil
)

; Echo bridging messages checkbox
RBMBolEcho = hiCreateBooleanButton(
	?name		'RBMBolEcho
	?buttonText	"Echo bridging string"
	?value		RBEcho
	?defValue	RBEcho
	?callback	nil
)

; Enable daemon logging checkbox
RBMBolLog = hiCreateBooleanButton(
	?name		'RBMBolLog
	?buttonText	"Daemon log (/tmp/RB.log)"
	?value		RBDLog
	?defValue	RBDLog
	?callback	nil
)

; Emergency kill all daemons button
RBMBtnStopAll = hiCreateButton(
	?name		'RBMBtnStopAll
	?buttonText	"KILL ALL DAEMON (USE AS BACKUP ONLY)"
	?callback	"RBStopAll() RBMRefresh()"
)

; Create the main monitor form
hiCreateAppForm(
	?name 'RBMonitor
	?formTitle "RAMIC Bridge Monitor"
	?fields list(
		list(RBMState		20:10	300:30)    ; Status label
		list(RBMBtnRefresh 	20:50 	100:30)    ; Refresh button
		list(RBMBtnStart  	150:50 	100:30)    ; Start button
		list(RBMBtnStop  	280:50 	100:30)    ; Stop button
		list(RBMIntPort		20:90	170:30 	30)  ; Port input
		list(RBMBolLocal	20:130	170:30)     ; Local-only checkbox
		list(RBMBolEcho		20:170	170:30)     ; Echo checkbox
		list(RBMBolLog		20:210	170:30)     ; Log checkbox
		list(RBMBtnStopAll 	20:250 	360:30)     ; Emergency stop button
	)
	?buttonLayout	'OKCancelApply
	?help			""
	?initialSize		400:320
	?minSize			400:320
	?maxSize			400:320
	?mapCB			"RBMRefresh()"
	?callback 		"RBMApply()"
)

; ============================================================================
; Menu Integration
; ============================================================================

; Create RAMIC menu in Virtuoso's banner
ramicMenu = hiCreatePulldownMenu(
	'ramicMenu
	"RAMIC"
	list(
		hiCreateMenuItem(
			?name 'RAMIC_Bridge
			?itemText "RAMIC Bridge..."
			?callback "hiDisplayForm(RBMonitor)"
		)
		hiCreateMenuItem(
			?name 'RAMIC_PrintLines
			?itemText "Print Empty Lines"
			?callback "RBPrintEmptyLines()"
		)
	)
)

; Insert the RAMIC menu into Virtuoso's main window
hiInsertBannerMenu(window(1) ramicMenu 3)
RBMonInstalled = t
)
) ; end unless(boundp 'RBMonInstalled) + progn

procedure(RBMRefresh()
    ; Refresh the monitor display with current status and settings
	when(boundp('RBMonInstalled)
	if(boundp('RBIpc) && ipcIsAliveProcess(RBIpc) then
		RBMonitor->RBMState->value = sprintf(nil "RAMIC Bridge State: Running | Session: %s | Port: %d" RBSessionId RBPort)
	else
		RBMonitor->RBMState->value = "RAMIC Bridge State: Down"
	)
	RBMonitor->RBMIntPort->value = RBPort
	RBMonitor->RBMBolLocal->value = RBLocal
	RBMonitor->RBMBolEcho->value = RBEcho
	RBMonitor->RBMBolLog->value = RBDLog
	)
)

procedure(RBMApply()
    ; Apply changes from the monitor form to the configuration
	when(boundp('RBMonInstalled)
	RBEcho = RBMonitor->RBMBolEcho->value
	prog((refresh)
	unless(RBMonitor->RBMBolLocal->value == RBLocal
		RBLocal = RBMonitor->RBMBolLocal->value
		refresh = t
	)
	unless(RBMonitor->RBMIntPort->value == RBPort
		RBPort = RBMonitor->RBMIntPort->value
		refresh = t
	)
	unless(RBMonitor->RBMBolLog->value == RBDLog
		RBDLog = RBMonitor->RBMBolLog->value
		refresh = t
	)
	when(refresh
		RBStop()
		RBStart()
	)
	)
	)
)

procedure(RBPrintEmptyLines()
	printf("\n\n\n\n\n\n\n\n\n\n\n\n")
)

; ============================================================================
; RBPrintBanner — print the Ready box to CIW (called by RBIpcErrHandler
;                 after PORT is confirmed, so the port is always correct)
; ============================================================================

procedure(RBPrintBanner()
    let((home daemonDisp ver)
        home = getShellEnvVar("HOME")
        ; Replace $HOME prefix with ~ for a compact path display
        daemonDisp = RBDPath
        when(strncmp(daemonDisp home strlen(home)) == 0
            daemonDisp = strcat("~" substring(daemonDisp strlen(home) + 1))
        )
        ver = if(RBDVersion != "" then RBDVersion else "?")
        printf("\n")
        printf("┌─────────────────────────────────────────┐\n")
        printf("│  vcli (Virtuoso CLI Bridge) — Ready     │\n")
        printf("├─────────────────────────────────────────┤\n")
        printf("│  Session : %-29s │\n" RBSessionId)
        printf("│  Port    : %-29d │\n" RBPort)
        printf("│  SSH     : %-29d │\n" RBsshPort)
        printf("│  Version : %-29s │\n" ver)
        printf("│  Daemon  : %-29s │\n" daemonDisp)
        printf("├─────────────────────────────────────────┤\n")
        printf("│  Terminal: vcli skill exec 'version()'  │\n")
        printf("│  Sessions: vcli session list            │\n")
        printf("└─────────────────────────────────────────┘\n")
        printf("\n")
    )
)

; ============================================================================
; vcli() — convenience wrapper: start bridge + show Ready banner.
; Banner prints after PORT is received so the port number is always correct.
; ============================================================================

procedure(vcli()
    if(boundp('RBIpc) && ipcIsAliveProcess(RBIpc) && RBPort > 0 then
        ; Already running — print banner immediately (port already known).
        RBPrintBanner()
    else
        ; Starting fresh — set flag so RBIpcErrHandler prints banner on PORT arrival.
        RBShowBanner = t
        RBStart()
        unless(boundp('RBIpc) && ipcIsAliveProcess(RBIpc)
            printf("[vcli] Bridge failed to start. Check RBDPath=%s\n" RBDPath)
            RBShowBanner = nil
        )
    )
)

; ============================================================================
; Auto-start on every load(): stop stale daemon, reset path, start fresh.
; Wrapped in a procedure so no intermediate return values echo in CIW.
; ============================================================================

procedure(_RBAutoStart()
    RBPython = ""
    RBDPath = strcat(getShellEnvVar("HOME") "/.cargo/bin/virtuoso-daemon")
    when(boundp('RBIpc) && ipcIsAliveProcess(RBIpc) RBStop())
    vcli()
)
_RBAutoStart()