Alternate screen size support.
This is a relatively big new feature, which introduces small breaking changes to the API: * NegotiateTelnet() now returns (DevInfo, error) instead of just error. The function signature for the Tx type now has one additional DevInfo parameter. * RunTransactions() requires one additional parameter, the DevInfo (which is allowed to be nil). Otherwise, existing behavior continues to work as-is and everything still defaults to operating on the default 24x80 terminal size. See example5 for a larger-terminal-aware application. Pending some time to make sure no new bugs cropped up in this release and that the alternate screen size support works in the wild, I anticipate this will become the v1.0.0 release of go3270.
This commit is contained in:
1 parent
3a38ce6e4a
commit
b6fa48f37f
19 files changed
+820
-126
No files matched your search
@@ -3,3 +3,4 @@ example1/example1
|
||||
example2/example2
|
||||
example3/example3
|
||||
example4/example4
|
||||
example5/example5
|
||||
@@ -10,7 +10,13 @@ This library allows you to write Go servers for tn3270 clients by building 3270
|
||||
Usage
|
||||
-----
|
||||
|
||||
See the example folders for quick demonstrations of using the library. example1 uses the lower-level function ShowScreenOpts(), and example2 uses a higher-level function HandleScreen(). example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
|
||||
See the example folders for quick demonstrations of using the library:
|
||||
|
||||
* example1 uses the lower-level function ShowScreenOpts().
|
||||
* example2 uses a higher-level convenience function HandleScreen().
|
||||
* example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
|
||||
* example4 demonstrates the RunTransactions() approach to handing control from one screen to the next. This is the recommended way to build applications using go3270.
|
||||
* example5 demonstrates support for larger-than-default (24x80) terminal sizes.
|
||||
|
||||
**NEW**: For larger applications, I recommend using the `RunTransactions()` function to serve as the driver for your application. You can implement transaction functions which pass control from one transaction to another. example4 demonstrates a "larger" application that uses this approach.
|
||||
|
||||
@@ -18,22 +24,17 @@ Here's [a video introducing the library][introVideo] as well.
|
||||
|
||||
[introVideo]: https://www.youtube.com/watch?v=h9XTjup5W5U
|
||||
|
||||
Known Problems
|
||||
--------------
|
||||
|
||||
- The telnet negotiation does not check for any errors or for any responses from the client. We just assume it goes well and we're actually talking to a tn3270 client.
|
||||
- Screen size is limited to exactly 24x80. In the future, the terminal type could be interrogated and the library could support the screen sizes of other 3270 models.
|
||||
|
||||
3270 information
|
||||
----------------
|
||||
|
||||
Everything I know about 3270 data streams I learned from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions].
|
||||
I started learning about 3270 data streams from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions]. The reference I use for 3270 data streams is [the 1981 version from IBM][ibmref].
|
||||
|
||||
[sprinkle]: http://www.tommysprinkle.com/mvs/P3270/
|
||||
[rfc1576]: https://tools.ietf.org/html/rfc1576
|
||||
[rfc1041]: https://tools.ietf.org/html/rfc1041
|
||||
[rfc854]: https://tools.ietf.org/html/rfc854
|
||||
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml
|
||||
[ibmref]: https://bitsavers.org/pdf/ibm/3270/GA23-0059-0_3270_Data_Stream_Programmers_Reference_Jan1981.pdf
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
+14
-2
@@ -9,15 +9,21 @@ import (
|
||||
)
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
encoded := getpos(0, 0)
|
||||
encoded := getpos(0, 0, 80)
|
||||
if encoded[0] != 0x40 || encoded[1] != 0x40 {
|
||||
t.Error("Position (0, 0) not correctly encoded")
|
||||
}
|
||||
|
||||
encoded = getpos(11, 39)
|
||||
encoded = getpos(11, 39, 80)
|
||||
if encoded[0] != 0x4e || encoded[1] != 0xd7 {
|
||||
t.Error("Position (11, 39) not correctly encoded")
|
||||
}
|
||||
|
||||
// Large screen, 14-bit addressing
|
||||
encoded = getpos(100, 120, 130)
|
||||
if encoded[0] != 0x33 || encoded[1] != 0x40 {
|
||||
t.Errorf("Position (100, 120) on 130-col screen not correctly encoded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
@@ -30,4 +36,10 @@ func TestDecode(t *testing.T) {
|
||||
if decoded != 919 {
|
||||
t.Error("Buffer address incorrectly decoded")
|
||||
}
|
||||
|
||||
// Large screen, 14-bit addressing
|
||||
decoded = decodeBufAddr([2]byte{0x33, 0x40})
|
||||
if decoded != 13120 {
|
||||
t.Error("14-bit buffer address incorrectly decoded")
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,10 @@ func handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// Always begin new connection by negotiating the telnet options
|
||||
go3270.NegotiateTelnet(conn)
|
||||
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||
fmt.Printf("ERROR: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
var response go3270.Response
|
||||
|
||||
@@ -90,7 +90,10 @@ func handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// Always begin new connection by negotiating the telnet options
|
||||
go3270.NegotiateTelnet(conn)
|
||||
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ func handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// Always begin new connection by negotiating the telnet options
|
||||
go3270.NegotiateTelnet(conn)
|
||||
if _, err := go3270.NegotiateTelnet(conn); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// First, let's send the initial screen and wait forever for the user to
|
||||
// press PF3, and when we get it, send a message on the done channel.
|
||||
|
||||
@@ -75,8 +75,13 @@ func handle(conn net.Conn, db DB, gblstate *global) {
|
||||
}()
|
||||
|
||||
// Always begin new connection by negotiating the telnet options
|
||||
go3270.NegotiateTelnet(conn)
|
||||
err := go3270.RunTransactions(conn, state.login, nil)
|
||||
devinfo, err := go3270.NegotiateTelnet(conn)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = go3270.RunTransactions(conn, devinfo, state.login, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@ var helpScreen = go3270.Screen{
|
||||
// generator function for it is a stand-alone function and not a method on the
|
||||
// session.
|
||||
func help(returnTransaction go3270.Tx) go3270.Tx {
|
||||
return func(conn net.Conn, data any) (go3270.Tx, any, error) {
|
||||
return func(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||
go3270.Tx, any, error) {
|
||||
_, err := go3270.HandleScreen(
|
||||
helpScreen, // the screen to display
|
||||
nil, // (no) rules to enforce
|
||||
|
||||
+4
-2
@@ -58,7 +58,8 @@ var loginScreenRules = go3270.Rules{
|
||||
|
||||
// login transaction accepts a string value in data if the login screen
|
||||
// should be initialized with an error message.
|
||||
func (sess *session) login(conn net.Conn, data any) (go3270.Tx, any, error) {
|
||||
func (sess *session) login(conn net.Conn, _ go3270.DevInfo,
|
||||
data any) (go3270.Tx, any, error) {
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
@@ -169,7 +170,8 @@ type newuserData struct {
|
||||
errmsg string
|
||||
}
|
||||
|
||||
func (sess *session) newuser(conn net.Conn, data any) (go3270.Tx, any, error) {
|
||||
func (sess *session) newuser(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||
go3270.Tx, any, error) {
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ type mainmenuData struct {
|
||||
|
||||
// mainmenu transaction accepts a mainmenuData struct as the data if the
|
||||
// option field or error message should be populated.
|
||||
func (sess *session) mainmenu(conn net.Conn, data any) (go3270.Tx, any, error) {
|
||||
func (sess *session) mainmenu(conn net.Conn, _ go3270.DevInfo, data any) (
|
||||
go3270.Tx, any, error) {
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
@@ -182,8 +183,8 @@ var exampleScreen = go3270.Screen{
|
||||
// exampleFeature is a transaction that will act as a placeholder for real
|
||||
// application functionality. It accepts a string in the data which will
|
||||
// be displayed on the panel.
|
||||
func (sess *session) exampleFeature(conn net.Conn, data any) (go3270.Tx,
|
||||
any, error) {
|
||||
func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo,
|
||||
data any) (go3270.Tx, any, error) {
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// This file is part of https://github.com/racingmars/go3270/
|
||||
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||
// LICENSE in the project root for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/racingmars/go3270"
|
||||
)
|
||||
|
||||
var biglayout = go3270.Screen{
|
||||
// Column will be calculated at runtime for the following field:
|
||||
{Row: 0, Intense: true, Content: "3270 Screen Size Example"},
|
||||
|
||||
{Row: 2, Col: 0,
|
||||
Content: "This screen is using the full size that your terminal supports."},
|
||||
|
||||
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
|
||||
{Row: 4, Col: 21, Name: "termtype", Intense: true},
|
||||
|
||||
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
|
||||
{Row: 5, Col: 21, Name: "rows", Intense: true},
|
||||
|
||||
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
|
||||
{Row: 6, Col: 21, Name: "cols", Intense: true},
|
||||
|
||||
{Row: 8, Col: 0, Content: "To visit a default sized screen, press"},
|
||||
{Row: 8, Col: 39, Content: "PF1", Color: go3270.Yellow, Intense: true},
|
||||
|
||||
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
|
||||
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
|
||||
|
||||
// a blank field for error messages
|
||||
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
|
||||
}
|
||||
|
||||
func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
|
||||
go3270.Tx, any, error) {
|
||||
|
||||
rows, cols := devinfo.AltDimensions()
|
||||
termtype := devinfo.TerminalType()
|
||||
|
||||
// Make a local copy of the screen definition that we can append lines to.
|
||||
screen := make(go3270.Screen, len(biglayout))
|
||||
copy(screen, biglayout)
|
||||
|
||||
// Center the title on any screen width
|
||||
screen[0].Col = (cols / 2) - (len(biglayout[0].Content) / 2)
|
||||
|
||||
// We'll start writing "data lines" at row 13 up to the penultimate row on
|
||||
// the terminal
|
||||
for i := 13; i < rows-1; i++ {
|
||||
newfield := go3270.Field{Row: i, Col: 0,
|
||||
Content: fmt.Sprintf("This is data row %d.", i-12)}
|
||||
if i == rows-2 {
|
||||
newfield.Content += " (The last.)"
|
||||
}
|
||||
screen = append(screen, newfield)
|
||||
|
||||
// And demonstrate that we can position to the full width, too.
|
||||
newfield = go3270.Field{Row: i, Col: cols - 5, Content: "<**>"}
|
||||
screen = append(screen, newfield)
|
||||
}
|
||||
|
||||
// And an input field on the last row, to make sure field buffer address
|
||||
// decoding works on larger screens.
|
||||
newfield := go3270.Field{Row: rows - 1, Col: 0,
|
||||
Content: "Enter data here:", Color: go3270.Pink, Intense: true}
|
||||
screen = append(screen, newfield)
|
||||
newfield = go3270.Field{Row: rows - 1, Col: 17, Name: "inputdata",
|
||||
Write: true}
|
||||
screen = append(screen, newfield)
|
||||
newfield = go3270.Field{Row: rows - 1, Col: cols - 1} // "stop" field
|
||||
screen = append(screen, newfield)
|
||||
|
||||
fieldValues := map[string]string{
|
||||
"termtype": termtype,
|
||||
"rows": strconv.Itoa(rows),
|
||||
"cols": strconv.Itoa(cols),
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
fieldValues["errormsg"] = fmt.Sprintf("You said: %s", data.(string))
|
||||
}
|
||||
|
||||
resp, err := go3270.HandleScreenAlt(
|
||||
screen, // the screen to display
|
||||
nil, // (no) rules to enforce
|
||||
fieldValues, // pre-populated values in fields
|
||||
[]go3270.AID{ // keys we accept -- validating
|
||||
go3270.AIDEnter,
|
||||
},
|
||||
[]go3270.AID{ // keys we accept -- non-validating
|
||||
go3270.AIDPF1,
|
||||
go3270.AIDPF3,
|
||||
},
|
||||
"errormsg", // name of field to put error messages in
|
||||
rows-1, 18, // cursor coordinates
|
||||
conn, // network connection
|
||||
devinfo, // device info for alternate screen size support
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch resp.AID {
|
||||
case go3270.AIDEnter:
|
||||
// Re-run current transaction, echoing back input
|
||||
return bigscreen, resp.Values["inputdata"], err
|
||||
case go3270.AIDPF1:
|
||||
// Go to default screen size transaction
|
||||
return normalscreen, nil, nil
|
||||
case go3270.AIDPF3:
|
||||
// Exit
|
||||
return nil, nil, nil
|
||||
default:
|
||||
// re-run current transaction
|
||||
return bigscreen, nil, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file is part of https://github.com/racingmars/go3270/
|
||||
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||
// LICENSE in the project root for license information.
|
||||
|
||||
// Example 5 demonstrates support for larger-than-default alternate screen
|
||||
// sizes in terminals that are larger than 24x80.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/racingmars/go3270"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ln, err := net.Listen("tcp", ":3270")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("LISTENING ON PORT 3270 FOR CONNECTIONS")
|
||||
fmt.Println("Press Ctrl-C to end server.")
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handle is the handler for individual user connections.
|
||||
func handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// Always begin new connection by negotiating the telnet options
|
||||
devinfo, err := go3270.NegotiateTelnet(conn)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = go3270.RunTransactions(conn, devinfo, bigscreen, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// This file is part of https://github.com/racingmars/go3270/
|
||||
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
|
||||
// LICENSE in the project root for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/racingmars/go3270"
|
||||
)
|
||||
|
||||
var normallayout = go3270.Screen{
|
||||
{Row: 0, Col: 28, Intense: true, Content: "3270 Screen Size Example"},
|
||||
|
||||
{Row: 2, Col: 0,
|
||||
Content: "This screen is using the default size that all terminals support, 24x80."},
|
||||
{Row: 3, Col: 0,
|
||||
Content: "But I know the following information about your particular terminal:"},
|
||||
|
||||
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
|
||||
{Row: 4, Col: 21, Name: "termtype", Intense: true},
|
||||
|
||||
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
|
||||
{Row: 5, Col: 21, Name: "rows", Intense: true},
|
||||
{Row: 5, Col: 28, Content: "(but currently using 24)"},
|
||||
|
||||
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
|
||||
{Row: 6, Col: 21, Name: "cols", Intense: true},
|
||||
{Row: 6, Col: 28, Content: "(but currently using 80)"},
|
||||
|
||||
{Row: 8, Col: 0, Content: "To visit a large sized screen, press"},
|
||||
{Row: 8, Col: 37, Content: "PF1", Color: go3270.Yellow, Intense: true},
|
||||
|
||||
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
|
||||
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
|
||||
|
||||
// a blank field for error messages
|
||||
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
|
||||
}
|
||||
|
||||
func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
|
||||
go3270.Tx, any, error) {
|
||||
|
||||
rows, cols := devinfo.AltDimensions()
|
||||
termtype := devinfo.TerminalType()
|
||||
|
||||
// Make a local copy of the screen definition that we can append lines to.
|
||||
screen := make(go3270.Screen, len(normallayout))
|
||||
copy(screen, normallayout)
|
||||
|
||||
// We'll start writing "data lines" at row 13 up to 24
|
||||
for i := 13; i < 24; i++ {
|
||||
newfield := go3270.Field{Row: i, Col: 0,
|
||||
Content: fmt.Sprintf("This is data row %d.", i-12)}
|
||||
if i == 23 {
|
||||
newfield.Content += " (The last.)"
|
||||
}
|
||||
screen = append(screen, newfield)
|
||||
|
||||
newfield = go3270.Field{Row: i, Col: 80 - 5, Content: "<**>"}
|
||||
screen = append(screen, newfield)
|
||||
}
|
||||
|
||||
fieldValues := map[string]string{
|
||||
"termtype": termtype,
|
||||
"rows": strconv.Itoa(rows),
|
||||
"cols": strconv.Itoa(cols),
|
||||
}
|
||||
|
||||
// We can call the old HandleScreen(), or we could have used the new
|
||||
// HandleScreenAlt() and provided a nil DevInfo.
|
||||
resp, err := go3270.HandleScreen(
|
||||
screen, // the screen to display
|
||||
nil, // (no) rules to enforce
|
||||
fieldValues, // pre-populated values in fields
|
||||
nil, // keys we accept -- validating
|
||||
[]go3270.AID{ // keys we accept -- non-validating
|
||||
go3270.AIDPF1,
|
||||
go3270.AIDPF3,
|
||||
},
|
||||
"errormsg", // name of field to put error messages in
|
||||
1, 1, // cursor coordinates
|
||||
conn, // network connection
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch resp.AID {
|
||||
case go3270.AIDPF1:
|
||||
// Go to big screen size transaction
|
||||
return bigscreen, nil, nil
|
||||
case go3270.AIDPF3:
|
||||
// Exit
|
||||
return nil, nil, nil
|
||||
default:
|
||||
// re-run current transaction
|
||||
return normalscreen, nil, nil
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,23 @@ type FieldRules struct {
|
||||
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
|
||||
// other cases, HandleScreen will re-present the screen to the user again,
|
||||
// possibly with an error message set in the errorField field.
|
||||
//
|
||||
// For alternate screen support (larger than 24x80), use HandleScreenAlt().
|
||||
func HandleScreen(screen Screen, rules Rules, values map[string]string,
|
||||
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
|
||||
conn net.Conn) (Response, error) {
|
||||
return HandleScreenAlt(screen, rules, values, pfkeys, exitkeys, errorField,
|
||||
crow, ccol, conn, nil)
|
||||
}
|
||||
|
||||
// HandleScreenAlt is identical to HandleScreen, but writes to the "alternate"
|
||||
// screen size provided by dev. To write a non-24-by-80 screen, use this
|
||||
// HandleScreenAlt function with a non-nil dev. If dev is nil, the behavior is
|
||||
// identical to HandleScreen, which is limited to 24x80 and will set larger
|
||||
// terminals to the default 24x80 mode.
|
||||
func HandleScreenAlt(screen Screen, rules Rules, values map[string]string,
|
||||
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
|
||||
conn net.Conn, dev DevInfo) (Response, error) {
|
||||
|
||||
// Save the original field values for any named fields to support
|
||||
// the MustChange rule. Also build a map of named fields.
|
||||
@@ -128,7 +142,7 @@ mainloop:
|
||||
}
|
||||
|
||||
resp, err := ShowScreenOpts(screen, myValues, conn,
|
||||
ScreenOpts{CursorRow: crow, CursorCol: ccol})
|
||||
ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev})
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
+21
-23
@@ -6,9 +6,7 @@ package go3270
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Response encapsulates data received from a 3270 client in response to the
|
||||
@@ -61,9 +59,11 @@ const (
|
||||
AIDPA2 AID = 0x6E
|
||||
AIDPA3 AID = 0x6B
|
||||
AIDClear AID = 0x6D
|
||||
|
||||
aidQueryResponse AID = 0x88
|
||||
)
|
||||
|
||||
func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
||||
func readResponse(c net.Conn, fm fieldmap, dev DevInfo) (Response, error) {
|
||||
var r Response
|
||||
aid, err := readAID(c)
|
||||
if err != nil {
|
||||
@@ -79,7 +79,12 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
row, col, _, err := readPosition(c)
|
||||
cols := 80
|
||||
if dev != nil {
|
||||
_, cols = dev.altDimensions()
|
||||
}
|
||||
|
||||
row, col, _, err := readPosition(c, cols)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
@@ -87,7 +92,7 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
|
||||
r.Row = row
|
||||
|
||||
var fieldValues map[string]string
|
||||
if fieldValues, err = readFields(c, fm); err != nil {
|
||||
if fieldValues, err = readFields(c, fm, cols); err != nil {
|
||||
return r, err
|
||||
}
|
||||
|
||||
@@ -114,7 +119,7 @@ func readAID(c net.Conn) (AID, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func readPosition(c net.Conn) (row, col, addr int, err error) {
|
||||
func readPosition(c net.Conn, cols int) (row, col, addr int, err error) {
|
||||
raw := make([]byte, 2)
|
||||
|
||||
// Read two bytes
|
||||
@@ -128,8 +133,8 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
|
||||
|
||||
// Decode the raw position
|
||||
addr = decodeBufAddr([2]byte{raw[0], raw[1]})
|
||||
col = addr % 80
|
||||
row = (addr - col) / 80
|
||||
col = addr % cols
|
||||
row = (addr - col) / cols
|
||||
|
||||
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
|
||||
addr)
|
||||
@@ -137,7 +142,7 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
|
||||
return row, col, addr, nil
|
||||
}
|
||||
|
||||
func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
|
||||
func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) {
|
||||
var infield bool
|
||||
var fieldpos int
|
||||
var fieldval bytes.Buffer
|
||||
@@ -174,7 +179,7 @@ func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
|
||||
fieldval = bytes.Buffer{}
|
||||
fieldpos = 0
|
||||
|
||||
if _, _, fieldpos, err = readPosition(c); err != nil {
|
||||
if _, _, fieldpos, err = readPosition(c, cols); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
@@ -203,20 +208,13 @@ func handleField(addr int, value []byte, fm fieldmap, values map[string]string)
|
||||
}
|
||||
|
||||
// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the
|
||||
// integer value of the address (i.e. 0-1919)
|
||||
// integer value of the address.
|
||||
func decodeBufAddr(raw [2]byte) int {
|
||||
if decodes[raw[0]] > 254 {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
|
||||
raw[0], raw[1])
|
||||
}
|
||||
if decodes[raw[1]] > 254 {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
|
||||
raw[0], raw[1])
|
||||
// 16-bit addressing
|
||||
if raw[0]&0xc0 == 0 {
|
||||
return int(raw[0])<<8 + int(raw[1])
|
||||
}
|
||||
|
||||
hi := decodes[raw[0]] << 6
|
||||
lo := decodes[raw[1]]
|
||||
return hi | lo
|
||||
// 12-bit addressing
|
||||
return int(raw[0]&0x3f)<<6 + int(raw[1]&0x3f)
|
||||
}
|
||||
@@ -98,6 +98,19 @@ type Screen []Field
|
||||
// ScreenOpts are the options that callers may set when sending a screen
|
||||
// to the 3270 client.
|
||||
type ScreenOpts struct {
|
||||
// If AltScreen is non-nil, the screen will be written to the "alternate"
|
||||
// screen size, which is the non-default (24x80) screen dimensions that
|
||||
// the terminal supports (although for many terminals, the alternate
|
||||
// screen is still just 24x80). If AltScreen is nil, the default (24x80)
|
||||
// mode will be used. Never switch between AltScreen and normal screen
|
||||
// (e.g. AltScreen = nil) unless NoClear is false. (That is, switching
|
||||
// between default and alternate screen size or back requires a screen
|
||||
// clear at the same time.) When AltScreen is nil, field positions in the
|
||||
// screen must be within the 24x80 screen (so rows 0-23 and cols 0-79),
|
||||
// when AltScreen is present, the field positions must be within the
|
||||
// dimensions of the DevInfo.AltDimensions() values.
|
||||
AltScreen DevInfo
|
||||
|
||||
// NoResponse will draw the screen and immediately return, without
|
||||
// waiting for any input data from the remote client.
|
||||
NoResponse bool
|
||||
@@ -107,12 +120,16 @@ type ScreenOpts struct {
|
||||
// the current screen.
|
||||
NoClear bool
|
||||
|
||||
// CursorRow sets the row (0-indexed) to position the cursor after
|
||||
// sending the screen, when NoClear is false. Maximum value is 23.
|
||||
// CursorRow sets the row (0-indexed) to position the cursor after sending
|
||||
// the screen, when NoClear is false. When AltScreen is nil, maximum value
|
||||
// is 23; otherwise, maximum is 1 less than the number of rows in
|
||||
// AltScreen.
|
||||
CursorRow int
|
||||
|
||||
// CursorCol sets the column (0-indexed) to position the cursor after
|
||||
// sending the screen, when NoClear is false. Maximum value is 79.
|
||||
// sending the screen, when NoClear is false. When AltScreen is nil,
|
||||
// maximum value is 79; otherwise, maximum is 1 less than the number of
|
||||
// columns in AltScreen.
|
||||
CursorCol int
|
||||
|
||||
// PostSendCallback is a function that, if non-nil, will be called after
|
||||
@@ -159,13 +176,15 @@ type fieldmap map[int]string
|
||||
// invalid. That is to say, while waiting for a response, don't perform other
|
||||
// actions from another thread that could layout the user input fields
|
||||
// differently.
|
||||
func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
|
||||
|
||||
func ShowScreenOpts(screen Screen,
|
||||
values map[string]string, conn net.Conn,
|
||||
opts ScreenOpts) (Response, error) {
|
||||
|
||||
var resp Response
|
||||
|
||||
fm, err := showScreenInternal(screen, values, opts.CursorRow,
|
||||
opts.CursorCol, conn, !opts.NoClear)
|
||||
opts.CursorCol, conn, !opts.NoClear, opts.AltScreen)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
@@ -178,7 +197,7 @@ func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
|
||||
}
|
||||
|
||||
if !opts.NoResponse {
|
||||
resp, err = readResponse(conn, fm)
|
||||
resp, err = readResponse(conn, fm, opts.AltScreen)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
@@ -216,13 +235,22 @@ func ShowScreenNoResponse(screen Screen, values map[string]string,
|
||||
}
|
||||
|
||||
func showScreenInternal(screen Screen, values map[string]string,
|
||||
crow, ccol int, conn net.Conn, clear bool) (fieldmap, error) {
|
||||
crow, ccol int, conn net.Conn, clear bool, dev DevInfo) (fieldmap, error) {
|
||||
|
||||
rows, cols := 24, 80
|
||||
if dev != nil {
|
||||
rows, cols = dev.altDimensions()
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
var fm = make(fieldmap) // field buffer positions -> name
|
||||
|
||||
if clear {
|
||||
b.WriteByte(0xf5) // Erase/Write to terminal
|
||||
if !(rows == 24 && cols == 80) {
|
||||
b.WriteByte(0x7e) // Erase/Write Alternate to terminal
|
||||
} else {
|
||||
b.WriteByte(0xf5) // Erase/Write to terminal
|
||||
}
|
||||
} else {
|
||||
b.WriteByte(0xf1) // Write to terminal
|
||||
}
|
||||
@@ -237,12 +265,12 @@ func showScreenInternal(screen Screen, values map[string]string,
|
||||
|
||||
// Build the commands for each field on the screen
|
||||
for _, fld := range screen {
|
||||
if fld.Row < 0 || fld.Row > 23 || fld.Col < 0 || fld.Col > 79 {
|
||||
if fld.Row < 0 || fld.Row > rows-1 || fld.Col < 0 || fld.Col > cols-1 {
|
||||
// Invalid field position
|
||||
continue
|
||||
}
|
||||
|
||||
b.Write(sba(fld.Row, fld.Col))
|
||||
b.Write(sba(fld.Row, fld.Col, cols))
|
||||
b.Write(buildField(fld))
|
||||
|
||||
// Use fld.Content, unless the field is named and appears in the
|
||||
@@ -262,7 +290,7 @@ func showScreenInternal(screen Screen, values map[string]string,
|
||||
// because we get the position of the field's first input position,
|
||||
// not the position of the field attribute byte).
|
||||
if fld.Write {
|
||||
bufaddr := fld.Row*80 + fld.Col
|
||||
bufaddr := fld.Row*cols + fld.Col
|
||||
fm[bufaddr+1] = fld.Name
|
||||
}
|
||||
}
|
||||
@@ -271,13 +299,13 @@ func showScreenInternal(screen Screen, values map[string]string,
|
||||
// caller-provided coordinates.
|
||||
if clear {
|
||||
// Set cursor position. Correct out-of-bounds values to 0.
|
||||
if crow < 0 || crow > 23 {
|
||||
if crow < 0 || crow > rows-1 {
|
||||
crow = 0
|
||||
}
|
||||
if ccol < 0 || ccol > 79 {
|
||||
if ccol < 0 || ccol > cols-1 {
|
||||
ccol = 0
|
||||
}
|
||||
b.Write(ic(crow, ccol))
|
||||
b.Write(ic(crow, ccol, cols))
|
||||
}
|
||||
|
||||
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
|
||||
@@ -292,10 +320,10 @@ func showScreenInternal(screen Screen, values map[string]string,
|
||||
}
|
||||
|
||||
// sba is the "set buffer address" 3270 command.
|
||||
func sba(row, col int) []byte {
|
||||
func sba(row, col, cols int) []byte {
|
||||
result := make([]byte, 1, 3)
|
||||
result[0] = 0x11 // SBA
|
||||
result = append(result, getpos(row, col)...)
|
||||
result = append(result, getpos(row, col, cols)...)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -372,20 +400,37 @@ func sfAttribute(write, intense, hidden, skip, numeric bool) byte {
|
||||
|
||||
// ic is the "insert cursor" 3270 command. This function will include the
|
||||
// appropriate SBA command.
|
||||
func ic(row, col int) []byte {
|
||||
func ic(row, col, cols int) []byte {
|
||||
result := make([]byte, 0, 3)
|
||||
result = append(result, sba(row, col)...)
|
||||
result = append(result, sba(row, col, cols)...)
|
||||
result = append(result, 0x13) // IC
|
||||
return result
|
||||
}
|
||||
|
||||
// getpos translates row and col to buffer address control characters.
|
||||
func getpos(row, col int) []byte {
|
||||
result := make([]byte, 2)
|
||||
address := row*80 + col
|
||||
hi := (address & 0xfc0) >> 6
|
||||
lo := address & 0x3f
|
||||
result[0] = codes[hi]
|
||||
result[1] = codes[lo]
|
||||
return result
|
||||
func getpos(row, col, cols int) []byte {
|
||||
address := row*cols + col
|
||||
|
||||
// Use 12-bit addressing if the buffer address fits in 12 bits
|
||||
if address < 1<<12 {
|
||||
hi := (address & 0xfc0) >> 6
|
||||
lo := address & 0x3f
|
||||
return []byte{codes[hi], codes[lo]}
|
||||
}
|
||||
|
||||
// Otherwise, use 14-bit addressing. The library limits terminal size to
|
||||
// fit within 14-bit addressing, because 16-bit addressing would require
|
||||
// us to track state that the current API design doesn't lend itself to.
|
||||
// Someday, perhaps in a v2 library version, we'll support absurdly large
|
||||
// terminal sizes. But for now, 14 bits is as big as we can go.
|
||||
hi := (address & 0x3f00) >> 8
|
||||
lo := address & 0xff
|
||||
|
||||
// It's possible the low byte is 0xff, in which case we need to telnet-
|
||||
// escape it.
|
||||
if lo == 0xff {
|
||||
return []byte{byte(hi), 0xff, byte(lo)}
|
||||
}
|
||||
|
||||
return []byte{byte(hi), byte(lo)}
|
||||
}
|
||||
@@ -1,48 +1,337 @@
|
||||
// This file is part of https://github.com/racingmars/go3270/
|
||||
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
|
||||
// LICENSE in the project root for license information.
|
||||
// Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
package go3270
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DevInfo provides information about the terminal that is connected.
|
||||
//
|
||||
// 3270 terminals operate at a default screen size of 24 rows that are 80
|
||||
// columns wide. The normal "Write/Erase" datastream command always writes to
|
||||
// the default 24x80 buffer. But some terminals support more rows and/or
|
||||
// columns, and the alternate sized buffer may be written to with the
|
||||
// "Write/Erase Alternate" command.
|
||||
type DevInfo interface {
|
||||
// AltDimensions returns the number or rows and columns on the alternate
|
||||
// screen size.
|
||||
AltDimensions() (rows, cols int)
|
||||
|
||||
// TerminalType reports the terminal-provided identification string. All
|
||||
// modern tn3270 clients will report one of the IBM-3278 models (-2, -3,
|
||||
// -4, or -5), or IBM-DYNAMIC if the alternate screen size isn't one of
|
||||
// the fixed sizes of the 3278 models. This string is purely
|
||||
// informational; the actual size of the alternate screen is available
|
||||
// from AltDimensions().
|
||||
TerminalType() string
|
||||
|
||||
// Private version of AltDimensions() so callers can't fake us out; only
|
||||
// real implementations returned by NegotiateTelnet() will work.
|
||||
altDimensions() (rows, cols int)
|
||||
}
|
||||
|
||||
const (
|
||||
binary = 0
|
||||
send = 1
|
||||
se = 240 // f0
|
||||
sb = 250 // fa
|
||||
will = 251 // fb
|
||||
wont = 252 // fc
|
||||
do = 253 // fd
|
||||
dont = 254 // fe
|
||||
iac = 255 // ff
|
||||
terminalType = 24 // 18
|
||||
eoroption = 25 // 19
|
||||
eor = 239 // f1
|
||||
se = 240 // 0xf0
|
||||
sb = 250 // 0xfa
|
||||
will = 251 // 0xfb
|
||||
wont = 252 // 0xfc
|
||||
do = 253 // 0xfd
|
||||
dont = 254 // 0xfe
|
||||
iac = 255 // 0xff
|
||||
|
||||
// Options
|
||||
|
||||
binaryOption = 0
|
||||
|
||||
eorOption = 25 // 0x19
|
||||
eor = 239 // 0xf1
|
||||
|
||||
terminalType = 24 // 0x18
|
||||
terminalTypeIs = 0
|
||||
terminalTypeSend = 1
|
||||
)
|
||||
|
||||
// NegotiateTelnet will naively (e.g. not checking client responses) negotiate
|
||||
// the options necessary for tn3270 on a new telnet connection, conn.
|
||||
func NegotiateTelnet(conn net.Conn) error {
|
||||
conn.Write([]byte{iac, do, terminalType})
|
||||
conn.Write([]byte{iac, sb, terminalType, send, iac, se})
|
||||
conn.Write([]byte{iac, do, eoroption})
|
||||
conn.Write([]byte{iac, do, binary})
|
||||
conn.Write([]byte{iac, will, eoroption, iac, will, binary})
|
||||
flushConnection(conn, time.Second*5)
|
||||
// ErrNo3270 indicates that the telnet client did not respond properly to the
|
||||
// options negotiation that are expected for a tn3270 client.
|
||||
var ErrNo3270 = errors.New("couldn't negotiate telnet options for tn3270")
|
||||
|
||||
// ErrTelnetError indicates an unexpected response was encountered in the
|
||||
// telnet protocol.
|
||||
var ErrTelnetError = errors.New("telnet or 3270 protocol error")
|
||||
|
||||
// ErrUnknownTerminal indicates the client did not identify itself as an
|
||||
// IBM-3277, 3278, 3279, or IBM-DYNAMIC model. All modern tn3270 clients
|
||||
// should report as IBM-3278 models or IBM-DYNAMIC.
|
||||
var ErrUnknownTerminal = errors.New("unknown terminal type")
|
||||
|
||||
var errOptionRejected = errors.New("option rejected")
|
||||
|
||||
// NegotiateTelnet will negotiate the options necessary for tn3270 on a new
|
||||
// telnet connection, conn.
|
||||
func NegotiateTelnet(conn net.Conn) (DevInfo, error) {
|
||||
|
||||
// Enable terminal type option
|
||||
if _, err := conn.Write([]byte{iac, do, terminalType}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := checkOptionResponse(conn, terminalType, do)
|
||||
if err == errOptionRejected || err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Switch to the first available terminal type
|
||||
conn.Write([]byte{iac, sb, terminalType, terminalTypeSend, iac, se})
|
||||
devtype, err := getTerminalType(conn)
|
||||
if err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Request end of record mode
|
||||
conn.Write([]byte{iac, do, eorOption})
|
||||
err = checkOptionResponse(conn, eorOption, do)
|
||||
if err == errOptionRejected || err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Request binary mode
|
||||
conn.Write([]byte{iac, do, binaryOption})
|
||||
err = checkOptionResponse(conn, binaryOption, do)
|
||||
if err == errOptionRejected || err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enter end of record mode
|
||||
conn.Write([]byte{iac, will, eorOption})
|
||||
err = checkOptionResponse(conn, eorOption, will)
|
||||
if err == errOptionRejected || err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enter binary mode
|
||||
conn.Write([]byte{iac, will, binaryOption})
|
||||
err = checkOptionResponse(conn, binaryOption, will)
|
||||
if err == errOptionRejected || err == ErrTelnetError {
|
||||
return nil, ErrNo3270
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
devinfo, err := makeDeviceInfo(conn, devtype)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return devinfo, nil
|
||||
}
|
||||
|
||||
// checkOptionResponse will check for the client's "will/wont" (if mode is do)
|
||||
// or "do/dont" (if mode is will) response. mode is the option command the
|
||||
// server just sent, and option is the option code to check for.
|
||||
func checkOptionResponse(conn net.Conn, option, mode byte) error {
|
||||
var buf [3]byte
|
||||
|
||||
var expectedYes, expectedNo byte
|
||||
switch mode {
|
||||
case do:
|
||||
expectedYes = will
|
||||
expectedNo = wont
|
||||
case will:
|
||||
expectedYes = do
|
||||
expectedNo = dont
|
||||
default:
|
||||
return ErrTelnetError
|
||||
}
|
||||
|
||||
n, err := conn.Read(buf[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 3 || buf[0] != iac {
|
||||
return ErrTelnetError
|
||||
}
|
||||
if buf[1] == expectedNo {
|
||||
// Was the correct option rejected?
|
||||
if buf[2] != option {
|
||||
return ErrTelnetError
|
||||
}
|
||||
return errOptionRejected
|
||||
}
|
||||
if buf[1] != expectedYes {
|
||||
return ErrTelnetError
|
||||
}
|
||||
|
||||
// We have "will" now. But for the right option?
|
||||
if buf[2] != option {
|
||||
return ErrTelnetError
|
||||
}
|
||||
|
||||
// All good, client accepted the option we requested.
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTerminalType reads the response to a "send terminal type" option
|
||||
// subfield command.
|
||||
func getTerminalType(conn net.Conn) (string, error) {
|
||||
var buf [100]byte
|
||||
var termtype string
|
||||
|
||||
n, err := conn.Read(buf[:])
|
||||
if err != nil {
|
||||
return termtype, err
|
||||
}
|
||||
|
||||
// At a minimum, with a one-character terminal type name, we expect
|
||||
// 7 bytes
|
||||
if n < 7 {
|
||||
return termtype, ErrTelnetError
|
||||
}
|
||||
|
||||
// We'll check the expected control bytes all in one go...
|
||||
if buf[0] != iac || buf[1] != sb || buf[2] != terminalType ||
|
||||
buf[3] != terminalTypeIs || buf[n-2] != iac || buf[n-1] != se {
|
||||
return termtype, ErrTelnetError
|
||||
}
|
||||
|
||||
// Everything looks good. The terminal type is an ASCII string between all
|
||||
// the control/command bytes.
|
||||
return string(buf[4 : n-2]), nil
|
||||
}
|
||||
|
||||
func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) {
|
||||
// Known fixed size device types. All modern tn3270 clients should
|
||||
// report as 3278, but we'll also include 3277 and 3279 just in case.
|
||||
switch termtype {
|
||||
case "IBM-3277-2", "IBM-3277-2-E", "IBM-3278-2", "IBM-3278-2-E",
|
||||
"IBM-3279-2", "IBM-3279-2-E":
|
||||
return &deviceInfo{24, 80, termtype}, nil
|
||||
case "IBM-3278-3", "IBM-3278-3-E", "IBM-3279-3", "IBM-3279-3-E":
|
||||
return &deviceInfo{32, 80, termtype}, nil
|
||||
case "IBM-3278-4", "IBM-3278-4-E":
|
||||
return &deviceInfo{43, 80, termtype}, nil
|
||||
case "IBM-3278-5", "IBM-3278-5-E":
|
||||
return &deviceInfo{27, 132, termtype}, nil
|
||||
}
|
||||
|
||||
// If it's not a fixed-size type, it should be IBM-DYNAMIC. If it isn't,
|
||||
// we don't know how to deal with it.
|
||||
if termtype != "IBM-DYNAMIC" {
|
||||
return nil, ErrUnknownTerminal
|
||||
}
|
||||
|
||||
// For IBM-DYNAMIC, we need to discover the alternate screen size with
|
||||
// a structured field query.
|
||||
|
||||
// First, we perform an ERASE / WRITE ALTERNATE to clear the screen
|
||||
// and put it in alternate screen mode. (EWA, reset WCC, telnet EOR)
|
||||
if _, err := conn.Write([]byte{0x7e, 0xc3, 0xff, 0xef}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we need to send the Write Structured Field command (0xf3) with the
|
||||
// "Read Partition - Query" structured field. Note that we're
|
||||
// telnet-escaping the 0xff in the data, but the subfield length is the
|
||||
// *unescaped* length (7).
|
||||
if _, err := conn.Write([]byte{0xf3, 0, 7, 0x01, 0xff, 0xff, 0x02,
|
||||
0xff, 0xef}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var aid [1]byte
|
||||
n, err := conn.Read(aid[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 1 || aid[0] != byte(aidQueryResponse) {
|
||||
return nil, ErrTelnetError
|
||||
}
|
||||
|
||||
var rows, cols int
|
||||
// There are an arbitrary number of query reply structured fields. We
|
||||
// are only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field,
|
||||
// so we'll just consume any others. Consume all data until the EOR is
|
||||
// received.
|
||||
for {
|
||||
// Two bytes are big-endian length.
|
||||
buf, err := telnetReadN(conn, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if buf == nil {
|
||||
// EOR. We're out of fields.
|
||||
break
|
||||
}
|
||||
|
||||
var l int = int(buf[0])<<8 + int(buf[1])
|
||||
|
||||
// Field length includes the 2 length bytes
|
||||
buf, err = telnetReadN(conn, l-2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if buf == nil {
|
||||
return nil, ErrTelnetError
|
||||
}
|
||||
|
||||
// Note that because length isn't at the beginning, offsets in buf
|
||||
// are 2 less than in the 3270 datastream documentation.
|
||||
|
||||
if !(buf[0] == 0x81 && buf[1] == 0x81) {
|
||||
// Not 'Usable Area' query reply
|
||||
continue
|
||||
}
|
||||
|
||||
// A valid Usable Area reply will always include at least 18 (20 with
|
||||
// length) bytes.
|
||||
if l < 18 {
|
||||
return nil, ErrTelnetError
|
||||
}
|
||||
|
||||
// big-endian two byte values
|
||||
cols = int(buf[4])<<8 + int(buf[5])
|
||||
rows = int(buf[6])<<8 + int(buf[7])
|
||||
}
|
||||
|
||||
if rows == 0 || cols == 0 {
|
||||
// We got an IBM-DYNAMIC device type, but it didn't include a
|
||||
// Usable Area query response.
|
||||
return nil, ErrUnknownTerminal
|
||||
}
|
||||
|
||||
// We support 12- and 14-bit addressing. Using 16-bit addressing would
|
||||
// require a mode change and the current API design doesn't support
|
||||
// tracking the state necessary for that.
|
||||
//
|
||||
// We'll limit the reported screen size to what fits in 14-bit addressing
|
||||
// by removing rows if necessary.
|
||||
for rows*cols >= 1<<14 {
|
||||
rows--
|
||||
}
|
||||
|
||||
return &deviceInfo{rows, cols, termtype}, nil
|
||||
}
|
||||
|
||||
// UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
|
||||
// to restore the telnet options state to what it was before NegotiateTelnet()
|
||||
// was called.
|
||||
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
|
||||
conn.Write([]byte{iac, wont, eoroption, iac, wont, binary})
|
||||
conn.Write([]byte{iac, dont, binary})
|
||||
conn.Write([]byte{iac, dont, eoroption})
|
||||
conn.Write([]byte{iac, wont, eorOption, iac, wont, binaryOption})
|
||||
conn.Write([]byte{iac, dont, binaryOption})
|
||||
conn.Write([]byte{iac, dont, eorOption})
|
||||
conn.Write([]byte{iac, dont, terminalType})
|
||||
flushConnection(conn, timeout)
|
||||
return nil
|
||||
@@ -138,3 +427,48 @@ func telnetRead(c net.Conn, passEOR bool) (b byte, valid, isEor bool, err error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// telnetReadN reads n unescaped, valid, non-EOR characters. The returned byte
|
||||
// slice will always be length n (see special case below, though), unless
|
||||
// error is non-nil, in which case the byte slice will be nil. Invalid or
|
||||
// early EOR will return ErrTelnetError.
|
||||
//
|
||||
// AS A SPECIAL CASE, if the first byte read is EOR, then the returned byte
|
||||
// slice AND error will be nil.
|
||||
func telnetReadN(conn net.Conn, n int) ([]byte, error) {
|
||||
buf := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
b, valid, isEor, err := telnetRead(conn, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i == 0 && isEor {
|
||||
// If we're still on the first byte and it's EOR, return a
|
||||
// non-error nil value.
|
||||
return nil, nil
|
||||
}
|
||||
if !valid || isEor {
|
||||
return nil, ErrTelnetError
|
||||
}
|
||||
buf[i] = b
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
type deviceInfo struct {
|
||||
rows, cols int
|
||||
termtype string
|
||||
}
|
||||
|
||||
func (d *deviceInfo) AltDimensions() (rows, cols int) {
|
||||
return d.rows, d.cols
|
||||
}
|
||||
|
||||
func (d *deviceInfo) TerminalType() string {
|
||||
return d.termtype
|
||||
}
|
||||
|
||||
func (d *deviceInfo) altDimensions() (rows, cols int) {
|
||||
return d.rows, d.cols
|
||||
}
|
||||
+23
-10
@@ -7,30 +7,43 @@ package go3270
|
||||
import "net"
|
||||
|
||||
// Tx is a function that serves as one transaction in a go3270 application.
|
||||
// The Tx function is called with the network connection to the client, and a
|
||||
// "data" value provided by the previous transaction. Tx functions return the
|
||||
// next transaction to run (or nil to indicate the RunTransactions() function
|
||||
// should terminate), the data to pass into the next transaction, and any
|
||||
// error. If the error is non-nil, the RunTransactions() function will
|
||||
// terminate and return the err. A non-nil error is _not_ passed between
|
||||
// transactions, it terminates transaction processing.
|
||||
type Tx func(conn net.Conn, data any) (next Tx, newdata any, err error)
|
||||
// The Tx function is called with the network connection to the client, the
|
||||
// DevInfo for use with alternate screen writes, and a "data" value provided
|
||||
// by the previous transaction. Tx functions return the next transaction to
|
||||
// run (or nil to indicate the RunTransactions() function should terminate),
|
||||
// the data to pass into the next transaction, and any error. If the error is
|
||||
// non-nil, the RunTransactions() function will terminate and return the err.
|
||||
// A non-nil error is _not_ passed between transactions, it terminates
|
||||
// transaction processing.
|
||||
type Tx func(conn net.Conn, dev DevInfo, data any) (
|
||||
next Tx, newdata any, err error)
|
||||
|
||||
// RunTransactions begins running transaction functions, starting with the
|
||||
// initial transaction, until a transaction eventually returns nil for the
|
||||
// next transaction, or until a transaction function returns a non-nil error
|
||||
// value. data (which may be nil, if the initial transaction does not require
|
||||
// data) is passed in as the data to the initial transaction.
|
||||
func RunTransactions(conn net.Conn, initial Tx, data any) error {
|
||||
//
|
||||
// dev is the DevInfo of the connected client, as obtained from
|
||||
// NegotiateTelnet(). It is safe to pass a nil DevInfo, in which case all
|
||||
// transactions will only be able to operate with the default 24x80 screen
|
||||
// size.
|
||||
func RunTransactions(conn net.Conn, dev DevInfo, initial Tx,
|
||||
data any) error {
|
||||
|
||||
var next Tx
|
||||
var err error
|
||||
|
||||
next = initial
|
||||
|
||||
if dev == nil {
|
||||
dev = &deviceInfo{rows: 24, cols: 80, termtype: "DEFAULT"}
|
||||
}
|
||||
|
||||
// We run transactions until there isn't a next transaction to run, or
|
||||
// an error.
|
||||
for {
|
||||
next, data, err = next(conn, data)
|
||||
next, data, err = next(conn, dev, data)
|
||||
if err != nil {
|
||||
// Error means we bail out and return the error to the caller.
|
||||
return err
|
||||
|
||||
@@ -23,8 +23,8 @@ func debugf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(Debug, format, a...)
|
||||
}
|
||||
|
||||
// codes are the 3270 control character I/O codes, pre-computed as provided
|
||||
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm
|
||||
// codes are the 3270 control character I/O codes for 12-bit addressing,
|
||||
// from Figure D-1 of GA23-0059-00. (Figure C-1 in later editions.)
|
||||
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
||||
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
|
||||
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
|
||||
@@ -32,23 +32,6 @@ var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
|
||||
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
|
||||
|
||||
// decodes is the inverse of the above table; -1 is used in invalid positions
|
||||
var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, 42, 43, 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2,
|
||||
3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1,
|
||||
-1, -1, -1, -1}
|
||||
|
||||
// AIDtoString returns a string representation of an AID key name.
|
||||
func AIDtoString(aid AID) string {
|
||||
switch aid {
|
||||
|
||||
Reference in new issue
Block a user