Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mx_execute.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Will execute a SASjs web service on SAS 9, Viya or SASjs Server
4 @details Prepares the input files and retrieves the resulting datasets from
5 the response JSON.
6
7 Note - the _webout fileref should NOT be assigned prior to running this macro.
8
9 @param [in] program The _PROGRAM endpoint to test
10 @param [in] inputfiles=(0) A list of space seperated fileref:filename pairs as
11 follows:
12 inputfiles=inref:filename inref2:filename2
13 @param [in] inputdatasets= (0) All datasets in this space seperated list are
14 converted into SASJS-formatted CSVs (see mp_ds2csv.sas) files and added to
15 the list of `inputfiles` for ingestion. The dataset will be sent with the
16 same name (no need for a colon modifier).
17 @param [in] inputparams=(0) A dataset containing name/value pairs in the
18 following format:
19 |name:$32|value:$1000|
20 |---|---|
21 |stpmacname|some value|
22 |mustbevalidname|can be anything, oops, %abort!!|
23
24 @param [in] debug= (131) Provide the _debug value.
25 @param [in] mdebug= (0) Set to 1 to provide macro debugging
26 @param [in] viyaresult= (WEBOUT_JSON) The Viya result type to return. For
27 more info, see mv_getjobresult.sas
28 @param [in] viyacontext= (SAS Job Execution compute context) The Viya compute
29 context on which to run the service
30 @param [out] outlib= (0) Output libref to contain the final tables. Set to
31 0 if the service output is not in JSON format.
32 @param [out] outref= (0) Output fileref to create, to contain the full _webout
33 response.
34 @param [out] outlogds= (_null_) Name of a dataset to receive the child's log
35 (one column, line). Populated on Viya, from the job log mv_jobflow
36 collects, and on SASjs Server, from the log the server appends to the
37 response - ms_testservice discards that log unless this is set. Not
38 populated on SAS 9, where proc stp returns no child log. On SASjs Server
39 it is only populated when debug is 131 or above: the server appends the log
40 to the response in debug mode, so a lower value returns an empty dataset.
41 @param [out] outstatus= (0) Name of a dataset to receive the outcome of each
42 child run - one row per program, with columns STATE, URI, ERRMSG and
43 PLATFORM. ERRMSG is only populated on SASjs Server, from the abort block;
44 the Viya job flow carries a state but no message, and SAS 9 gives neither.
45
46 <h4> SAS Macros </h4>
47 @li mf_getplatform.sas
48 @li mf_getuniquefileref.sas
49 @li mf_getuniquename.sas
50 @li mp_abort.sas
51 @li mp_binarycopy.sas
52 @li mp_ds2csv.sas
53 @li ms_testservice.sas
54 @li mv_getjobresult.sas
55 @li mv_jobflow.sas
56 @li mf_nobs.sas
57
58 <h4> Related Programs </h4>
59 @li mx_execute.test.sas
60
61 @version 9.4
62 @author Allan Bowe
63
64 @sasjslint {"maxLineLength": 95}
65**/
66
67%macro mx_execute(program,
68 inputfiles=0,
69 inputdatasets=0,
70 inputparams=0,
71 debug=131,
72 mdebug=0,
73 outlib=0,
74 outref=0,
75 outlogds=_null_,
76 outstatus=0,
77 viyaresult=WEBOUT_JSON,
78 viyacontext=SAS Job Execution compute context
79);
80%local dbg pcnt fref1 fref2 webref webrefpath i webcount var platform jobds respref clearmeref
81 msgref abortmsg;
82%if &mdebug=1 %then %do;
83 %put &sysmacroname entry vars:;
84 %put _local_;
85%end;
86%else %let dbg=*;
87
88/* sanitise inputparams */
89%let pcnt=0;
90%if &inputparams ne 0 %then %do;
91 data _null_;
92 set &inputparams;
93 if not nvalid(name,'v7') then putlog (_all_)(=);
94 else if name in (
95 'program','inputfiles','inputparams','debug','outlib','outref'
96 ) then putlog (_all_)(=);
97 else do;
98 x+1;
99 call symputx(name,quote(cats(value)),'l');
100 call symputx(cats('pval',x),name,'l');
101 call symputx('pcnt',x,'l');
102 end;
103 run;
104 %mp_abort(iftrue= (%mf_nobs(&inputparams) ne &pcnt)
105 ,mac=&sysmacroname
106 ,msg=%str(Invalid values in &inputparams)
107 )
108%end;
109
110/* convert inputdatasets to filerefs */
111%if "&inputdatasets" ne "0" %then %do;
112 %if %quote(&inputfiles)=0 %then %let inputfiles=;
113 %do i=1 %to %sysfunc(countw(&inputdatasets,%str( )));
114 %let var=%scan(&inputdatasets,&i,%str( ));
115 %local dsref&i;
116 %let dsref&i=%mf_getuniquefileref();
117 %mp_ds2csv(&var,outref=&&dsref&i,headerformat=SASJS)
118 %let inputfiles=&inputfiles &&dsref&i:%scan(&var,-1,.);
119 %end;
120%end;
121
122%let platform=%mf_getplatform();
123%let fref1=%mf_getuniquefileref();
124%let fref2=%mf_getuniquefileref();
125%let webref=%mf_getuniquefileref();
126%let webrefpath=%sysfunc(pathname(work))/%mf_getuniquename();
127/* mp_chop requires a physical path as input */
128filename &webref "&webrefpath";
129
130%if &platform=SASMETA %then %do;
131
132 /* parse the input files */
133 %if %quote(&inputfiles) ne 0 %then %do;
134 %let webcount=%sysfunc(countw(&inputfiles));
135 %put &=webcount;
136 %do i=1 %to &webcount;
137 %let var=%scan(&inputfiles,&i,%str( ));
138 %local webfref&i webname&i;
139 %let webref&i=%scan(&var,1,%str(:));
140 %let webname&i=%scan(&var,2,%str(:));
141 %put webref&i=&&webref&i;
142 %put webname&i=&&webname&i;
143 %end;
144 %end;
145 %else %let webcount=0;
146
147 proc stp program="&program";
148 inputparam _program="&program"
149 %do i=1 %to &webcount;
150 %if &webcount=1 %then %do;
151 _webin_fileref="&&webref&i"
152 _webin_name="&&webname&i"
153 %end;
154 %else %do;
155 _webin_fileref&i="&&webref&i"
156 _webin_name&i="&&webname&i"
157 %end;
158 %end;
159 _webin_file_count="&webcount"
160 _debug="&debug"
161 %do i=1 %to &pcnt;
162 /* resolve name only, proc stp fetches value */
163 &&pval&i=&&&&&&pval&i
164 %end;
165 ;
166 %do i=1 %to &webcount;
167 inputfile &&webref&i;
168 %end;
169 outputfile _webout=&webref;
170 run;
171
172 data _null_;
173 infile &webref;
174 file &fref1;
175 input;
176 length line $10000;
177 if index(_infile_,'>>weboutBEGIN<<') then do;
178 line=tranwrd(_infile_,'>>weboutBEGIN<<','');
179 put line;
180 end;
181 else if index(_infile_,'>>weboutEND<<') then do;
182 line=tranwrd(_infile_,'>>weboutEND<<','');
183 put line;
184 stop;
185 end;
186 else put _infile_;
187 run;
188 data _null_;
189 infile &fref1;
190 input;
191 put _infile_;
192 run;
193 %if &outlib ne 0 %then %do;
194 libname &outlib json (&fref1);
195 %end;
196 %if &outref ne 0 %then %do;
197 filename &outref temp;
198 %mp_binarycopy(inref=&webref,outref=&outref)
199 %end;
200
201 %if "&outlogds" ne "_null_" %then %do;
202 /* SAS 9 returns no child log. Create the dataset all the same, so a caller
203 * that reads it gets no rows rather than a missing table. */
204 data &outlogds;
205 length line $32767;
206 stop;
207 run;
208 %end;
209
210 /* proc stp gives no job status - say so rather than imply success */
211 %if "&outstatus" ne "0" %then %do;
212 data &outstatus;
213 length state $20 uri $256 errmsg $256 platform $10;
214 state='unknown';
215 uri='';
216 errmsg='proc stp returns no job status';
217 platform='SASMETA';
218 output;
219 run;
220 %end;
221
222%end;
223%else %if &platform=SASVIYA %then %do;
224
225 /* prepare inputparams */
226 %local ds1;
227 %let ds1=%mf_getuniquename();
228 %if "&inputparams" ne "0" %then %do;
229 proc transpose data=&inputparams out=&ds1;
230 id name;
231 var value;
232 run;
233 %end;
234 %else %do;
235 data &ds1;run;
236 %end;
237
238 /* parse the input files - convert to sasjs params */
239 %local webcount i var sasjs_tables;
240 %if %quote(&inputfiles) ne 0 %then %do;
241 %let webcount=%sysfunc(countw(&inputfiles));
242 %put &=webcount;
243 %do i=1 %to &webcount;
244 %let var=%scan(&inputfiles,&i,%str( ));
245 %local webfref&i webname&i sasjs&i.data;
246 %let webref&i=%scan(&var,1,%str(:));
247 %let webname&i=%scan(&var,2,%str(:));
248 %put webref&i=&&webref&i;
249 %put webname&i=&&webname&i;
250
251 %let sasjs_tables=&sasjs_tables &&webname&i;
252 data _null_;
253 infile &&webref&i lrecl=32767;
254 input;
255 if _n_=1 then call symputx("sasjs&i.data",_infile_);
256 else call symputx(
257 "sasjs&i.data",cats(symget("sasjs&i.data"),'0D0A'x,_infile_)
258 );
259 putlog "&sysmacroname infile: " _infile_;
260 run;
261 data &ds1;
262 set &ds1;
263 length sasjs&i.data $32767 sasjs_tables $1000;
264 sasjs&i.data=symget("sasjs&i.data");
265 sasjs_tables=symget("sasjs_tables");
266 run;
267 %end;
268 %end;
269 %else %let webcount=0;
270
271 data &ds1;
272 retain _program "&program";
273 retain _contextname "&viyacontext";
274 set &ds1;
275 putlog "&sysmacroname inputparams:";
276 putlog (_all_)(=);
277 run;
278
279 /* the flow table is an internal detail: unique name, thrown away below */
280 %let jobds=%mf_getuniquename(prefix=mx_execute_);
281 %mv_jobflow(inds=&ds1
282 ,maxconcurrency=1
283 ,outds=&jobds
284 ,outref=&fref1
285 ,mdebug=&mdebug
286 )
287 /* show the log */
288 data _null_;
289 infile &fref1;
290 input;
291 putlog _infile_;
292 run;
293 /* and keep it, if the caller asked */
294 %if "&outlogds" ne "_null_" %then %do;
295 data &outlogds;
296 infile &fref1 lrecl=32767 end=eof;
297 length line $32767;
298 do while(not eof);
299 input;
300 line=_infile_;
301 output;
302 end;
303 run;
304 %end;
305 /* get the uri to fetch results */
306 data _null_;
307 set &jobds;
308 call symputx('uri',uri);
309 putlog "&sysmacroname: fetching results for " uri;
310 run;
311 /* the outcome, before the flow table goes away */
312 %if "&outstatus" ne "0" %then %do;
313 data &outstatus;
314 length state $20 uri $256 errmsg $32767 platform $10;
315 platform='SASVIYA';
316 errmsg='';
317 set &jobds(keep=state uri);
318 run;
319 %end;
320 /* nothing else needs the flow table - do not leave it in the caller's WORK */
321 proc datasets lib=work nolist nowarn;
322 delete &jobds;
323 quit;
324 /* the abort message is not in the job state, it is in the result - so a
325 * result file is needed to read it even when the caller did not ask for one */
326 %let msgref=&outref;
327 %if "&outstatus" ne "0" and "&outref"="0" %then %do;
328 %let msgref=%mf_getuniquefileref();
329 %let clearmeref=1;
330 %end;
331 /* fetch results from webout.json */
332 %mv_getjobresult(uri=&uri,
333 result=&viyaresult,
334 outref=&msgref,
335 outlib=&outlib,
336 mdebug=&mdebug
337 )
338 /* a job that did not complete carries the abort message in its result, so
339 * errmsg is filled from there rather than left blank */
340 %if "&outstatus" ne "0" %then %do;
341 %let abortmsg=;
342 data _null_;
343 length line $32767 msg $32767;
344 retain found 0;
345 set &outstatus(keep=state);
346 if state ne 'completed' then do;
347 infile &msgref end=eof lrecl=32767;
348 do while(not eof);
349 input;
350 if found=0 and index(_infile_,'"MSG"')>0 then do;
351 line=tranwrd(_infile_,'\"','"');
352 if prxmatch('/"MSG" *: *"(.*)"/',line) then do;
353 msg=prxchange('s/.*"MSG" *: *"(.*)".*/$1/',-1,line);
354 found=1;
355 end;
356 end;
357 end;
358 end;
359 if found then call symputx('abortmsg',msg,'l');
360 run;
361 %if %superq(abortmsg) ne %then %do;
362 data &outstatus;
363 modify &outstatus;
364 errmsg=symget('abortmsg');
365 replace;
366 end;
367 %end;
368 %end;
369 /* only a ref created here may be cleared */
370 %if %length(&clearmeref)>0 %then %do;
371 filename &msgref clear;
372 %end;
373
374%end;
375%else %if &platform=SASJS %then %do;
376
377 /* the response is needed to judge the outcome, even if not requested -
378 * and only a ref we create here may be cleared afterwards */
379 %let respref=&outref;
380 %if "&respref"="0" and "&outstatus" ne "0" %then %do;
381 %let respref=%mf_getuniquefileref();
382 %let clearmeref=1;
383 %end;
384 %ms_testservice(&program
385 ,inputfiles=&inputfiles
386 ,inputdatasets=&inputdatasets
387 ,inputparams=&inputparams
388 ,debug=&debug
389 ,mdebug=&mdebug
390 ,outlib=&outlib
391 ,outref=&respref
392 ,outlogds=&outlogds
393 )
394 %if "&outstatus" ne "0" %then %do;
395 data &outstatus;
396 length state $20 uri $256 errmsg $32767 platform $10;
397 length line $32767;
398 retain state 'completed' uri '' errmsg '' platform 'SASJS' abortseen 0;
399 infile &respref end=eof lrecl=32767;
400 /* scan the whole response - the abort block is not always on the first
401 * line - but only take a message from inside that block, so a service
402 * with its own MSG field does not look like an abort */
403 do while(not eof);
404 input;
405 if index(_infile_,'"sasjsAbort"')>0 then do;
406 state='canceled';
407 abortseen=1;
408 end;
409 if abortseen=1 and errmsg='' and index(_infile_,'"MSG"')>0 then do;
410 /* a quote inside the message arrives escaped, and the capture below
411 * used to stop at it, returning the part before the first \" and
412 * dropping the rest. The escaped pairs are unescaped first, so the
413 * value is captured whole: the match is greedy and runs to the
414 * closing quote of the value rather than to the first quote after
415 * the field name. */
416 line=tranwrd(_infile_,'\"','"');
417 if prxmatch('/"MSG" *: *"(.*)"/',line) then
418 errmsg=prxchange('s/.*"MSG" *: *"(.*)".*/$1/',-1,line);
419 end;
420 end;
421 /* one row, whatever the response contained */
422 output;
423 run;
424 %end;
425 %if %length(&clearmeref)>0 %then %do;
426 filename &respref clear;
427 %end;
428
429%end;
430%else %do;
431 %put %str(ERR)OR: Unrecognised platform: &platform;
432%end;
433
434%if &mdebug=0 %then %do;
435 filename &fref1 clear;
436 %if &platform ne SASJS %then %do;
437 filename &fref2 clear;
438 filename &webref clear;
439 %end;
440%end;
441%else %do;
442 %put &sysmacroname exit vars:;
443 %put _local_;
444%end;
445
446%mend mx_execute;