Oratcl Logon Dialog

Roland B. Roberts - When I originally started using Oratcl, it included an application call wosql which provided a simple windowed interface to Oracle. wosql included a logon dialog that I liked so much that I stole it :-). More recent versions os Oratcl do not come with wosql, but below is my current version of the logon dialog widget. Although this is functional enough to use as is, it can almost certainly stand some improvement. From a purely stylistic standpoint, I originally wrote it using StudlyCapitalizedNames which I don't particularly like anymore. Even apart from that, if you have suggestions, enhancements or bug fixes, please let me know.

This should work with Oratcl 3.x; it certainly works with Oratcl 3.3. It may work with Oratcl 2.7; I don't know of anything incompatible with Oratcl 2.7 or Tcl 8.0, but I haven't paid close enough attention to be sure. I will not work with Oratcl 4.x. Among other things, there are are references to oramsg to retrieve the error message and that array has been removed in version 4.0. Working around that is probably trivial, but I don't have Oracle 9 libraries for testing.

After sourcing the file below, you will call ::tkora::getSignOn callback to launch the dialog. Here is a sample extracted from one of my applications. If you run this from a shell window, or from TkCon, showLogon will be called with the logon-handle, the handle used by Oratcl's oraopen call. You will then see the contents of your Oracle signon (user, password, TNS name) and a message indicating the logon dialog was destroyed.


#!/bin/sh
#
# The following is a required on unix and harmless elsewhere
# *** DO NOT TOUCH THIS OR THE FOLLOWING LINE! *** \
exec wish $0 ${1+"$@"}

# Not all of these are required by everything, but...better to find
# out at the beginning that something is missing.
package require Tk
package require Oratcl

wm withdraw .
source logon.tcl

proc showLogon args {
    foreach k $args {
        puts "Oracle handle => $k"
    }
    foreach {k v} [array get ::tkora::logon::logon] {
        puts "$k ==> $v"
    }
}

# Fire up sign-on screen and wait.
set w [::tkora::logon::getSignOn ::showLogon]
tkwait window $w
puts "$w destroyed!"

And here is the file "logon.tcl" referenced above.

#
# Copyright © 2000-2002, FT/Interactive Data
# Copyright © 1998-1999, Muller Data Corporation
# Copyright © 1993?, Tom Poindexter <URL:http://www.nyx.net/~tpoindex/tcl.html>
# Roland B. Roberts <[email protected]>
#
#************************************************************************
# Copyright Info:
#
#     This code is derived from wosql, distributed with Oratcl 2.7
#
#************************************************************************
#
# RCS Revision
#   $Id: 4588,v 1.11 2002-11-15 09:02:26 jcw Exp $
#   $Source: /home/kennykb/Tcl/wiki/cvsroot/twhist/4588,v $
#
# Required by old TRCS scripts
#   $Header: /home/kennykb/Tcl/wiki/cvsroot/twhist/4588,v 1.11 2002-11-15 09:02:26 jcw Exp $
#
# 
# Variables
#    logon        - array holding logon information after successful
#                   logon
#    widget       - name of the toplevel widget containing the login
#                   dialog 
#    usropt       - dialog (display) options
#    serverList   - list of servers obtained from tnsnames.ora
#
# Procedures
#    getSignOn       - Oracle login dialog
#    reconnect       - reconnect using old (or specified) Oracle
#                      connect-string 
#    getServerList   - 
#    getFile         - Suck a whole file into a string
#    showSignOnError - Update logon dialog to display Oracle error
#    connect         - called by getSignOn when user enters Oracle logon
#                      information.  Retrieves dialog strings for actual
#                      Oracle logon attempt. 
#    connect_1       - called by connect to do "real" Oracle logon
#                      attempt. 
#
# Description
#
#    Only getSignOn and reconnect are intended to be public funcions.
#
#    getSignOn displays a dialog for entering an Oracle username,
#    password, and DB name.  getSignOn takes a single argument, the name
#    of a callback to be called when the signon completes successfully.
#    It returns the name of the toplevel widget containing the entire
#    signon dialog.
#
#    reconnect takes a single optional argument which is a connect
#    string of any form which is acceptable to oralogon with the default
#    to reuse the previous connect string, the one generated by
#    getSignOn in ::tkora::logon::logon(connect-string).
#
#    usropt contains logon-dialog specific options.  Eventually, I hope
#    to change getSignOn to take Tcl-style options for the values in
#    this array, something like "getSignon -title $text ...."
#

namespace eval ::tkora::logon {
    # Holds actual logon toplevel widget name.
    variable widget
    # Eye candy
    variable usropt
    array set usropt [list title "MIPS Operations" \
                          iconname "MIPS/OPS" \
                          geometry 275x300]
    # Cached copy of valid Oracle TNS names.
    variable serverList
    variable logon

    # o s.m is the contains the message header, this is a message widget.
    # o Menubutton and Button takes care of the labels on the buttons.
    # o Label takes care of the text (entry) labels.
    # o Entry takes care of the text entry widgets.
    # o s.err is the error message; this is a message widget.
    option add *MIPSLogon*s.m*font {-*-helvetica-bold-o-*-*-20-*-*-*-*-*-*-*}
    option add *MIPSLogon*Menubutton*font {-*-helvetica-bold-*-*-*-14-*-*-*-*-*-*-*}
    option add *MIPSLogon*Button*font {-*-helvetica-bold-*-*-*-14-*-*-*-*-*-*-*}
    option add *MIPSLogon*Label*font {-*-helvetica-bold-o-*-*-14-*-*-*-*-*-*-*}
    option add *MIPSLogon*Entry*font {-*-helvetica-normal-*-*-*-14-*-*-*-*-*-*-*}
    option add *MIPSLogon*s.err*font {-*-helvetica-bold-*-*-*-12-*-*-*-*-*-*-*}
    option add *MIPSLogon*s.err*foreground red

    # Should make a more OS-friendly name here.
    catch {option readfile [file join $env(HOME) .mipslogon]}
}

########################
# getServerlist
#  figure out what Oracle servers are available
#
proc ::tkora::logon::getServerList {} {
    global env
    set serverList ""

    # On Windows 95, look in these locations, depending on what version
    # of the Oracle libraries you have.
    foreach {ffile fremote} [list $env(ORACLE_HOME)/network/admin/tnsnames.ora 1 \
                                 $env(ORACLE_HOME)/net80/admin/tnsnames.ora 1] {
        set lines ""
        if {[file isfile $ffile]} {
            set ifile [split [::tkora::logon::getFile $ffile] \n]
            foreach line $ifile {
                if {[regexp -nocase {sid = ([a-z_]*)} $line m s1]} {
                    if {$fremote} {
                        lappend serverList @$s1
                    } else {
                        set s1 [lindex [split $s1 :] 0]
                        lappend serverList $s1
                    }
                }
            }
        }
    }

    foreach {ffile fremote} [list /etc/oratab 0 \
                                 /etc/sqlnet 1 \
                                 $env(HOME)/.sqlnet 1 \
                                 $env(HOME)/.tnsnames.ora 1] {

        set lines ""
        if {[file isfile $ffile]} {
            set ifile [split [::tkora::logon::getFile $ffile] \n]
            foreach line $ifile {
                if {[regexp -nocase "(^\[a-z_]\[^ \t\r]*).*$" $line m s1]} {
                    if {$fremote} {
                        lappend serverList @$s1
                    } else {
                        set s1 [lindex [split $s1 :] 0]
                        lappend serverList $s1
                    }
                }
            }
        }

    }

    # nothing found? put in serverList what names should look like
    if {[llength $serverList] == 0} {
        lappend serverList "(localdb)"
        lappend serverList "(@remote_alias)"
        lappend serverList "(@T:host:remotedb)"
    }
    return $serverList
}

########################
# getSignOn
#   The first window, get logon info and trys to connect to the server
#
proc ::tkora::logon::getSignOn {callback} {
    global env
    global tcl_platform
    variable logon
    variable serverList
    variable widget
    variable usropt

    # set ORACLE_HOME if not already set
    set ora_home [lsearch [array names env] ORACLE_HOME]

    if {$ora_home == -1} {
        set ora_home ""
        if {[string compare $tcl_platform(platform) windows] == 0} {
            # Okay, I really only know that this works for Win2k.  Under
            # Win95, the environment variable ORACLE_HOME was set.
            set ora_home [registry get "HKEY_LOCAL_MACHINE\\SOFTWARE\\ORACLE" ORACLE_HOME]
        } else {
            catch {set ora_home [exec ypcat passwd | egrep  ^oracle: ]}
            if {[string length $ora_home] > 0} {
                set ora_home [lindex [split $ora_home :] 5]
            } else {
                catch {set ora_home [exec egrep ^oracle: < /etc/passwd ]}
                if {[string length $ora_home] > 0} {
                    set ora_home [lindex [split $ora_home :] 5]
                } else {
                    set ora_home ""
                }
            }
        }
        set env(ORACLE_HOME) $ora_home
    } else {
        set ora_home $env(ORACLE_HOME)
    }

    # Get valid servers from various files
    if {![info exists serverList]} {
        set serverList [getServerList]
    } elseif {[llength serverList] == 0} {
        set serverList [getServerList]
    }

    # Maybe someone else likes the name ".logon" for their logon screen
    set mytop .logon
    for {set i 0} {$i < 100} {incr i} {
        if [catch {toplevel $mytop -class MIPSLogon} widget] {
            set mytop .logon$i
        } else {
            break;
        }
    }

    wm title    $widget $usropt(title)
    wm iconname $widget $usropt(iconname)
    wm geometry $widget $usropt(geometry)

    frame $widget.s
    message $widget.s.m -justify center  -text "Oracle Server Sign-On" -aspect 2000
    frame $widget.s.i
    entry $widget.s.i.uid -relief sunken  -width 12
    label $widget.s.i.id  -text "  User Id" -anchor e

    frame $widget.s.p
    entry $widget.s.p.pw  -relief sunken -width 12 -show * -exportselection false
    label $widget.s.p.p   -text "  Password" -anchor e

    frame $widget.s.s
    entry $widget.s.s.ser -relief sunken -width 12
    menubutton $widget.s.s.s -text " Servers  " -anchor e -menu $widget.s.s.s.m \
            -relief raised
    menu $widget.s.s.s.m
    foreach s $serverList {
        $widget.s.s.s.m add command -label $s \
            -command "$widget.s.s.ser delete 0 end; $widget.s.s.ser insert 0 $s "

    }
    if {[lsearch [array names env] ORACLE_SID] >= 0} {
        $widget.s.s.s.m add command -label $env(ORACLE_SID)
    }

    message $widget.s.err -text " " -justify center -aspect 500

    frame $widget.s.b
    button $widget.s.b.ok  -text "Sign on" -command "::tkora::logon::connect $callback"
    button $widget.s.b.can -text "Cancel" -command "destroy $widget"

    pack $widget.s.b     -side bottom -fill x -expand 0
    pack $widget.s.b.ok $widget.s.b.can -side left -fill x -expand 1

    pack $widget.s    -side top -fill both -expand 1
    pack $widget.s.m  -side top -fill x -pady 5

    pack $widget.s.i.uid -side right -expand 1 -padx 20
    pack $widget.s.i.id  -side left
    pack $widget.s.i  -side top -pady 10 -anchor e

    pack $widget.s.p.pw  -side right -expand 1 -padx 20
    pack $widget.s.p.p   -side left
    pack $widget.s.p  -side top -pady 10 -anchor e

    pack $widget.s.err   -side top -fill both

    pack $widget.s.s.ser -side right -expand 1 -padx 20

    pack $widget.s.s.s   -side left
    pack $widget.s.s     -side bottom -pady 10 -anchor se -expand 1

    $widget.s.i.uid delete 0 end
    if [info exists logon(userid)] {
        $widget.s.i.uid insert 0 $logon(userid)
        $widget.s.i.uid selection range 0 end
    }
    $widget.s.s.ser delete 0 end
    if [info exists logon(server)] {
        $widget.s.s.ser insert 0 $logon(server)
    } else {
        $widget.s.s.ser insert 0 [lindex $serverList 0]
    }

    focus $widget.s.i.uid

    bind $widget.s.i.uid <KeyPress-Return> "focus $widget.s.p.pw"
    bind $widget.s.p.pw  <KeyPress-Return> "$widget.s.b.ok invoke"
    bind $widget.s.s.ser <KeyPress-Return> "$widget.s.b.ok invoke"

    # allow Entry and Text to paste selections
    bind Entry <ButtonRelease-2> {
        set tk_s_rc [catch {set tk_s_s [selection get]} ]
        if {$tk_s_rc == 0} {%W insert insert $tk_s_s}
    }

    return $widget
}

########################
# getFile
#  read a file, return contents as string
#
proc ::tkora::logon::getFile {afile} {
    set contents {}
    catch { 
        set fd [open $afile]
        set contents [read $fd]
        close $fd
    }
    return $contents
}

proc ::tkora::logon::showSignOnError {w1 w2} {
    global oramsg
    $w1 configure -text $oramsg(errortxt)
    focus $w2
}


proc ::tkora::logon::connect {callback} {
    variable widget
    set userid [$widget.s.i.uid get]
    set passwd [$widget.s.p.pw get]
    set server [$widget.s.s.ser get]
    ::tkora::logon::connect_1 $userid $passwd $server $callback \
            [list ::tkora::logon::showSignOnError \
            ${::tkora::logon::widget}.s.err ${::tkora::logon::widget}.s.p.pw]
}

########################
# logon::connect_1
#   Try a connection to the Oracle server.
#
proc ::tkora::logon::connect_1 {userid passwd server cbokay cbfail} {
    global env
    variable logon
    variable widget

    # Check for local DB specification
    if {[string first @ $server] != 0} {
        set oldsid $env(ORACLE_SID)
        set env(ORACLE_SID) $server
        set server ""
    }

    set retcode [catch {oralogon ${userid}/${passwd}${server}} lda]
    if {$retcode == 0} {
        destroy $widget
        set dbh $lda
        set logon(connect-string) ${userid}/${passwd}${server}
        set logon(server) $server
        set logon(userid) $userid
        set logon(passwd) $passwd
        eval $cbokay $dbh
    } else {
        if [info exists oldsid] {
            set env(ORACLE_SID) $oldsid
        }
        eval $cbfail
    }
}

proc ::tkora::logon::reconnect {{connect-string {}}} {
    variable logon
    if {[string length ${connect-string}] == 0} {
        set connect-string $logon(connect-string)
    }
    set retcode [catch {oralogon ${connect-string}} lda]
    if {$retcode == 0} {
        return $lda
    } else {
        return
    }
}