Skip to content

Commit bf359d1

Browse files
authored
Merge pull request #89 from 3270io/claude/demo-screen-recordings-edsqd9
Record the profiler and MCP demos, and fix what stopped the profiler running
2 parents 89523c4 + 4853fe5 commit bf359d1

22 files changed

Lines changed: 823 additions & 25 deletions

connect3270/emulator.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,11 @@ func (e *Emulator) WaitForField(timeout time.Duration, maxRetries int) error {
239239
unlockOutput, unlockErr := e.execCommand(unlockCommand)
240240

241241
// Query keyboard lock state after Wait(timeout, Unlock)
242-
if kbLockState, kbErr := e.query("KeyboardLock"); kbErr == nil {
243-
if Verbose {
244-
log.Printf("Keyboard lock state after Unlock wait: %s", kbLockState)
245-
}
246-
}
242+
if kbLockState, kbErr := e.query("KeyboardLock"); kbErr == nil {
243+
if Verbose {
244+
log.Printf("Keyboard lock state after Unlock wait: %s", kbLockState)
245+
}
246+
}
247247

248248
// Check if unlock failed or status is not "U"
249249
needsReset := false
@@ -262,13 +262,13 @@ func (e *Emulator) WaitForField(timeout time.Duration, maxRetries int) error {
262262
// Retry unlock after reset
263263
time.Sleep(retryDelay)
264264
unlockOutput, unlockErr = e.execCommand(unlockCommand)
265-
265+
266266
// Query keyboard lock state again after reset
267-
if kbLockState, kbErr := e.query("KeyboardLock"); kbErr == nil {
268-
if Verbose {
269-
log.Printf("Keyboard lock state after Reset and Unlock: %s", kbLockState)
270-
}
271-
}
267+
if kbLockState, kbErr := e.query("KeyboardLock"); kbErr == nil {
268+
if Verbose {
269+
log.Printf("Keyboard lock state after Reset and Unlock: %s", kbLockState)
270+
}
271+
}
272272
}
273273
}
274274

@@ -279,12 +279,12 @@ func (e *Emulator) WaitForField(timeout time.Duration, maxRetries int) error {
279279
for retries := 0; retries < maxRetries; retries++ {
280280
output, err := e.execCommand(command)
281281
if err == nil {
282-
if output == "" {
283-
if Verbose {
284-
log.Println("Wait command executed successfully (no output)")
285-
}
286-
return nil
287-
}
282+
if output == "" {
283+
if Verbose {
284+
log.Println("Wait command executed successfully (no output)")
285+
}
286+
return nil
287+
}
288288

289289
// Extract the keyboard status from the command output
290290
statusParts := strings.Fields(output)
@@ -511,6 +511,16 @@ func (e *Emulator) GetValue(x, y, length int) (string, error) {
511511
return "", fmt.Errorf("maximum GetValue retries reached")
512512
}
513513

514+
// NormalizeQueryResponse reduces a raw Query reply to the value it carries.
515+
//
516+
// s3270 answers on the scripting protocol with the value on a "data:" line
517+
// followed by a status line and "ok". Callers that want to parse the value —
518+
// the host compatibility profiler is one — need the value alone, and Query
519+
// deliberately hands back the reply untouched.
520+
func NormalizeQueryResponse(raw string) string {
521+
return normalizeAsciiData(raw)
522+
}
523+
514524
// normalizeAsciiData trims the s3270/x3270 "data:" prefix and drops status lines.
515525
func normalizeAsciiData(raw string) string {
516526
lines := strings.Split(raw, "\n")

connect3270/emulator_test.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,28 @@ func TestWaitForFieldErrorIncludesKeyboardLockDetail(t *testing.T) {
151151
// We can't fully test WaitForField without a real connection,
152152
// but we can verify that when it would fail, the error format includes
153153
// the KeyboardLockDetail message.
154-
154+
155155
// This test verifies the error message structure by checking that the
156156
// failure path includes the expected "KeyboardLockDetail:" text
157157
// The actual implementation will add this detail when query succeeds
158158
expectedSubstring := "KeyboardLockDetail:"
159-
159+
160160
// Create an emulator (won't be connected)
161161
e := &Emulator{
162162
Host: "test.host",
163163
Port: 23,
164164
ScriptPort: "5000",
165165
}
166-
166+
167167
// Call WaitForField with minimal retries (will fail without connection)
168168
// This should timeout and include KeyboardLockDetail in the error
169169
err := e.WaitForField(1, 1)
170-
170+
171171
// Verify error occurred (expected since there's no connection)
172172
if err == nil {
173173
t.Fatal("Expected WaitForField to fail without connection")
174174
}
175-
175+
176176
// Verify the error message contains the KeyboardLockDetail marker
177177
if !strings.Contains(err.Error(), expectedSubstring) {
178178
t.Errorf("WaitForField error should contain '%s', got: %v", expectedSubstring, err)
@@ -260,3 +260,44 @@ func TestCaptureAttrsEscapeValues(t *testing.T) {
260260
t.Errorf("captureAttrs() = %q, want the host escaped", attrs)
261261
}
262262
}
263+
264+
// The host compatibility profiler parses these replies into a document that is
265+
// diffed against 3270Web's, so the value has to arrive without s3270's framing
266+
// around it. Leaving the prefix on produced a profile that described the
267+
// framing: host "data", terminal type "data:", 24 columns instead of 80.
268+
func TestNormalizeQueryResponse(t *testing.T) {
269+
cases := []struct {
270+
name string
271+
raw string
272+
want string
273+
}{
274+
{
275+
name: "strips the prefix and the status trailer",
276+
raw: "data: 127.0.0.1:3270\nU F U C(127.0.0.1) I 2 24 80 4 20 0x0 0.000\nok\n",
277+
want: "127.0.0.1:3270",
278+
},
279+
{
280+
name: "handles the prefix without a following space",
281+
raw: "data:IBM-3279-2-E\nok\n",
282+
want: "IBM-3279-2-E",
283+
},
284+
{
285+
name: "returns the reply unchanged when there is no data line",
286+
raw: "ok\n",
287+
want: "ok",
288+
},
289+
{
290+
name: "empty stays empty",
291+
raw: "",
292+
want: "",
293+
},
294+
}
295+
296+
for _, tc := range cases {
297+
t.Run(tc.name, func(t *testing.T) {
298+
if got := NormalizeQueryResponse(tc.raw); got != tc.want {
299+
t.Errorf("NormalizeQueryResponse(%q) = %q, want %q", tc.raw, got, tc.want)
300+
}
301+
})
302+
}
303+
}
23 KB
Loading
1.39 MB
Binary file not shown.
42.4 KB
Loading
1.91 MB
Binary file not shown.

docs/host-profiler.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ captures the first screen's banner signature, records timing, and writes
3131
the JSON to `-profileOut` (or stdout when omitted). The process exits
3232
non-zero on failure so CI can fail fast.
3333

34+
<figure class="demo-video">
35+
<video controls preload="metadata" playsinline
36+
poster="/assets/video/terminal-host-profiler.jpg">
37+
<source src="/assets/video/terminal-host-profiler.mp4" type="video/mp4">
38+
<a href="/assets/video/terminal-host-profiler.mp4">Download the video</a>.
39+
</video>
40+
<figcaption>
41+
Profiling the bundled sample host: the probe, the device and protocol it
42+
reports, and the queries it could not get an answer to.
43+
</figcaption>
44+
</figure>
45+
3446
## Flags
3547

3648
| Flag | Description |

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ Shorter walk-throughs of one thing at a time:
224224
| Call it over HTTP — one POST returns the screen | [Advanced Features](advanced-features.md#api-mode-in-practice) |
225225
| The operations console, end to end | [Web Dashboard](dashboard.md#a-tour-of-the-console) |
226226
| Sign-in and administration with `AUTH_MODE=local` | [Accounts and Sign-In](authentication.md#what-it-looks-like) |
227+
| Profile a host before you trust a workflow against it | [Host Compatibility Profiler](host-profiler.md#quick-start) |
228+
| Drive it from an AI client over MCP | [MCP Server](mcp.md#check-it-works-first) |
227229

228230
## Conclusion
229231

docs/mcp.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ That prints the tool catalogue as JSON and exits. It needs no host, no
3131
workflow and no run in progress. If you see JSON, the wiring is right and
3232
anything that fails afterwards is configuration.
3333

34+
<figure class="demo-video">
35+
<video controls preload="metadata" playsinline
36+
poster="/assets/video/terminal-mcp-server.jpg">
37+
<source src="/assets/video/terminal-mcp-server.mp4" type="video/mp4">
38+
<a href="/assets/video/terminal-mcp-server.mp4">Download the video</a>.
39+
</video>
40+
<figcaption>
41+
The tool catalogue, the descriptions a model reads to choose between tools,
42+
and a real stdio session — an <code>initialize</code> followed by a
43+
<code>tools/call</code> — answered by the server by hand.
44+
</figcaption>
45+
</figure>
46+
3447
## Setting up Claude Desktop
3548

3649
1. Open **Claude Desktop****Settings****Developer****Edit Config**.

go3270Connect.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,11 @@ func main() {
16911691
startPrometheusListener(promListen)
16921692
}
16931693

1694+
// Before the profiler branch, not after it: -verbose and -headless are only
1695+
// two assignments, and leaving them until later meant a -profile run was
1696+
// driven by whatever the defaults happened to be.
1697+
setGlobalSettings()
1698+
16941699
if profileMode {
16951700
runProfileMode()
16961701
return

0 commit comments

Comments
 (0)