How do I do HTTP GET and POST in Progress/OpenEdge ABL? - progress-4gl

The Progress docs spill plenty of ink on SOAP, but I'm having trouble finding the example for a simple HTTP GET/POST with Progress ABL.
How do I GET and POST strings to/from a URL?
Can the URL be https://?
Can Progress provide HTTP Basic or HTTP Digest authentication?

For future onlookers at this question:
Openedge now (since 11.5.1 I believe) has built in support for calling REST based webservices. These are enclosed in a provided .pl archive that is not in your PROPATH by default so that needs to be handled first (or the archive can be moved to a "better location").
The propath can be set in a number of ways, init files, registry, programatically etc. This is how it can be done in ABL (if done this way it must be repeated for each new session).
PROPATH = PROPATH + ",c:\pathtoprogress\OpenEdge\gui\OpenEdge.Net.pl".
There's also a version in "tty" directory, as well as an archive containing source code in the "src" directory.
Here's a very basic example:
USING OpenEdge.Net.HTTP.IHttpRequest.
USING OpenEdge.Net.HTTP.IHttpResponse.
USING OpenEdge.Net.HTTP.ClientBuilder.
USING OpenEdge.Net.HTTP.RequestBuilder.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oResponse AS IHttpResponse NO-UNDO.
oRequest = RequestBuilder:Get('http://stackoverflow.com/'):Request.
oResponse = ClientBuilder:Build():Client:Execute(oRequest).
MESSAGE
oResponse:StatusCode SKIP
oResponse:StatusReason SKIP
VIEW-AS ALERT-BOX.
Documentation for 11.6 can be found here.

Openedge has built in statements for handling SOAP services, but no simple statement for a GET/POST. What it does have, however, is a mechanism to read / write to specific sockets. So you could use this to build a HTTP post routine, or for that matter a routine to handle any other socket based protocol.
There is a routine - http.p - which will do a GET for you. Will let you see how the socket programming works if nothing else. You should be able to modify it quite easily to do a simple POST, but using SSL or getting into authentication might take quite a bit of work. You might be easier just dropping out to CURL in this case.
http.p used to be available from freeframework.org, but I've just checked and that domain has expired, so I've posted the code below.
/*-----------------------------------------------------------------------*
File........: http.p
Version.....: 1.1
Description : Makes a "Get" request from an HTTP server
Input Param : pHost = host name (without the "http://")
pPort = port number (usually 80)
pURL = begin with the first slash after the domain name.
Output Param: pResult = 0 or 1 (character)
pResponse = http headers returned
pContent = the document returned.
Author......: S.E. Southwell, Mario Paranhos - United Systems, Inc. (770) 449-9696
Created.....: 12/13/2000
Notes.......: Will not work with HTTPS.
Usage:
define var v-result as char no-undo.
define var v-response as char no-undo.
define var v-content as char no-undo.
{&out} "Hello" skip.
put stream webstream control null(0).
run proc/http.p("www.whosplayin.com","80","/",output v-result,output v-response,output v-content).
{&out} v-result "<hr>" skip
html-encode(v-response) "<hr>" skip
html-encode(v-content) "<hr>" skip
.
Last Modified: 10/20/01 - SES - Fixed to work in batch mode, or within a UDF.
--------------------------------------------------------------------------*/
&SCOPED-DEFINE HTTP-NEWLINE CHR(13) + CHR(10)
&SCOPED-DEFINE RESPONSE-TIMEOUT 45
DEFINE INPUT PARAMETER pHOST AS CHAR NO-UNDO.
DEFINE INPUT PARAMETER pPORT AS CHAR NO-UNDO.
DEFINE INPUT PARAMETER pURL AS CHAR NO-UNDO.
DEFINE OUTPUT PARAMETER pRESULT AS CHAR NO-UNDO.
DEFINE OUTPUT PARAMETER pRESPONSE AS CHAR NO-UNDO.
DEFINE OUTPUT PARAMETER pContent AS CHAR NO-UNDO.
DEFINE VARIABLE requestString AS CHAR NO-UNDO.
DEFINE VARIABLE vSocket AS HANDLE NO-UNDO.
DEFINE VARIABLE vBuffer AS MEMPTR NO-UNDO.
DEFINE VARIABLE vloop AS LOGICAL NO-UNDO.
DEFINE VARIABLE vPackets AS INTEGER NO-UNDO.
DEFINE VARIABLE wStatus AS LOGICAL NO-UNDO.
ASSIGN requestString = "GET " + pURL + " HTTP/1.0" + {&HTTP-NEWLINE} +
"Accept: */*" + {&HTTP-NEWLINE} +
"Host: " + phost + {&HTTP-NEWLINE} +
/*"Connection: Keep-Alive" + {&HTTP-NEWLINE} + */
{&HTTP-NEWLINE}.
/*OPEN THE SOCKET*/
CREATE SOCKET vSocket.
vSocket:SET-READ-RESPONSE-PROCEDURE ("readHandler",THIS-PROCEDURE).
ASSIGN wstatus = vSocket:CONNECT("-H " + phost + " -S " + pport) NO-ERROR.
/*Now make sure the socket is open*/
IF wstatus = NO THEN DO:
pResult = "0:No Socket".
DELETE OBJECT vSocket.
RETURN.
END.
/*Got socket - Now make HTTP request*/
SET-SIZE(vBuffer) = LENGTH(requestString) + 1.
PUT-STRING(vBuffer,1) = requestString.
vSocket:WRITE(vBuffer, 1, LENGTH(requestString)).
SET-SIZE(vBuffer) = 0.
/*Wait for a response*/
ASSIGN vloop = TRUE. /*Turns off automatically when request is done*/
DEFINE VAR vstarttime AS INTEGER.
ASSIGN vstarttime = etime.
WAITLOOP: DO WHILE vloop:
PROCESS EVENTS.
PAUSE 1.
/* Build in timer in case sending is never set to NO
this will terminate the program after 60 seconds
start-Etime will be reset by WriteData each time there
is activity on the socket to allow for long transmissions */
IF vstarttime + ({&RESPONSE-TIMEOUT} * 1000) < ETIME
THEN DO:
MESSAGE "timed out at " + string(etime - vstarttime) + " msec".
vSocket:DISCONNECT().
ASSIGN pResult = "0:Failure".
RETURN.
END. /*No Response, or timed out*/
END.
/*At this point, pResponse should be populated with the result (up to 32K)*/
vSocket:DISCONNECT().
DELETE OBJECT vSocket.
/*All Done!*/
ASSIGN pResult = "1:Success".
ASSIGN
pContent = SUBSTRING(pResponse,INDEX(pResponse,{&HTTP-NEWLINE} + {&HTTP-NEWLINE}),-1)
.
ASSIGN
pResponse = SUBSTRING(pResponse,1,INDEX(pResponse,{&HTTP-NEWLINE} + {&HTTP-NEWLINE}))
.
RETURN.
/*Handle the response from the webserver*/
PROCEDURE readHandler:
DEFINE VARIABLE bytesAvail AS INTEGER NO-UNDO.
DEFINE VARIABLE b AS MEMPTR NO-UNDO.
DEFINE VARIABLE lastBytes AS INTEGER NO-UNDO.
IF vSocket:connected() THEN ASSIGN bytesAvail = vSocket:GET-BYTES-AVAILABLE().
IF bytesAvail = 0 THEN DO: /*All Done*/
ASSIGN vloop = FALSE.
RETURN.
END.
/*OK, there's something on the wire... Read it in*/
SET-SIZE(b) = bytesAvail + 1.
vSocket:READ(b, 1, bytesAvail, 1).
ASSIGN pResponse = pResponse + GET-STRING(b,1).
SET-SIZE(b) = 0.
END PROCEDURE. /*readHandler*/

Progress Kbase ID: 20011: "Sample Code To Access a Web Site via HTTP with 4GL Sockets" is also a nice, generic example.

I recommend using the code example of Gordon Roberertson above because there is the "WAIT-FOR" of the Progress KB article replaced with a loop. So the prcoedure terminates after timeout period if anything goes wrong.
Please note that changing anything in requestString can cause timeouts. But adding a User-Agent is possible if you need for logging on your webserver:
ASSIGN requestString = "GET " + Path + " HTTP/1.0" + {&HTTP-NEWLINE}
+ "Accept: */*" + {&HTTP-NEWLINE}
+ "User-Agent: " + "User Agent String" + {&HTTP-NEWLINE}
+ "Host: " + Host + {&HTTP-NEWLINE}
+ {&HTTP-NEWLINE}.
Thanks to Gordon for his code example.

Related

Save job output from SDSF into a PDS and using ISPF functions in REXX

We periodically runs jobs and we need to save the output into a PDS and then parse the output to extract parts of it to save into another member. It needs to be done by issuing a REXX command using the percent sign and the REXX member name as an SDSF command line. I've attempted to code a REXX to do this, but it is getting an error when trying to invoke an ISPF service, saying the ISPF environment has not been established. But, this is SDSF running under ISPF.
My code has this in it (copied from several sources and modified):
parse arg PSDSFPARMS "(" PUSERPARMS
parse var PSDSFPARMS PCURRPNL PPRIMPNL PROWTOKEN PPRIMCMD .
PRIMCMD=x2c(PPRIMCMD)
RC = isfquery()
if RC <> 0 then
do
Say "** SDSF environment does not exist, exec ending."
exit 20
end
RC = isfcalls("ON")
Address SDSF "ISFGET" PPRIMPNL "TOKEN('"PROWTOKEN"')" ,
" (" VERBOSE ")"
LRC = RC
if LRC > 0 then
call msgrtn "ISFGET"
if LRC <> 0 then
Exit 20
JOBNAME = value(JNAME.1)
JOBNBR = value(JOBID.1)
SMPDSN = "SMPE.*.OUTPUT.LISTINGS"
LISTC. = ''
SMPODSNS. = ''
SMPODSNS.0 = 0
$ = outtrap('LISTC.')
MSGVAL = msg('ON')
address TSO "LISTC LVL('"SMPDSN"') ALL"
MSGVAL = msg(MSGVAL)
$ = outtrap('OFF')
do LISTCi = 1 to LISTC.0
if word(LISTC.LISTCi,1) = 'NONVSAM' then
do
parse var LISTC.LISTCi . . DSN
SMPODSNS.0 = SMPODSNS.0 + 1
i = SMPODSNS.0
SMPODSNS.i = DSN
end
IX = pos('ENTRY',LISTC.LISTCi)
if IX <> 0 then
do
IX = pos('NOT FOUND',LISTC.LISTCi,IX + 8)
if IX <> 0 then
do
address ISPEXEC "SETMSG MSG(IPLL403E)"
EXITRC = 16
leave
end
end
end
LISTC. = ''
if EXITRC = 16 then
exit 0
address ISPEXEC "TBCREATE SMPDSNS NOWRITE" ,
"NAMES(TSEL TSMPDSN)"
I execute this code by typing %SMPSAVE next to the spool output line on the "H" SDSF panel and it runs fine until it gets to this point in the REXX:
114 *-* address ISPEXEC "TBCREATE SMPDSNS NOWRITE" ,
"NAMES(TSEL TSMPDSN)"
>>> "TBCREATE SMPDSNS NOWRITE NAMES(TSEL TSMPDSN)"
ISPS118S SERVICE NOT INVOKED. A VALID ISPF ENVIRONMENT DOES NOT EXIST.
+++ RC(20) +++
Does anyone know why it says I don't have a valid ISPF environment and how I can get around this?
I've done quite a bit in the past with REXX, including writing REXX code to handle line commands, but this is the first time I've tried to use ISPEXEC commands within this code.
Thank you,
Alan

String Concatenating functions in Progress 4GL?

I came across a function in C#, string.Join() which is really helpful for string concatenation.
I am wondering is there any possibility of doing the same functionality in Progress 4GL ?
Typical C# example would be,
String result = “ ”;
result = string.Join(",", this.grpBox1.Controls.OfType<CheckBox>()
.Where(x => x.Checked)
.Select(c => c.Text));
MessageBox.Show(result);
I am not trying to convert C# to Progress but it would be really helpful if i could achieve the same functionality in Progress.
if you do really want your function you could wrap it and declare it as external you can re-link _progres to include your own c libs. used to be called HLC (probuild) but now its a set of scripts under a new directory name oebuild directly under the Progress install directory(DLC). The hlc and make directories are under the oebuild directory.
Instructions on the build process can be found in the README file located in the oebuild directory under the DLC directory. Prob. a sledge hammer to crack a nut you would have to understand what your doing not for the faint of heart. but if you have something written in c you want to call you can do it. prob. best to stick to the 4gl nearest thing progress has to arrays is extent not used much most people break out to a temp table . rows in a tt are far easier to handle and iterate so its a "mind set" switch a bit.
Important comments can also be found in the hlprodsp.c file located in the hlc directory, a subdirectory of oebuild.
As far as I know, there's nothing built in to that effort. But if I understand correctly, when you issue that command you get a list of that given object type (in your example, checkbox) as a CSV.
I built something that resembles it, as a start to whatever you'd like to implement. It's probably far from perfect, but it works for me. It's a window with various widget types, you select from the combo the type you'd like to search and generate, and it will show you at the end the values of two CSV variables: one with the widget names that match that type, and one with the respective values.
Here it goes:
&Scoped-define WINDOW-NAME C-Win
/*------------------------------------------------------------------------
File:
Description:
Input Parameters:
<none>
Output Parameters:
<none>
Author:
Created:
------------------------------------------------------------------------*/
/* This .W file was created with the Progress AppBuilder. */
/*----------------------------------------------------------------------*/
/* Create an unnamed pool to store all the widgets created
by this procedure. This is a good default which assures
that this procedure's triggers and internal procedures
will execute in this procedure's storage, and that proper
cleanup will occur on deletion of the procedure. */
CREATE WIDGET-POOL.
/* *************************** Definitions ************************** */
/* Parameters Definitions --- */
/* Local Variable Definitions --- */
DEFINE VARIABLE cWidgetLst AS CHARACTER NO-UNDO.
DEFINE VARIABLE cValueLst AS CHARACTER NO-UNDO.
/* ******************** Preprocessor Definitions ******************** */
&Scoped-define PROCEDURE-TYPE Window
&Scoped-define DB-AWARE no
/* Name of designated FRAME-NAME and/or first browse and/or first query */
&Scoped-define FRAME-NAME DEFAULT-FRAME
/* Standard List Definitions */
&Scoped-Define ENABLED-OBJECTS COMBO-BOX-1 BUTTON-1 SLIDER-1 SELECT-1 ~
RADIO-SET-1 TOGGLE-1 FILL-IN-2 COMBO-BOX-3
&Scoped-Define DISPLAYED-OBJECTS COMBO-BOX-1 SLIDER-1 SELECT-1 RADIO-SET-1 ~
TOGGLE-1 FILL-IN-2 COMBO-BOX-3
/* Custom List Definitions */
/* List-1,List-2,List-3,List-4,List-5,List-6 */
/* *********************** Control Definitions ********************** */
/* Define the widget handle for the window */
DEFINE VAR C-Win AS WIDGET-HANDLE NO-UNDO.
/* Definitions of the field level widgets */
DEFINE BUTTON BUTTON-1
LABEL "Generate"
SIZE 15 BY 1.14.
DEFINE VARIABLE COMBO-BOX-1 AS CHARACTER FORMAT "X(256)":U
VIEW-AS COMBO-BOX INNER-LINES 5
LIST-ITEMS "Fill-in","Radio-set","Toggle-box","Selection-List","slider","combo-box"
DROP-DOWN-LIST
SIZE 16 BY 1 NO-UNDO.
DEFINE VARIABLE COMBO-BOX-3 AS CHARACTER FORMAT "X(256)":U
LABEL "Combo 3"
VIEW-AS COMBO-BOX INNER-LINES 5
LIST-ITEMS "Large","Tall","Venti"
DROP-DOWN-LIST
SIZE 16 BY 1 NO-UNDO.
DEFINE VARIABLE FILL-IN-2 AS CHARACTER FORMAT "X(256)":U
LABEL "Fill 2"
VIEW-AS FILL-IN
SIZE 14 BY 1 NO-UNDO.
DEFINE VARIABLE RADIO-SET-1 AS INTEGER
VIEW-AS RADIO-SET VERTICAL
RADIO-BUTTONS
"Item 1", 1,
"Item 2", 2,
"Item 3", 3
SIZE 12 BY 3 NO-UNDO.
DEFINE VARIABLE SELECT-1 AS CHARACTER
VIEW-AS SELECTION-LIST SINGLE SCROLLBAR-VERTICAL
LIST-ITEMS "First","Second","Third"
SIZE 23 BY 2 NO-UNDO.
DEFINE VARIABLE SLIDER-1 AS INTEGER INITIAL 0
VIEW-AS SLIDER MIN-VALUE 0 MAX-VALUE 100 HORIZONTAL
TIC-MARKS NONE
SIZE 31 BY 2.38 NO-UNDO.
DEFINE VARIABLE TOGGLE-1 AS LOGICAL INITIAL no
LABEL "Toggle 1"
VIEW-AS TOGGLE-BOX
SIZE 13.4 BY .81 NO-UNDO.
DEFINE VARIABLE COMBO-BOX-2 AS CHARACTER FORMAT "X(256)":U
LABEL "Combo 2"
VIEW-AS COMBO-BOX INNER-LINES 5
LIST-ITEMS "Item 1"
DROP-DOWN-LIST
SIZE 16 BY 1 NO-UNDO.
DEFINE VARIABLE FILL-IN-3 AS CHARACTER FORMAT "X(256)":U
LABEL "Fill 3"
VIEW-AS FILL-IN
SIZE 14 BY 1 NO-UNDO.
DEFINE VARIABLE RADIO-SET-2 AS INTEGER
VIEW-AS RADIO-SET VERTICAL
RADIO-BUTTONS
"Item 1", 1,
"Item 2", 2,
"Item 3", 3
SIZE 12 BY 3 NO-UNDO.
DEFINE VARIABLE SLIDER-2 AS INTEGER INITIAL 0
VIEW-AS SLIDER MIN-VALUE 0 MAX-VALUE 100 HORIZONTAL
TIC-MARKS NONE
SIZE 17 BY 2.14 NO-UNDO.
DEFINE VARIABLE TOGGLE-2 AS LOGICAL INITIAL no
LABEL "Toggle 2"
VIEW-AS TOGGLE-BOX
SIZE 13.4 BY .81 NO-UNDO.
/* ************************ Frame Definitions *********************** */
DEFINE FRAME DEFAULT-FRAME
COMBO-BOX-1 AT ROW 1.95 COL 13 COLON-ALIGNED NO-LABEL WIDGET-ID 14
BUTTON-1 AT ROW 1.95 COL 32 WIDGET-ID 2
SLIDER-1 AT ROW 3.62 COL 42 NO-LABEL WIDGET-ID 18
SELECT-1 AT ROW 3.86 COL 10 NO-LABEL WIDGET-ID 16
RADIO-SET-1 AT ROW 6.24 COL 10 NO-LABEL WIDGET-ID 6
TOGGLE-1 AT ROW 6.24 COL 30 WIDGET-ID 10
FILL-IN-2 AT ROW 7.19 COL 28 COLON-ALIGNED WIDGET-ID 12
COMBO-BOX-3 AT ROW 7.29 COL 56 COLON-ALIGNED WIDGET-ID 20
WITH 1 DOWN NO-BOX KEEP-TAB-ORDER OVERLAY
SIDE-LABELS NO-UNDERLINE THREE-D
AT COL 1 ROW 1
SIZE 80 BY 16 WIDGET-ID 100.
DEFINE FRAME FRAME-A
TOGGLE-2 AT ROW 1.24 COL 51 WIDGET-ID 6
RADIO-SET-2 AT ROW 1.48 COL 3 NO-LABEL WIDGET-ID 2
FILL-IN-3 AT ROW 1.95 COL 25 COLON-ALIGNED WIDGET-ID 8
SLIDER-2 AT ROW 3.14 COL 46 NO-LABEL WIDGET-ID 12
COMBO-BOX-2 AT ROW 3.38 COL 24 COLON-ALIGNED WIDGET-ID 10
WITH 1 DOWN KEEP-TAB-ORDER OVERLAY
SIDE-LABELS NO-UNDERLINE THREE-D
AT COL 5 ROW 10.29
SIZE 72 BY 5.71
TITLE "Frame A" WIDGET-ID 200.
/* *********************** Procedure Settings ************************ */
/* Settings for THIS-PROCEDURE
Type: Window
Allow: Basic,Browse,DB-Fields,Window,Query
Other Settings: COMPILE
*/
/* ************************* Create Window ************************** */
IF SESSION:DISPLAY-TYPE = "GUI":U THEN
CREATE WINDOW C-Win ASSIGN
HIDDEN = YES
TITLE = "<insert window title>"
HEIGHT = 16
WIDTH = 80
MAX-HEIGHT = 16
MAX-WIDTH = 80
VIRTUAL-HEIGHT = 16
VIRTUAL-WIDTH = 80
RESIZE = yes
SCROLL-BARS = no
STATUS-AREA = no
BGCOLOR = ?
FGCOLOR = ?
KEEP-FRAME-Z-ORDER = yes
THREE-D = yes
MESSAGE-AREA = no
SENSITIVE = yes.
ELSE {&WINDOW-NAME} = CURRENT-WINDOW.
/* END WINDOW DEFINITION */
/* *********** Runtime Attributes and AppBuilder Settings *********** */
/* SETTINGS FOR WINDOW C-Win
VISIBLE,,RUN-PERSISTENT */
/* REPARENT FRAME */
ASSIGN FRAME FRAME-A:FRAME = FRAME DEFAULT-FRAME:HANDLE.
/* SETTINGS FOR FRAME DEFAULT-FRAME
FRAME-NAME */
DEFINE VARIABLE XXTABVALXX AS LOGICAL NO-UNDO.
ASSIGN XXTABVALXX = FRAME FRAME-A:MOVE-AFTER-TAB-ITEM (COMBO-BOX-3:HANDLE IN FRAME DEFAULT-FRAME)
/* END-ASSIGN-TABS */.
/* SETTINGS FOR FRAME FRAME-A
*/
IF SESSION:DISPLAY-TYPE = "GUI":U AND VALID-HANDLE(C-Win)
THEN C-Win:HIDDEN = no.
/* ************************ Control Triggers ************************ */
&Scoped-define SELF-NAME C-Win
ON END-ERROR OF C-Win /* <insert window title> */
OR ENDKEY OF {&WINDOW-NAME} ANYWHERE DO:
/* This case occurs when the user presses the "Esc" key.
In a persistently run window, just ignore this. If we did not, the
application would exit. */
IF THIS-PROCEDURE:PERSISTENT THEN RETURN NO-APPLY.
END.
ON WINDOW-CLOSE OF C-Win /* <insert window title> */
DO:
/* This event will close the window and terminate the procedure. */
APPLY "CLOSE":U TO THIS-PROCEDURE.
RETURN NO-APPLY.
END.
&Scoped-define SELF-NAME BUTTON-1
ON CHOOSE OF BUTTON-1 IN FRAME DEFAULT-FRAME /* Generate */
DO:
assign cWidgetLst = ''
cValueLst = ''.
run downTheRabbitHole ( input {&window-name}:handle).
MESSAGE cWidgetLst SKIP cValueLst
VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
&UNDEFINE SELF-NAME
/* *************************** Main Block *************************** */
/* Set CURRENT-WINDOW: this will parent dialog-boxes and frames. */
ASSIGN CURRENT-WINDOW = {&WINDOW-NAME}
THIS-PROCEDURE:CURRENT-WINDOW = {&WINDOW-NAME}.
/* The CLOSE event can be used from inside or outside the procedure to */
/* terminate it. */
ON CLOSE OF THIS-PROCEDURE
RUN disable_UI.
/* Best default for GUI applications is... */
PAUSE 0 BEFORE-HIDE.
/* Now enable the interface and wait for the exit condition. */
/* (NOTE: handle ERROR and END-KEY so cleanup code will always fire. */
MAIN-BLOCK:
DO ON ERROR UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
RUN enable_UI.
IF NOT THIS-PROCEDURE:PERSISTENT THEN
WAIT-FOR CLOSE OF THIS-PROCEDURE.
END.
/* ********************** Internal Procedures *********************** */
PROCEDURE buildCSV :
/*------------------------------------------------------------------------------
Purpose:
Parameters: <none>
Notes:
------------------------------------------------------------------------------*/
DEFINE INPUT-OUTPUT PARAMETER iopcList AS CHARACTER NO-UNDO.
DEFINE INPUT PARAMETER ipcValue AS CHARACTER NO-UNDO.
if ipcValue = ? then
assign ipcValue = '?'.
if ipcValue = '' then
assign ipcValue = ' '.
assign iopcList = iopcList + (if iopcList = '' then '' else ',') + ipcValue.
END PROCEDURE.
PROCEDURE disable_UI :
/*------------------------------------------------------------------------------
Purpose: DISABLE the User Interface
Parameters: <none>
Notes: Here we clean-up the user-interface by deleting
dynamic widgets we have created and/or hide
frames. This procedure is usually called when
we are ready to "clean-up" after running.
------------------------------------------------------------------------------*/
/* Delete the WINDOW we created */
IF SESSION:DISPLAY-TYPE = "GUI":U AND VALID-HANDLE(C-Win)
THEN DELETE WIDGET C-Win.
IF THIS-PROCEDURE:PERSISTENT THEN DELETE PROCEDURE THIS-PROCEDURE.
END PROCEDURE.
PROCEDURE downTheRabbitHole :
/*------------------------------------------------------------------------------
Purpose:
Parameters: <none>
Notes:
------------------------------------------------------------------------------*/
DEFINE INPUT PARAMETER iphWidget AS HANDLE NO-UNDO.
DEFINE VARIABLE hHandle AS HANDLE NO-UNDO.
assign hHandle = iphWidget:first-child.
do while valid-handle(hHandle):
if can-query(hHandle,"type") and hHandle:type = combo-box-1:screen-value in frame {&frame-name} then do:
/* something something something dark side... */
if can-query(hHandle,"name") then run buildCSV(input-output cWidgetLst,hHandle:name).
if can-query(hHandle,"screen-value") then run buildCSV(input-output cValueLst,hHandle:screen-value).
end.
if hHandle:type = "FIELD-GROUP" or hHandle:type = "FRAME" then
run downTheRabbitHole(input hHandle:first-child).
assign hHandle = hHandle:next-sibling.
end.
END PROCEDURE.
PROCEDURE enable_UI :
/*------------------------------------------------------------------------------
Purpose: ENABLE the User Interface
Parameters: <none>
Notes: Here we display/view/enable the widgets in the
user-interface. In addition, OPEN all queries
associated with each FRAME and BROWSE.
These statements here are based on the "Other
Settings" section of the widget Property Sheets.
------------------------------------------------------------------------------*/
DISPLAY COMBO-BOX-1 SLIDER-1 SELECT-1 RADIO-SET-1 TOGGLE-1 FILL-IN-2
COMBO-BOX-3
WITH FRAME DEFAULT-FRAME IN WINDOW C-Win.
ENABLE COMBO-BOX-1 BUTTON-1 SLIDER-1 SELECT-1 RADIO-SET-1 TOGGLE-1 FILL-IN-2
COMBO-BOX-3
WITH FRAME DEFAULT-FRAME IN WINDOW C-Win.
{&OPEN-BROWSERS-IN-QUERY-DEFAULT-FRAME}
DISPLAY TOGGLE-2 RADIO-SET-2 FILL-IN-3 SLIDER-2 COMBO-BOX-2
WITH FRAME FRAME-A IN WINDOW C-Win.
ENABLE TOGGLE-2 RADIO-SET-2 FILL-IN-3 SLIDER-2 COMBO-BOX-2
WITH FRAME FRAME-A IN WINDOW C-Win.
{&OPEN-BROWSERS-IN-QUERY-FRAME-A}
VIEW C-Win.
END PROCEDURE.

Request.QueryString And Request.ServerVariables("QUERY_STRING") Not Displaying UNICODE

I'm adding logging functionality to a Classic ASP application and am having difficulty getting Request.QueryString and Request.ServerVariables to display correctly when users submit UNICODE queries.
For example -
Response.write Request.QueryString
strVar1=&strVar2=%E8%A1%8C%E9%9B%B2%E6%B5%81%E6%B0%B4&strVar3=blah1&strVar4=blah2
Response.write Request.ServerVariables("QUERY_STRING")
strVar1=&strVar2=%E8%A1%8C%E9%9B%B2%E6%B5%81%E6%B0%B4&strVar3=blah1&strVar4=blah2
Yet if I specify a UNICODE variable in Request.QueryString it prints correctly:
Response.write Request.QueryString("strVar2")
行雲流水
How can I get Request.QueryString or Request.ServerVariables("QUERY_STRING") to include UNICODE? I do have the following on both my search and results pages and queries execute successfully against the database:
<%
Response.CodePage = 65001
Response.CharSet = "utf-8"
%>
To answer bobince's question, I'm trying to log search terms and pages from which they're submitted - if there's a better way to do this I'm all ears:
'Enable Logging: writes to MyDB.dbo.logging
'---------------------------------------------------------------------------------------------
Set cmd = Server.CreateObject("ADODB.Command")
cmd.CommandType = adCmdText
cmd.ActiveConnection = objConn
strADName = UCase(Request.ServerVariables("AUTH_USER"))
Protocol = Request.ServerVariables("SERVER_PROTOCOL")
Protocol = Left(Protocol,InStr(Request.ServerVariables("SERVER_PROTOCOL"),"/")-1)
if Request.ServerVariables("SERVER_PORT") = "80" then
port = ""
else
port = ":" & Request.ServerVariables("SERVER_PORT")
end if
CurPageURL = lcase(Protocol) & "://" & Request.ServerVariables("SERVER_NAME") &_
port & Request.ServerVariables("SCRIPT_NAME") & "?" & _
Request.ServerVariables("QUERY_STRING")
strSQL = "INSERT INTO MyDB.dbo.Logging([user],URL) SELECT ?, ? "
cmd.Parameters.Append (cmd.CreateParameter("User", adVarWChar, adParamInput, len(strADName), strADName))
cmd.Parameters.Append (cmd.CreateParameter("URL", adVarWChar, adParamInput, len(CurPageURL), CurPageURL))
cmd.CommandText = strSQL
set objRS = cmd.Execute
'-----------------------------------------------------------------------------------------------
This isn't really anything to do with Unicode.
Request.QueryString does two separate things.
If you use it without arguments, it returns the whole query string exactly as submitted by the browser (as above, the same as the QUERY_STRING server variable).
If you use it with an argument like "strVar2", it splits up the query string into parameter parts, finds the one(s) that correspond to the argument name (strVar2=...), and returns the value. In doing so it takes care of URL-decoding the components of the query string including the name and the value, so the %xx sequences in the input are decoded to the byte sequence they represent: 0xE8, 0xA1, 0x8C and so on. When you print that byte string to a page that a browser decodes as UTF-8, they will see 行雲流水.
You can do a URL-decode step yourself on the full query string if you really want. There isn't a built-in for URL-decoding in Classic ASP but you can write such a function yourself (see eg URLDecode from http://www.aspnut.com/reference/encoding.asp), or use
decodeURIComponent(s.replace(/\+/g, ' '))
from JScript.
Note that if you URL-decode a whole URL string together you are kind of breaking it. For example for input query string:
foo=abc%26def&bar=%e6%97%a5%e6%9c%ac%e8%aa%9e
you would get:
foo=abc&def&bar=日本語
which is fine for the Japanese, but the ampersand in the value for foo has broken the whole string so you can no longer tell for sure what the original parameters were. You should generally only decode URL components once you have split them up from the URL they came in. This is what ASP does for you: Request.QueryString("foo") would correctly return abc&def here which is why you should almost always be using that method to retrieve parameters.
What exactly are you trying to do by decoding a whole query string?
In case this can help someone else:
I resolved this by extracting variable names from QUERY_STRING using the split function with & as the delimiter. I then used the variables with Request.QueryString to get the values, including ones with UNICODE. This works unless the user includes & in the query, which will be pretty rare. I do trap for it so at least in that case I can still see the user and page accessed in the logs. Bobince your response was excellent and I will select it as the answer.
'Enable Logging: writes to MyDB.dbo.logging
'---------------------------------------------------------------------------------------------
Set cmd = Server.CreateObject("ADODB.Command")
cmd.CommandType = adCmdText
cmd.ActiveConnection = objConn
cmd.CommandTimeOut = 1200
strADName = UCase(Request.ServerVariables("AUTH_USER"))
Protocol = Request.ServerVariables("SERVER_PROTOCOL")
Protocol = Left(Protocol,InStr(Request.ServerVariables("SERVER_PROTOCOL"),"/")-1)
if Request.ServerVariables("SERVER_PORT") = "80" then
port = ""
else
port = ":" & Request.ServerVariables("SERVER_PORT")
end if
CurPageURL = lcase(Protocol) & "://" & Request.ServerVariables("SERVER_NAME") &_
port & Request.ServerVariables("SCRIPT_NAME") & "?"
On Error Resume Next
a = Request.ServerVariables("QUERY_STRING")
a = split(a,"&")
for each x in a
'response.write(left(x,InStr(x,"=")-1) & "<br />")
CurPageURL = CurPageURL + left(x,InStr(x,"=")-1) + "=" + Request.QueryString(left(x,InStr(x,"=")-1)) & "&"
next
If Err.Number <> 0 Then
CurPageURL = CurPageURL + "Error: Search Term Contained a &"
Err.Clear
Else
CurPageURL = left(CurPageURL,len(CurPageURL)-1)
End If
strSQL = "INSERT INTO MyDB.dbo.Logging([user],URL) SELECT ?, ? "
cmd.Parameters.Append (cmd.CreateParameter("User", adVarWChar, adParamInput, len(strADName), strADName))
cmd.Parameters.Append (cmd.CreateParameter("URL", adVarWChar, adParamInput, len(CurPageURL), CurPageURL))
cmd.CommandText = strSQL
set objRS = cmd.Execute

Best way to handle multiple connections at the same time

I have an application which listens to multiple connections and verifies whether the user is active or not
I use a 1 thread socket handling method with WSAASyncSelect.
The problem is that sometimes when a lot of users connecting at the same time some users get no reply
i think it is because the "send" hasn't been called yet and the program has received another connection so it goes again to handle the new connection ignoring the previous one. Like WSAASyncSelect has triggered and now it processing a new connection instead of completing the previous request.
So what to do to fix this issue? i tried to stop the events from WSAASyncSelect temporary by calling it with zero parameters when handling the connection until finish it then re enable network events but that didn't help either.
Here are the codes that handling the events (recieve then decrypt and then compare the bytes then send data according to what in listbox ie Active user or not)
This called upon receive of FD_READ
WSAAsyncSelect s, frmMain.hwnd, 0, 0 'Disabling Notifications event
Do Until bytesRecieved = SOCKET_ERROR
bytesRecieved = recv(wParam, buffer(Bytes), 500, 0)
If bytesRecieved > 0 Then
Bytes = Bytes + bytesRecieved
ElseIf bytesRecieved = 0 Then
Exit Sub
End If
Loop
Call MemCopy(ByVal decryptedArrival, buffer(0), Bytes)
WSAAsyncSelect s, frmMain.hwnd, WINSOCKMSG, FD_CONNECT + FD_READ + FD_CLOSE + FD_ACCEPT + FD_WRITE
If frmMain.chkSaveLog.value = vbChecked Then
frmMain.txtConnectionsLog.Text = frmMain.txtConnectionsLog.Text & Now & " Receiving a connection (" & wParam & ")" & vbNewLine
AutoScroll
If frmMain.chkAutoSave.value = vbChecked Then
strCurrentLogLine = Now & " Receiving a connection (" & wParam & ")"
AutoSaveLog (strCurrentLogLine)
frmMain.cmdClearLogs.Enabled = True
End If
End If
Below here is a decryption of bytes then comparing by ID as byte identifier like 1 = check for update
2 - send user info etc
in a Select Case statement following by a send Api.
And the accepting procedure
This called upon receive of FD_ACCEPT
Function AcceptConnection(wParam As Long)
lpString = String(32, 0)
AcSock = accept(wParam, sockaddress, Len(sockaddress))
strTempIP = getascip(sockaddress.sin_addr)
frmMain.txtConnectionsLog.Text = frmMain.txtConnectionsLog.Text & Now & " Getting a connection from IP address: " & _
strTempIP & " (" & AcSock & ")" & vbNewLine
AutoScroll
If frmMain.chkAutoSave.value = vbChecked Then
strCurrentLogLine = Now & " Getting a connection from IP address: " & strTempIP & " (" & AcSock & ")" & vbNewLine
AutoSaveLog (strCurrentLogLine)
End If
End Function
Are there any suggestions for a better performance?
What you showed is NOT the correct way to use WSAAsyncSelect(). Try something more like this instead:
When creating a listening socket:
lSock = socket(...)
bind(lSock, ...)
listen(lSock, ...)
WSAAsyncSelect lSock, frmMain.hwnd, WINSOCKMSG, FD_ACCEPT
When a listening socket receives FD_ACCEPT:
Function AcceptConnection(wParam As Long)
AcSock = accept(wParam, sockaddress, Len(sockaddress))
If AcSock = INVALID_SOCKET Then
Exit Sub
End If
WSAAsyncSelect AcSock, frmMain.hwnd, WINSOCKMSG, FD_READ + FD_CLOSE + FD_WRITE
...
End Function
When an accepted client socket receives FD_READ:
Function ReadConnection(wParam As Long)
Do
bytesRecieved = recv(wParam, ReadBuffer(ReadBytes), 500, 0)
If bytesRecieved = SOCKET_ERROR Then
If WSAGetLastError() <> WSAEWOULDBLOCK Then
Exit Sub
End If
ElseIf bytesRecieved = 0 Then
Exit Sub
Else
ReadBytes = ReadBytes + bytesRecieved
End If
Loop Until bytesRecieved = SOCKET_ERROR
' process ReadBuffer up to ReadBytes number of bytes as needed...
' remove processed bytes from front of ReadBuffer and decrement ReadBytes accordingly
...
End Function
When an accepted client socket receives FD_WRITE:
Function WriteConnection(wParam As Long)
While SendBytes > 0
bytesSent = send(wParam, SendBuffer(0), SendBytes, 0)
If bytesSent = SOCKET_ERROR Then
Exit Sub
End If
' remove bytesSent number of bytes from front of SendBuffer ...
SendBytes = SendBytes - bytesSent;
End While
End Function
The trick is that you need to allocate separate ReadBuffer and SendBuffer buffers for each accepted client. Make sure that each time you receive FD_READ that you are appending bytes only to the ReadBuffer of the socket that triggered FD_READ, and each time you receive FD_WRITE that you are removing bytes only from the SendBuffer of the socket that triggered FD_WRITE.
When recv() has no more bytes to read, process that socket's ReadBuffer as needed, removing only complete messages from the front and leaving incomplete messages for later processing.
When send() fails with WSAEWOULDBLOCK, append any unsent bytes to the SendBuffer of the socket that caused send() to fail. When you receive an FD_WRITE event for a socket, check that socket's SenBuffer and resend any bytes that are in it, stopping when the buffer is exhausted or an WSAEWOULDBLOCK error occurs.
Very easy, and quite effective, way to do it is to fork out for every incoming connection. This will most likely require you to restructure your application, but the basic flow should be as follows:
1. New connection is opened to the server
2. Server accepts the connection and forks out
3. The fork closes the original socket for listening, so only the parent will be accepting new connections
4. And then your magic happens, separate from the original thread.
This way you do not have to worry about issues of concurrency, as long as your machine can handle all the traffic and load because each connections is independent.

How to handle numbers of prowin32.exe

I've been doing an OS-COMMAND that execute the -p "procedure.p" with -param that are inside a loop.
My question is how can I handle the number of prowin32.exe that will not spawn too much so it won't consume all my processor and also not less to make it fast?
Note: Version 8.3c
Sample Code
DEFINE VARIABLE Filter AS CHARACTER NO-UNDO.
For Each Item No-lock:
Assign Filter = "Item.Item = " + Item.Item NO-ERROR.
OS-COMMAND NO-WAIT
VALUE("C:\dlcs\83c\bin\prowin32 " +
"-p C:\nanox\syte_server5\ATMS-CONFIGURED-ICONS\LIVE\Nanox_utility\procedure.p " +
"-pf C:\nanox\syte_server5\ATMS-CONFIGURED-ICONS\LIVE\Nanox_utility\Conf\pilot.pf " +
"-param " + Filter).
END.
QUIT.
I would do it a bit differently. Decide ahead of time how many threads and then divvy up the work like so:
/* worker.p
*
* i.e.:
* mbpro dbName -p worker.p -param "0,5"
* mbpro dbName -p worker.p -param "1,5"
* mbpro dbName -p worker.p -param "2,5"
* mbpro dbName -p worker.p -param "3,5"
* mbpro dbName -p worker.p -param "4,5"
*
* in this sample the threadNum starts at 0 -- so end it at numThreads - 1
*
*/
define variable threadNum as integer no-undo.
define variable numThreads as integer no-undo.
assign
threadNum = integer( entry( 1, session:parameter ))
numThreads = integer( entry( 2, session:parameter ))
.
for each item no-lock where (( item.item modulo numThreads ) = threadnum ):
/* whatever */
end.
return.
For starters I suggest that numThreads be roughly the number of cores available in the server.
Also, headless batch processes shouldn't be using prowin32.exe. They should use _progres.exe.
8.3 is unspeakably ancient, obsolete and unsupported. Is this system running on a single core Pentium with Windows 3.11? That's relevant because the 8.3 "workgroup edition" will not behave very well if this is a multi-core system. If it is "enterprise" it won't be so bad. But even so you'd be very well advised to upgrade to a current release (11.3 as of this writing) if performance is at all important to you.
I do not work with version8.3, but it must pass (possibly replace the temp-table by a work-table).
Here I what I certainly do but it must improve:
DEFINE TEMP-TABLE ttpid NO-UNDO
FIELD pid AS CHARACTER
FIELD SIZE AS CHARACTER
INDEX i1 IS UNIQUE PRIMARY pid.
DEFINE VARIABLE wline AS CHARACTER NO-UNDO.
DEFINE VARIABLE wfile AS CHARACTER NO-UNDO.
wfile = "c:/tmp/list_tasklis.txt".
OS-COMMAND SILENT VALUE("tasklist > " + wfile).
INPUT FROM VALUE(wfile).
REPEAT:
IMPORT UNFORMATTED wline.
IF wline BEGINS "prowin32.exe"
THEN DO:
DO WHILE wline MATCHES "* *":
wline = REPLACE(wline, " ", " ").
END.
FIND FIRST ttpid
WHERE ttpid.pid = ENTRY(2, wline, " ")
NO-LOCK NO-ERROR.
IF NOT AVAILABLE ttpid
THEN DO:
CREATE ttpid.
ASSIGN ttpid.pid = ENTRY(2, wline, " ")
ttpid.SIZE = ENTRY(5, wline, " ").
END.
END.
END.
INPUT CLOSE.
FOR EACH ttpid
NO-LOCK:
MESSAGE "PID =" ttpid.pid SKIP
"SIZE =" ttpid.SIZE
VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
OS-DELETE VALUE (wfile) NO-ERROR.