Add RunTransactions() approach to application design

This commit is contained in:
Matthew R. Wilson committed 2025-04-13 22:04:54 -07:00
1 parent 7b5603eb11
commit 78aaf72e84
9 files changed
+804 -24

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
*.exe
example1/example1
example2/example2
example3/example3
example4/example4
+21 -23
View File
@@ -5,7 +5,27 @@ Go 3270 Server Library
This library allows you to write Go servers for tn3270 clients by building 3270 data streams from fields and processing the client's response to receive the attention keys and field values entered by users.
**The library is incomplete, likely buggy, and under heavy development: the interface is UNSTABLE until this notice is removed from this readme and version 1.0 is released.**
**Project status:** This library has been used by a small number of projects, and I believe the overall functionality is sound and relatively bug-free. Feedback is appreciated. At this point, I will try not to make breaking changes to the API, but I have not yet declared the library to be at v1.0, so I make no promises.
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.
**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.
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].
@@ -15,28 +35,6 @@ Everything I know about 3270 data streams I learned from [Tommy Sprinkle's tutor
[rfc854]: https://tools.ietf.org/html/rfc854
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml
Usage
-----
See the example folders for a quick demonstration of using the library. example1 uses the lower-level function ShowScreen(), 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.
Here's [a video introducing the library][introVideo] as well.
[introVideo]: https://www.youtube.com/watch?v=h9XTjup5W5U
Future Enhancements
-------------------
I would like to add:
- ~~Extended field attribute support (e.g. color).~~ **Done**
- Utility functions for easily laying out forms.
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.
License
-------
+96
View File
@@ -0,0 +1,96 @@
// 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.
// This example demonstrates using the RunTransactions() approach to
// structuring a go3270 application.
// This file contains a mock in-memory database for the purposes of our
// example application.
package main
import (
"errors"
"sync"
"time"
)
type User struct {
Username string
Password string
Name string
SignupDate time.Time
}
type DB interface {
GetUser(username string) (User, error)
CreateUser(user User) (User, error)
UpdateUser(user User) (User, error)
}
type dbstate struct {
lock sync.Mutex
users map[string]User
}
func Connect() DB {
return &dbstate{
users: make(map[string]User),
}
}
var ErrUserExists = errors.New("username already exists")
var ErrNotFound = errors.New("record not found")
func (db *dbstate) GetUser(username string) (User, error) {
db.lock.Lock()
defer db.lock.Unlock()
user, ok := db.users[username]
if !ok {
return User{}, ErrNotFound
}
return user, nil
}
func (db *dbstate) CreateUser(user User) (User, error) {
db.lock.Lock()
defer db.lock.Unlock()
if _, ok := db.users[user.Username]; ok {
return User{}, ErrUserExists
}
user.SignupDate = time.Now().UTC().Truncate(time.Second)
// Obviously in a real application you wouldn't store plaintext password,
// but this is a simple example for the purposes of demonstrating go3270,
// not demonstrating how to build a secure application.
db.users[user.Username] = user
return user, nil
}
func (db *dbstate) UpdateUser(user User) (User, error) {
db.lock.Lock()
defer db.lock.Unlock()
olduser, ok := db.users[user.Username]
if !ok {
return User{}, ErrNotFound
}
// Make sure original signup date is maintained
user.SignupDate = olduser.SignupDate
// Carry over existing password if we aren't setting a new password
if user.Password == "" {
user.Password = olduser.Password
}
db.users[user.Username] = user
return user, nil
}
+83
View File
@@ -0,0 +1,83 @@
// 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.
// This example demonstrates using the RunTransactions() approach to
// structuring a go3270 application.
package main
import (
"fmt"
"net"
"sync"
"github.com/racingmars/go3270"
)
type global struct {
usercount int // number of connected users
countlock sync.Mutex
}
// session is the structure which holds global state for a user session of our
// application. The various Transactions will be methods on this struct.
type session struct {
db DB
user User
// We can also share global state between users
g *global
}
func main() {
// "Connect" to our database (for this example, it's just a mock in-memory
// database).
dbconn := Connect()
gblstate := new(global)
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, dbconn, gblstate)
}
}
// handle is the handler for individual user connections.
func handle(conn net.Conn, db DB, gblstate *global) {
defer conn.Close()
// Create the state for this client's connection
state := session{
db: db,
g: gblstate,
}
// Add to the global user counter
gblstate.countlock.Lock()
gblstate.usercount++
gblstate.countlock.Unlock()
// When the session ends, reduce the user count
defer func() {
gblstate.countlock.Lock()
gblstate.usercount--
gblstate.countlock.Unlock()
}()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
err := go3270.RunTransactions(conn, state.login, nil)
if err != nil {
fmt.Println(err)
}
}
+68
View File
@@ -0,0 +1,68 @@
// 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.
// This example demonstrates using the RunTransactions() approach to
// structuring a go3270 application.
// This file contains the help transaction generator.
package main
import (
"net"
"github.com/racingmars/go3270"
)
var helpScreen = go3270.Screen{
{Row: 0, Col: 45, Intense: true, Content: "Online Help"},
{Row: 2, Col: 0,
Content: "This help screen is an example of a transaction that"},
{Row: 3, Col: 0,
Content: "can be used as the next transaction from many other transactions"},
{Row: 4, Col: 0, Content: "and returns to the transaction that called it."},
{Row: 6, Col: 0, Content: "Press"},
{Row: 6, Col: 6, Content: "PF3", Color: go3270.White, Intense: true},
{Row: 6, Col: 10,
Content: "to return to the transaction from which you came."},
// Error message
{Row: 21, Col: 0, Name: "errormsg", Color: go3270.Red, Intense: true},
// Key legend
{Row: 23, Col: 1, Content: "F3=Exit"},
}
// The help transaction is an example of returning a closure over a parameter,
// in this case the desired next transaction. This allows us to use the same
// transaction from multiple other transactions and return to the requested
// transaction when the user leaves the help transaction.
//
// Any data passed to help will be passed back to the transaction it returns
// to.
//
// The help transaction has no need for any global or session state, so this
// 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) {
_, err := go3270.HandleScreen(
helpScreen, // the screen to display
nil, // (no) rules to enforce
nil, // pre-populated values in fields
[]go3270.AID{go3270.AIDPF3}, // keys we accept
nil,
"errormsg", // name of field to put error messages in
23, 79, // cursor coordinates
conn)
if err != nil {
return nil, nil, err
}
// Any accepted key returns to the requested returnTransaction
return returnTransaction, data, nil
}
}
+257
View File
@@ -0,0 +1,257 @@
// 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.
// This example demonstrates using the RunTransactions() approach to
// structuring a go3270 application.
// This file contains the login and user registration transactions.
package main
import (
"net"
"github.com/racingmars/go3270"
)
const loginUsername = "username"
const loginPassword = "password"
const loginPasswordConf = "password confirmation"
const loginName = "name"
const loginErr = "errormsg"
var loginScreen = go3270.Screen{
{Row: 0, Col: 37, Intense: true, Content: "Logon"},
{Row: 2, Col: 0,
Content: "Welcome to the go3270 example application. Please log on."},
// Username
{Row: 4, Col: 0, Content: "Username . . .", Color: go3270.Green},
{Row: 4, Col: 15, Name: loginUsername, Write: true,
Highlighting: go3270.Underscore, Color: go3270.Turquoise},
{Row: 4, Col: 24, Autoskip: true}, // field "stop" character
// Password
{Row: 5, Col: 0, Content: "Password . . .", Color: go3270.Green},
{Row: 5, Col: 15, Name: loginPassword, Write: true, Hidden: true},
{Row: 5, Col: 79}, // field "stop" character
// Registration instructions
{Row: 7, Col: 0, Content: "If you don't yet have an account, press"},
{Row: 7, Col: 40, Content: "PF5", Color: go3270.White, Intense: true},
{Row: 7, Col: 44, Content: "to register a new account."},
// Error message
{Row: 21, Col: 0, Name: loginErr, Color: go3270.Red, Intense: true},
// Key legend
{Row: 23, Col: 1, Content: "F1=Help"},
{Row: 23, Col: 14, Content: "F3=Exit"},
{Row: 23, Col: 27, Content: "F5=Register"},
}
var loginScreenRules = go3270.Rules{
loginUsername: {Validator: go3270.NonBlank},
loginPassword: {Validator: go3270.NonBlank, Reset: true},
}
// 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) {
fieldValues := make(map[string]string)
if data != nil {
if errmsg, ok := data.(string); ok {
fieldValues[loginErr] = errmsg
}
}
resp, err := go3270.HandleScreen(
loginScreen, // the screen to display
loginScreenRules, // the rules to enforce
fieldValues, // pre-populated values in fields
[]go3270.AID{go3270.AIDEnter}, // keys we accept -- validating
[]go3270.AID{ // keys we accept -- non-validating
go3270.AIDPF1,
go3270.AIDPF3,
go3270.AIDPF5,
go3270.AIDClear},
loginErr, // name of field to put error messages in
4, 16, // cursor coordinates
conn)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDClear:
// re-run the transaction with empty values
return sess.login, nil, nil
case go3270.AIDPF1:
// Display help transaction, then return to this transaction
return help(sess.login), nil, nil
case go3270.AIDPF3:
// User wants to quit; return no next transaction
return nil, nil, nil
case go3270.AIDPF5:
// Send user to registration transaction
return sess.newuser, nil, nil
}
// If we didn't get one of the other allowed keys, try to log the user in.
username := resp.Values[loginUsername]
password := resp.Values[loginPassword]
user, err := sess.db.GetUser(username)
// NOTE: this is just an example application. Obviously in a real
// application, password hashing would be in place.
if err != nil || user.Password != password {
// If we couldn't get the username from the DB, or if the password
// doesn't match, re-run the login transaction with an error
// message displayed.
return sess.login, "Username or password not valid.", nil
}
// Login was successful. Update the session state to the logged-in user.
sess.user = user
return sess.mainmenu, nil, nil
}
var newuserScreen = go3270.Screen{
{Row: 0, Col: 30, Intense: true, Content: "New User Registration"},
{Row: 2, Col: 0,
Content: "Please provide your user registration details."},
// Username
{Row: 4, Col: 0, Content: "Username . . .", Color: go3270.Green},
{Row: 4, Col: 15, Name: loginUsername, Write: true,
Highlighting: go3270.Underscore, Color: go3270.Turquoise},
{Row: 4, Col: 24, Autoskip: true}, // field "stop" character
// Password
{Row: 5, Col: 0, Content: "Password . . .", Color: go3270.Green},
{Row: 5, Col: 15, Name: loginPassword, Write: true, Hidden: true},
{Row: 5, Col: 79, Autoskip: true}, // field "stop" character
// Password
{Row: 6, Col: 0, Content: "Confirm Pass .", Color: go3270.Green},
{Row: 6, Col: 15, Name: loginPasswordConf, Write: true, Hidden: true},
{Row: 6, Col: 79, Autoskip: true}, // field "stop" character
// Name
{Row: 7, Col: 0, Content: "Name . . . . .", Color: go3270.Green},
{Row: 7, Col: 15, Name: loginName, Write: true,
Highlighting: go3270.Underscore, Color: go3270.Turquoise},
{Row: 7, Col: 46, Autoskip: true}, // field "stop" character
// Error message
{Row: 21, Col: 0, Name: loginErr, Color: go3270.Red, Intense: true},
// Key legend
{Row: 23, Col: 1, Content: "F1=Help"},
{Row: 23, Col: 14, Content: "F3=Exit"},
}
var newuserScreenRules = go3270.Rules{
loginUsername: {Validator: go3270.NonBlank},
loginPassword: {Validator: go3270.NonBlank, Reset: true},
loginPasswordConf: {Validator: go3270.NonBlank, Reset: true},
}
// newuserData is the data that can be passed into the newuser transaction
type newuserData struct {
username string
name string
errmsg string
}
func (sess *session) newuser(conn net.Conn, data any) (go3270.Tx, any, error) {
fieldValues := make(map[string]string)
if data != nil {
if newdata, ok := data.(newuserData); ok {
fieldValues[loginUsername] = newdata.username
fieldValues[loginName] = newdata.name
fieldValues[loginErr] = newdata.errmsg
}
}
resp, err := go3270.HandleScreen(
newuserScreen, // the screen to display
newuserScreenRules, // the rules to enforce
fieldValues, // pre-populated values in fields
[]go3270.AID{go3270.AIDEnter}, // keys we accept -- validating
[]go3270.AID{ // keys we accept -- non-validating
go3270.AIDPF1,
go3270.AIDPF3,
go3270.AIDClear,
},
loginErr, // name of field to put error messages in
4, 16, // cursor coordinates
conn)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDClear:
// Re-run transaction with empty values
return sess.newuser, nil, nil
case go3270.AIDPF1:
// Display help screen, returning to this transaction after
return help(sess.newuser), nil, nil
case go3270.AIDPF3:
// User wants to cancel registration, go back to login screen.
return sess.login, nil, nil
}
// Otherwise, we'll try creating the user.
username := resp.Values[loginUsername]
password := resp.Values[loginPassword]
passwordConf := resp.Values[loginPasswordConf]
name := resp.Values[loginName]
if password != passwordConf {
// Re-run the newuser transaction with an error message
return sess.newuser, newuserData{
username: username,
name: name,
errmsg: "Passwords do not match.",
}, nil
}
user, err := sess.db.CreateUser(User{
Username: username,
Password: password,
Name: name,
})
if err == ErrUserExists {
// Re-run the newuser transaction with an error message
return sess.newuser, newuserData{
username: username,
name: name,
errmsg: "Username already exists; please choose a new one.",
}, nil
}
if err != nil {
return sess.newuser, newuserData{
username: username,
name: name,
errmsg: "Unknown error creating new user.",
}, nil
}
// Success! We'll stick the new user in the session state like login does
// and go to the main menu.
sess.user = user
return sess.mainmenu, nil, nil
}
+232
View File
@@ -0,0 +1,232 @@
// 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.
// This example demonstrates using the RunTransactions() approach to
// structuring a go3270 application.
// This file contains the main menu and the example placeholder features
// of the application.
package main
import (
"net"
"strconv"
"strings"
"time"
"github.com/racingmars/go3270"
)
const mainmenuOption = "option"
const mainmenuUsername = "username"
const mainmenuTime = "time"
const mainmenuUsers = "users"
const mainmenuError = "errormsg"
var mainmenuScreen = go3270.Screen{
{Row: 0, Col: 31, Intense: true, Content: "Main Menu"},
// Option
{Row: 1, Col: 0, Content: "Option ===>", Color: go3270.Green},
{Row: 1, Col: 12, Name: mainmenuOption, Write: true,
Highlighting: go3270.Underscore, Color: go3270.Turquoise},
{Row: 1, Col: 79, Autoskip: true}, // field "stop" character
// Info
{Row: 3, Col: 57, Content: "User ID . :", Color: go3270.Green},
{Row: 3, Col: 69, Name: mainmenuUsername, Color: go3270.Turquoise},
{Row: 4, Col: 57, Content: "Time. . . :", Color: go3270.Green},
{Row: 4, Col: 69, Name: mainmenuTime, Color: go3270.Turquoise},
{Row: 5, Col: 57, Content: "Users . . :", Color: go3270.Green},
{Row: 5, Col: 69, Name: mainmenuUsers, Color: go3270.Turquoise},
// Options
{Row: 3, Col: 0, Content: "1", Color: go3270.White},
{Row: 3, Col: 3, Content: "Feature 1",
Color: go3270.Turquoise, Intense: true},
{Row: 3, Col: 17, Content: "A very cool feature", Color: go3270.Green},
{Row: 4, Col: 0, Content: "2", Color: go3270.White},
{Row: 4, Col: 3, Content: "Feature 2",
Color: go3270.Turquoise, Intense: true},
{Row: 4, Col: 17, Content: "Another neat feature", Color: go3270.Green},
{Row: 5, Col: 0, Content: "3", Color: go3270.White},
{Row: 5, Col: 3, Content: "Feature 3",
Color: go3270.Turquoise, Intense: true},
{Row: 5, Col: 17, Content: "This one's a boring feature",
Color: go3270.Green},
{Row: 7, Col: 5, Content: "Enter", Color: go3270.Green},
{Row: 7, Col: 11, Content: "X", Color: go3270.Turquoise, Intense: true},
{Row: 7, Col: 13, Content: "to log off and exit.", Color: go3270.Green},
// Error message
{Row: 21, Col: 0, Name: mainmenuError, Color: go3270.Red, Intense: true},
// Key legend
{Row: 23, Col: 1, Content: "F1=Help"},
{Row: 23, Col: 14, Content: "F3=Exit"},
{Row: 23, Col: 27, Content: "F5=Refresh"},
}
type mainmenuData struct {
option string // pre-populated option field value
errmsg string // error message to display
}
// 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) {
fieldValues := make(map[string]string)
if data != nil {
if d, ok := data.(mainmenuData); ok {
fieldValues[mainmenuOption] = d.option
fieldValues[mainmenuError] = d.errmsg
}
}
fieldValues[mainmenuUsername] = strings.ToUpper(sess.user.Username)
fieldValues[mainmenuTime] = time.Now().UTC().Format("15:04:05")
fieldValues[mainmenuUsers] = strconv.Itoa(sess.g.usercount)
resp, err := go3270.HandleScreen(
mainmenuScreen, // the screen to display
nil, // (no) rules to enforce
fieldValues, // pre-populated values in fields
[]go3270.AID{go3270.AIDEnter}, // keys we accept -- validating
[]go3270.AID{ // keys we accept -- non-validating
go3270.AIDPF1,
go3270.AIDPF3,
go3270.AIDPF5,
},
mainmenuError, // name of field to put error messages in
1, 13, // cursor coordinates
conn)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDPF1:
// Display help, then return to this transaction
return help(sess.mainmenu), nil, nil
case go3270.AIDPF3:
// Exit
return nil, nil, nil
case go3270.AIDPF5:
// Refresh
return sess.mainmenu, nil, nil
}
// Handle the available options:
switch resp.Values[mainmenuOption] {
case "1":
return sess.exampleFeature,
"This is feature 1, a very cool feature.", nil
case "2":
return sess.exampleFeature,
"This is feature 2, another neat feature.", nil
case "3":
return sess.exampleFeature,
"This is feature 3, which is a boring one.", nil
case "x", "X":
// As if user hit PF3; exit
return nil, nil, nil
case "":
// As if user hit PF5, refresh
return sess.mainmenu, nil, nil
default:
// Run the transaction again with an error message
return sess.mainmenu,
mainmenuData{
option: resp.Values[mainmenuOption],
errmsg: "Unknown option",
}, nil
}
}
const featureMessage = "message"
var exampleScreen = go3270.Screen{
{Row: 0, Col: 30, Intense: true, Content: "Application Feature"},
// Info -- replicate the data shown on the main menu
{Row: 3, Col: 57, Content: "User ID . :", Color: go3270.Green},
{Row: 3, Col: 69, Name: mainmenuUsername, Color: go3270.Turquoise},
{Row: 4, Col: 57, Content: "Time. . . :", Color: go3270.Green},
{Row: 4, Col: 69, Name: mainmenuTime, Color: go3270.Turquoise},
{Row: 5, Col: 57, Content: "Users . . :", Color: go3270.Green},
{Row: 5, Col: 69, Name: mainmenuUsers, Color: go3270.Turquoise},
// Feature-specific message
{Row: 12, Col: 0, Name: featureMessage},
{Row: 14, Col: 0, Content: "Press"},
{Row: 14, Col: 6, Content: "PF3", Intense: true, Color: go3270.White},
{Row: 14, Col: 10, Content: "to return to the main menu."},
// Error message
{Row: 21, Col: 0, Name: mainmenuError, Color: go3270.Red, Intense: true},
// Key legend
{Row: 23, Col: 1, Content: "F1=Help"},
{Row: 23, Col: 14, Content: "F3=Exit"},
{Row: 23, Col: 27, Content: "F5=Refresh"},
}
// 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) {
fieldValues := make(map[string]string)
if data != nil {
if message, ok := data.(string); ok {
fieldValues[featureMessage] = message
}
}
fieldValues[mainmenuUsername] = strings.ToUpper(sess.user.Username)
fieldValues[mainmenuTime] = time.Now().UTC().Format("15:04:05")
fieldValues[mainmenuUsers] = strconv.Itoa(sess.g.usercount)
resp, err := go3270.HandleScreen(
exampleScreen, // 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,
go3270.AIDPF5,
},
mainmenuError, // name of field to put error messages in
23, 79, // cursor coordinates
conn)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDPF1:
// Display help, then return to this transaction
return help(sess.exampleFeature), data, nil
case go3270.AIDPF3:
// Exit
return sess.mainmenu, nil, nil
case go3270.AIDPF5:
// Refresh
return sess.exampleFeature, data, nil
}
// ...there shouldn't be any actions not handled above. Just in case,
// we'll just re-run this transaction as if refresh was hit.
return sess.exampleFeature, data, nil
}
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/racingmars/go3270
go 1.13
go 1.18
+44
View File
@@ -0,0 +1,44 @@
// 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 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)
// 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 {
var next Tx
var err error
next = initial
// We run transactions until there isn't a next transaction to run, or
// an error.
for {
next, data, err = next(conn, data)
if err != nil {
// Error means we bail out and return the error to the caller.
return err
}
if next == nil {
// nil next transaction means we're done.
return nil
}
}
}