├── ship.md ├── tests ├── rpc.dws ├── waiter.dws ├── assertFail.dyalog ├── RPCInit.aplf ├── assert.dyalog ├── RPCDo.aplf ├── setup.dyalog ├── test_APLProcess.dyalog └── LoadConga.aplf ├── README.md ├── Documentation ├── APLProcess.md └── Tool.md ├── LICENSE ├── Tool.dyalog └── APLProcess.dyalog /ship.md: -------------------------------------------------------------------------------- 1 | ### Folders to be shipped with Dyalog Installations: 2 | 3 | Documentation -------------------------------------------------------------------------------- /tests/rpc.dws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/library-core/master/tests/rpc.dws -------------------------------------------------------------------------------- /tests/waiter.dws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/library-core/master/tests/waiter.dws -------------------------------------------------------------------------------- /tests/assertFail.dyalog: -------------------------------------------------------------------------------- 1 | assertFail msg 2 | :If ~0∊⍴msg 3 | 223 ⎕SIGNAL⍨⎕←msg 4 | :EndIf 5 | -------------------------------------------------------------------------------- /tests/RPCInit.aplf: -------------------------------------------------------------------------------- 1 | r←RPCInit port;rc 2 | :If 0≠⊃rc←##.DRC.Clt(r←'c',⍕port)''port 3 | :AndIf 1009≠⊃rc ⍝ name already in use 4 | r←rc 5 | :EndIf 6 | -------------------------------------------------------------------------------- /tests/assert.dyalog: -------------------------------------------------------------------------------- 1 | assert←{ 2 | ⍺←'assertion failed' 3 | 0={0::0 ⋄ ⍎⍵}⍵:⍺,': ',⍵,' ⍝ at ',(2⊃⎕XSI,⊂'(immediate execution)'),'[',(⍕2⊃2↑⎕LC),']' 4 | '' 5 | } 6 | -------------------------------------------------------------------------------- /tests/RPCDo.aplf: -------------------------------------------------------------------------------- 1 | r←client RPCDo expr;rc 2 | rc←##.DRC.Send client expr 3 | assertFail 'DRC.Send failure' assert'0=⊃rc' 4 | rc←##.DRC.Wait client 5000 5 | assertFail 'DRC.Wait failure' assert '0=⊃rc' 6 | r←4⊃rc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # library-core 2 | 3 | This repository contains files which constitute the core application 4 | development library for Dyalog APL. Installations of Dyalog APL from 5 | version 16.0 (June 2017) will contain these files in the folder 6 | Library/Core below the main Dyalog folder. -------------------------------------------------------------------------------- /Documentation/APLProcess.md: -------------------------------------------------------------------------------- 1 | # APLProcess 2 | 3 | This will contain the documentation for the APLProcess tool. 4 | 5 | Example of use: 6 | 7 | ]load APLProcess 8 | #.APLProcess 9 | proc←⎕NEW APLProcess ('dfns.dws' 'MAXWS=200M' 0) 10 | proc.HasExited 11 | 0 -------------------------------------------------------------------------------- /Documentation/Tool.md: -------------------------------------------------------------------------------- 1 | # Tool 2 | 3 | This will contain the documentation for the Tool loading utility. 4 | 5 | Example of use: 6 | 7 | ]load tool 8 | #.Tool 9 | iR←Tool.New 'RConnect' ⍝ A new instance of RConnect 10 | 1⍕iR.x'rnorm(10,100,1)' 11 | 99.6 99.5 101.7 98.7 101.1 100.2 100.1 101.2 99.9 100.8 12 | -------------------------------------------------------------------------------- /tests/setup.dyalog: -------------------------------------------------------------------------------- 1 | setup;⎕ML;⎕IO;t 2 | (⎕IO ⎕ML)←1 3 | :If 0=##.⎕NC'TESTSOURCE' 4 | :If ~0∊⍴t←4⊃5179⌶⊃⎕XSI 5 | ##.TESTSOURCE←⊃1 ⎕NPARTS t 6 | :Else 7 | ##.TESTSOURCE←'/git/library-core/tests/' 8 | :EndIf 9 | :EndIf 10 | 2 ⎕FIX'file://',##.TESTSOURCE,'../APLProcess.dyalog' 11 | 2 ⎕FIX'file://',##.TESTSOURCE,'assert.dyalog' 12 | 2 ⎕FIX'file://',##.TESTSOURCE,'LoadConga.aplf' 13 | -------------------------------------------------------------------------------- /tests/test_APLProcess.dyalog: -------------------------------------------------------------------------------- 1 | test_APLProcess port;client;p 2 | :If 0=⎕NC'Debug' ⋄ Debug←0 ⋄ :EndIf 3 | :Trap Debug↓223 4 | assertFail assert'0=LoadConga' 5 | p←⎕NEW APLProcess 6 | p.Ws←##.TESTSOURCE,'rpc.dws' 7 | p.Args←'RPCPort=',⍕port 8 | p.Run 9 | ⎕DL 2 10 | assertFail assert'1=p.(IsRunning Proc.Id)' 11 | client←RPCInit port 12 | assertFail assert'(⎕DR client)∊80 82' 13 | assertFail assert'(⍳10)≡ client RPCDo ''⍳10''' 14 | assertFail assert'1=p.Kill' 15 | ⎕DL 2 16 | assertFail assert'0=p.(IsRunning Proc.Id)' 17 | ⎕←'Test completed' 18 | :Else 19 | ⎕←'Test failed' 20 | :EndTrap 21 | {}{0::1 ⋄ ##.DRC.Close client}⍬ 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dyalog Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/LoadConga.aplf: -------------------------------------------------------------------------------- 1 | rc←LoadConga;⎕IO;⎕ML;nc;rc 2 | (⎕IO ⎕ML rc)←1 3 | :Trap 0 4 | :If 0≠nc←⊃##.⎕NC'DRC' 5 | :If 0≠rc←{0::1 ⋄ ⊃##.DRC.Describe'.'}'' 6 | :Select nc 7 | :Case 9.1 ⍝ DRC namespace? 8 | :If 0≠⊃rc←##.DRC.Init'' 9 | ⎕←'Could not initialize pre-existing DRC namespace' 10 | →∆END 11 | :EndIf 12 | :Case 9.2 ⍝ Conga.[LIB] instance? 13 | ##.DRC←##.Conga.Init'' 14 | :EndSelect 15 | :If 0≠rc←{0::1 ⋄ ⊃##.DRC.Describe'.'}'' 16 | ⎕←'Could not initialize Conga' 17 | :EndIf 18 | :EndIf 19 | :Else 20 | :Trap 11 21 | 'Conga'##.⎕CY'conga' 22 | ##.DRC←##.Conga.Init'' 23 | rc←0 24 | :Else 25 | :Trap 11 26 | 'DRC'##.⎕CY'conga' 27 | ##.DRC.Init'' 28 | rc←0 29 | :Else 30 | ⎕←'Could not copy Conga or DRC from the Conga workspace' 31 | :EndTrap 32 | :EndTrap 33 | :EndIf 34 | :Else 35 | rc←-⎕EN 36 | ⎕←'Unexpected error attempting to load Conga' 37 | ⎕←↑⎕DM 38 | :EndTrap -------------------------------------------------------------------------------- /Tool.dyalog: -------------------------------------------------------------------------------- 1 | :Namespace Tool 2 | ⎕IO←⎕ML←1 3 | ⍝ Tool loader fo Dyalog APL 4 | ⍝ Currently supports Conga, SQAPL, RConnect, SharpPlot 5 | 6 | ⍝ ref←Tool.New 'toolname' [rarg] [larg] [minver] [target] 7 | ⍝ Returns a ref to an instance of the tool, read for use 8 | ⍝ Optional rarg is passed to constructor or initialisation function 9 | ⍝ Optional larg is passed if not '' 10 | ⍝ Optional minver is minimum version required (NOT SUPPORTED YET) 11 | ⍝ Optional target is ref to namespace to load into 12 | 13 | ⍝ ref←Tool.Prepare 'toolname' [minver] [target] 14 | ⍝ Returns a ref to the main namespace or class which implements the tool 15 | ⍝ See Tool.New for a description of arguments 16 | 17 | ∇ lc←_SetLc 18 | :Trap 2 19 | lc←⎕C 20 | :Else 21 | lc←819⌶ 22 | :EndTrap 23 | ∇ 24 | lc←_SetLc 25 | 26 | ge←{(1000⊥⍺)≥1000⊥⍵} ⍝ compare version numbers 27 | 28 | ∇ r←isWin 29 | r←'Win'≡3↑1⊃#.⎕WG'APLVersion' 30 | ∇ 31 | 32 | ∇ r←findws ws;DYALOG;path 33 | ⍝ Look for workpace in the current directory, then in the DYALOG folder, finally search WSPATH 34 | 35 | :If ~⎕NEXISTS r←ws,'.dws' ⍝ check current folder first 36 | DYALOG←{⍵,'/'↓⍨'/\'∊⍨¯1↑⍵}2 ⎕NQ'.' 'GetEnvironment' 'DYALOG' 37 | :AndIf ~⎕NEXISTS r←DYALOG,'/ws/',r ⍝ check DYALOG folder 38 | :For path :In ':;'[1+isWin](≠⊆⊢)2 ⎕NQ'.' 'GetEnvironment' 'WSPATH' 39 | →0⍴⍨⎕NEXISTS r←path,'/',ws,'.dws' 40 | :EndFor 41 | ('Unable to locate workspace "',ws,'"')⎕SIGNAL 11 42 | :EndIf 43 | ∇ 44 | 45 | ∇ r←New args;module;rarg;larg;minver;target;z;ns 46 | ⍝ Initialise a Core Dyalog Application Component 47 | ⍝ args[1] - or simple argument: Conga|SQAPL|SharpPlot (more to come) 48 | ⍝ The rest are optional 49 | ⍝ args[2] - right argument to Init function of the component (default = '') 50 | ⍝ [3] - left argument to Init function (missing or '' = monadic call) 51 | ⍝ [4] - Minimum version required in the form (major minor svnrev) 52 | ⍝ [5] - Terget namespace to materialise namespaces or classes in (default = #) 53 | 54 | (module rarg larg minver target)←args←{⍵,(≢⍵)↓'' '' ''(0 0 0)#},⊆args 55 | 56 | :If larg≡'' 57 | z←⎕EX'larg' ⋄ larg←⊢ 58 | :EndIf 59 | 60 | :Trap 0 61 | ns←Prepare args[1 4 5] 62 | :Else 63 | (⊃⎕DMX.DM)⎕SIGNAL ⎕DMX.EN 64 | :EndTrap 65 | 66 | ⍝ Now initialise 67 | :Select lc module 68 | 69 | :Case 'conga' 70 | :If 9.2=⎕NC⊂'ns' ⍝ Instance (of Conga) 71 | r←ns 72 | :ElseIf 3=ns.⎕NC'FindInst' ⍝ Looks like the Conga namespace 73 | r←larg ns.Init rarg 74 | :ElseIf 3=ns.⎕NC'IWAAuth' ⍝ Looks like old style DRC 75 | :If (⊃z←larg ns.Init rarg)∊0 76 | r←ns 77 | :Else 78 | ('DRC.Init returned: ',⍕z)⎕SIGNAL 11 79 | :EndIf 80 | :Else 81 | ('Unable to determine Conga version of ',ns)⎕SIGNAL 11 82 | :EndIf 83 | 84 | :Case 'sqapl' 85 | :If 0=⊃z←ns.Init rarg 86 | r←ns 87 | :Else 88 | ('SQA.Init returned: ',⍕z)⎕SIGNAL 11 89 | :EndIf 90 | 91 | :Case 'sharpplot' 92 | r←#.⎕NEW ns 93 | 94 | :Case 'rconnect' 95 | r←#.⎕NEW ns 96 | {}r.init 97 | :Else 98 | 'Supported tools are: Conga RConnect SharpPlot SQAPL'⎕SIGNAL 6 99 | :EndSelect 100 | ∇ 101 | 102 | ∇ r←Prepare args;module;minver;target 103 | ⍝ Load, but do not initialise a Core Dyalog Application Component 104 | ⍝ Return a reference to the namespace/class that was loaded 105 | 106 | ⍝ args[1] - or simple argument: Conga|SQAPL|SharpPlot|RConnect 107 | 108 | ⍝ Optional arguments: 109 | ⍝ [2] - Minimum version required in the form (major minor svnrev) 110 | ⍝ [3] - Terget namespace to materialise namespaces or classes in (default = #) 111 | 112 | (module minver target)←{⍵,(≢⍵)↓''(0 0 0)#},⊆args 113 | 'Minimum Version not yet supported'⎕SIGNAL(0 0 0≢3↑minver)/11 114 | 115 | :Select lc module 116 | :Case 'conga' 117 | r←LoadConga minver target 118 | :Case 'sqapl' 119 | r←LoadSQAPL minver target 120 | :Case 'sharpplot' 121 | r←LoadSharpPlot minver target 122 | :Case 'rconnect' 123 | r←LoadRConnect minver target 124 | :Else 125 | 'Supported tools are: Conga RConnect SharpPlot SQAPL'⎕SIGNAL 6 126 | :EndSelect 127 | ∇ 128 | 129 | ∇ r←LoadRConnect(minver target) 130 | ⍝ Unable to verify version, rconnect doesn't expose one 131 | :If 9≠target.⎕NC'R' 132 | target.⎕CY findws'rconnect' 133 | :EndIf 134 | r←target.R 135 | ∇ 136 | 137 | ∇ r←LoadSQAPL(minver target) 138 | :If 9≠target.⎕NC'SQA' 139 | 'SQA'target.⎕CY findws'sqapl' 140 | :EndIf 141 | r←target.SQA 142 | ∇ 143 | 144 | :Section Conga 145 | 146 | ∇ r←LoadConga(minver target);ns;m;nss;copied;ws 147 | 148 | r←copied←'' 149 | 150 | :If ∨/m←0≠target.⎕NC nss←'Conga' 'DRC' 151 | r←target⍎ns←(m⍳1)⊃nss 152 | :Else 153 | (ns←'Conga')target.⎕CY ws←findws'conga' 154 | copied←' copied from "',ws,'"' 155 | r←target.Conga 156 | :EndIf 157 | 158 | ⍝ :If ~r.Version ge minver 159 | ⍝ ('LoadConga: ',ns,copied,' has version ',⍕minver)⎕SIGNAL 11 160 | ⍝ :EndIf 161 | ∇ 162 | 163 | :EndSection Conga 164 | 165 | :Section SharpPlot 166 | 167 | ∇ r←{apl}LoadSharpPlot(minver target);nc;version 168 | ⍝ note that ns may be an array of references 169 | 170 | CAUSEWAY←⎕NULL ⍝ reference to APL ⎕CY of sharpplot 171 | 172 | :If 0=⎕NC'apl' ⋄ apl←0 ⋄ :EndIf ⍝ do not force APL unless told to 173 | :If apl 174 | :OrIf 4≠⊃DotNetVersion ⍝ require .Net Framework until .NetCore has proper GDI and WinForms 175 | target.System.Drawing←target.System←target.Causeway←GetAplCauseway 176 | :Else 177 | target.⎕USING,←',system.drawing.dll' ',sharpplot.dll' 178 | :EndIf 179 | nc←target.⎕NC⊂'Causeway.SharpPlot.Version' 180 | :If ¯2.6=nc ⍝ .Net 181 | :OrIf ¯2.3=nc ⍝ APL post-v3.39 182 | version←target.Causeway.SharpPlot.Version 183 | :ElseIf ¯3.1=⎕NC⊂'ns.Causeway.SharpPlot.GetVersion' ⍝ APL pre-v3.39 184 | version←target.Causeway.SharpPlot.GetVersion 185 | :Else ⍝ failed to load sharpplot ! 186 | version←'' 187 | :EndIf 188 | r←target.Causeway.SharpPlot 189 | ∇ 190 | 191 | ∇ v←DotNetVersion;⎕USING 192 | ⍝ Assumptions : 193 | ⍝ Dyalog only allows .Net Framework v4 (v1 v2 and v3 are not supported anymore) 194 | ⍝ Therefore v3 and v5 must mean .Net Core 195 | :Trap 0 ⍝ ⎕USING← has been known to fail 196 | ⎕USING←'' ⍝ Ensure that System is present if at all possible 197 | v←System.Environment.Version.(Major Minor) 198 | :Else 199 | v←0 0 200 | :EndTrap 201 | ∇ 202 | 203 | ∇ ns←GetAplCauseway ⍝ do the ⎕CY only once, if needed 204 | :If CAUSEWAY≡⎕NULL 205 | CAUSEWAY←#.(⎕NS'') ⍝ unnamed namespace in # 206 | CAUSEWAY.⎕CY findws'sharpplot' 207 | :EndIf 208 | ns←CAUSEWAY 209 | ∇ 210 | 211 | :EndSection SharpPlot 212 | 213 | :EndNamespace 214 | -------------------------------------------------------------------------------- /APLProcess.dyalog: -------------------------------------------------------------------------------- 1 | :Class APLProcess 2 | ⍝ Start (and eventually dispose of) a Process 3 | ⍝ Note: ssh support under Windows requires Renci.SshNet.dll 4 | 5 | (⎕IO ⎕ML)←1 1 6 | 7 | ∇ r←Version 8 | :Access Public Shared 9 | r←'APLProcess' '2.2.8' '2 February 2023' 10 | ∇ 11 | 12 | :Field Public Args←'' 13 | :Field Public Ws←'' 14 | :Field Public Exe←'' 15 | :Field Public Proc 16 | :Field Public onExit←'' 17 | :Field Public RunTime←0 ⍝ Boolean or name of runtime executable 18 | :Field Public IsSsh 19 | :Field Public RideInit←'' 20 | :Field Public OutFile←'' 21 | :Field Public WorkingDir←'' 22 | 23 | endswith←{w←,⍵ ⋄ a←,⍺ ⋄ w≡(-(⍴a)⌊⍴w)↑a} 24 | tonum←{⊃⊃(//)⎕VFI ⍵} 25 | eis←{2>|≡⍵:,⊂⍵ ⋄ ⍵} ⍝ enclose if simple 26 | deb←{1↓¯1↓{⍵/⍨~' '⍷⍵}' ',⍵,' '} ⍝ delete extraneous blanks 27 | part←{⍵⊆⍨~⍺{⍵∧⍺>+\⍵}' '=⍵} ⍝ partition first ⍺ sections 28 | 29 | ∇ r←IsWin 30 | :Access public shared 31 | r←'Win'≡Platform 32 | ∇ 33 | 34 | ∇ r←IsMac 35 | :Access public shared 36 | r←'Mac'≡Platform 37 | ∇ 38 | 39 | ∇ r←IsAIX 40 | :Access public shared 41 | r←'AIX'≡Platform 42 | ∇ 43 | 44 | ∇ r←Platform 45 | :Access public shared 46 | r←3↑⊃#.⎕WG'APLVersion' 47 | ∇ 48 | 49 | ∇ r←IsNetCore 50 | :Access public shared 51 | r←1 1≡2↑2250⌶0 ⍝ 2250⌶0 is now the preferred mechanism to interrogate .NET availability 52 | ⍝ r←(,'1')≡2 ⎕NQ'.' 'GetEnvironment' 'DYALOG_NETCORE' 53 | ∇ 54 | 55 | ∇ r←UsingSystemDiagnostics 56 | :Access public shared 57 | r←(1+IsNetCore)⊃'System,System.dll' 'System,System.Diagnostics.Process' 58 | ∇ 59 | 60 | ∇ path←SourcePath;source 61 | ⍝ Determine the source path of the class 62 | 63 | :Trap 6 64 | source←⍎'(⊃⊃⎕CLASS ⎕THIS).SALT_Data.SourceFile' ⍝ ⍎ works around a bug 65 | :Else 66 | :If 0=⍴source←{((⊃¨⍵)⍳⊃⊃⎕CLASS ⎕THIS)⊃⍵,⊂''}5177⌶⍬ 67 | source←⎕WSID 68 | :Else ⋄ source←4⊃source 69 | :EndIf 70 | :EndTrap 71 | path←{(-⌊/(⌽⍵)⍳'\/')↓⍵}source 72 | ∇ 73 | 74 | ∇ make 75 | :Access public instance 76 | :Implements constructor 77 | make_common 78 | ∇ 79 | 80 | ∇ make1 args;rt;cmd;ws 81 | :Access Public Instance 82 | :Implements Constructor 83 | ⍝ args is: 84 | ⍝ [1] the workspace to load 85 | ⍝ [2] any command line arguments 86 | ⍝ {[3]} if present, a Boolean indicating whether to use the runtime version, OR a character vector of the executable name to run 87 | ⍝ {[4]} if present, the RIDE_INIT parameters to use 88 | ⍝ {[5]} if present, a log-file prefix for process output 89 | ⍝ {[6]} if present, the "current directory" when APL is started 90 | make_common 91 | args←{2>|≡⍵:,⊂⍵ ⋄ ⍵}args 92 | args←6↑args,(⍴args)↓'' '' 0 RideInit OutFile WorkingDir 93 | (ws cmd rt RideInit OutFile WorkingDir)←args 94 | PATH←SourcePath 95 | Start(ws cmd rt) 96 | ∇ 97 | 98 | ∇ make_common 99 | Proc←⎕NS'' ⍝ Do NOT do this in the field definition 100 | IsSsh←0 101 | WorkingDir←1⊃1 ⎕NPARTS'' ⍝ default directory 102 | ∇ 103 | 104 | ∇ Run 105 | :Access Public Instance 106 | Start(Ws Args RunTime) 107 | ∇ 108 | 109 | ∇ Start(ws args rt);psi;pid;cmd;host;port;keyfile;exe;z;output 110 | (Ws Args)←ws args 111 | args,←' RIDE_INIT="',RideInit,'"',(0≠≢RideInit)/' RIDE_SPAWNED=1' ⍝ NB Always set RIDE_INIT to override current process setting 112 | 113 | :If ~0 2 6∊⍨10|⎕DR rt ⍝ if rt is character or nested, it defines what to start 114 | Exe←(RunTimeName⍣rt)GetCurrentExecutable ⍝ else, deduce it 115 | :Else 116 | Exe←rt 117 | rt←0 118 | :EndIf 119 | 120 | :If IsWin∧~IsSsh←326=⎕DR Exe 121 | ⎕USING←UsingSystemDiagnostics 122 | psi←⎕NEW Diagnostics.ProcessStartInfo,⊂Exe(ws,' ',args) 123 | psi.WindowStyle←Diagnostics.ProcessWindowStyle.Minimized 124 | psi.WorkingDirectory←WorkingDir 125 | 126 | :If ~0∊⍴OutFile 127 | psi.UseShellExecute←0 ⍝ this needs to be false to redirect IO (.NET Core defaults to false, .NET Framework defaults to true) 128 | psi.StandardOutputEncoding←Text.Encoding.UTF8 129 | psi.RedirectStandardOutput←1 ⍝ redirect standard output 130 | :Else 131 | psi.RedirectStandardOutput←0 132 | psi.UseShellExecute←1 133 | :EndIf 134 | 135 | Proc←Diagnostics.Process.Start psi 136 | 137 | :Else ⍝ Unix 138 | :If ~∨/'LOG_FILE'⍷args ⍝ By default 139 | args,←' LOG_FILE=/dev/null ' ⍝ no log file 140 | :EndIf 141 | 142 | cmd←(~0∊⍴WorkingDir)/'cd ',WorkingDir,'; ' 143 | :If IsSsh 144 | (host port keyfile exe)←Exe 145 | cmd,←args,' ',exe,' +s -q ',ws 146 | Proc←SshProc host port keyfile cmd 147 | :Else 148 | z←⍕GetCurrentProcessId 149 | output←(1+×≢OutFile)⊃'/dev/null'OutFile 150 | cmd,←'{ ',args,' ',Exe,' +s -q ',ws,' -c APLppid=',z,' ',output,' 2>&1 & } ; echo $!' 151 | pid←tonum⊃_SH cmd 152 | Proc.Id←pid 153 | Proc.HasExited←HasExited 154 | :EndIf 155 | Proc.StartTime←⎕NEW Time ⎕TS 156 | :EndIf 157 | ∇ 158 | 159 | ∇ Close;out 160 | :Implements Destructor 161 | :If IsWin 162 | :AndIf ~0∊⍴OutFile 163 | WaitForKill 200 0.1 ⍝ don't run this in a separate thread if redirecting output on Windows 164 | :Trap 0 165 | out←Proc.StandardOutput.ReadToEnd 166 | (⊂out)⎕NPUT OutFile 1 167 | :EndTrap 168 | :Else 169 | WaitForKill&200 0.1 ⍝ otherwise run in a thread for improved throughput 170 | :EndIf 171 | ∇ 172 | 173 | ∇ WaitForKill(limit interval);count 174 | :If (0≠⍴onExit)∧~HasExited ⍝ If the process is still alive 175 | :Trap 0 ⋄ ⍎onExit ⋄ :EndTrap ⍝ Try this 176 | 177 | count←0 178 | :While ~HasExited 179 | {}⎕DL interval 180 | count←count+1 181 | :Until count>limit 182 | :EndIf ⍝ OK, have it your own way 183 | 184 | {}Kill Proc 185 | ∇ 186 | 187 | ∇ r←GetCurrentProcessId;t 188 | :Access Public Shared 189 | :If IsWin 190 | r←⍎'t'⎕NA'U4 kernel32|GetCurrentProcessId' 191 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 192 | r←Proc.Pid 193 | :Else 194 | r←tonum⊃_SH'echo $PPID' 195 | :EndIf 196 | ∇ 197 | 198 | ∇ r←GetCurrentExecutable;⎕USING;t;gmfn 199 | :Access Public Shared 200 | :If IsWin 201 | r←'' 202 | :Trap 0 203 | 'gmfn'⎕NA'U4 kernel32|GetModuleFileName* P =T[] U4' 204 | r←⊃⍴/gmfn 0(1024⍴' ')1024 205 | :EndTrap 206 | :If 0∊⍴r 207 | ⎕USING←UsingSystemDiagnostics 208 | r←2 ⎕NQ'.' 'GetEnvironment' 'DYALOG' 209 | r←r,(~(¯1↑r)∊'\/')/'/' ⍝ Add separator if necessary 210 | r←r,(Diagnostics.Process.GetCurrentProcess.ProcessName),'.exe' 211 | :EndIf 212 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 213 | ∘∘∘ ⍝ Not supported 214 | :Else 215 | t←⊃1↓_SH'ps -o args -p ',⍕GetCurrentProcessId ⍝ AWS 216 | :If '"'''∊⍨⊃t ⍝ if command begins with ' or " 217 | r←{⍵/⍨{∧\⍵∨≠\⍵}⍵=⊃⍵}t 218 | :Else 219 | r←{⍵↑⍨¯1+1⍳⍨(¯1↓0,⍵='\')<⍵=' '}t ⍝ otherwise find first non-escaped space (this will fail on files that end with '\\') 220 | :EndIf 221 | :EndIf 222 | ∇ 223 | 224 | ∇ r←RunTimeName exe 225 | ⍝ Assumes that: 226 | ⍝ Windows runtime ends in "rt.exe" 227 | ⍝ *NIX runtime ends in ".rt" 228 | r←exe 229 | :If IsWin 230 | :If 'rt.exe'≢¯6↑{('rt.ex',⍵)[⍵⍳⍨'RT.EX',⍵]}exe ⍝ deal with case insensitivity 231 | r←'rt.exe',⍨{(~∨\⌽<\⌽'.'=⍵)/⍵}exe 232 | :EndIf 233 | :Else 234 | r←exe,('.rt'≢¯3↑exe)/'.rt' 235 | :EndIf 236 | ∇ 237 | 238 | 239 | ∇ r←KillChildren Exe;kids;⎕USING;p;m;i;mask 240 | :Access Public Shared 241 | ⍝ returns [;1] pid [;2] process name of any processes that were not killed 242 | r←0 2⍴0 '' 243 | :If ~0∊⍴kids←ListProcesses Exe ⍝ All child processes using the exe 244 | :If IsWin 245 | ⎕USING←UsingSystemDiagnostics 246 | p←Diagnostics.Process.GetProcessById¨kids[;1] 247 | p.Kill 248 | ⎕DL 1 249 | :If 0≠⍴p←(~p.HasExited)/p 250 | ⎕DL 1 251 | p.Kill 252 | ⎕DL 1 253 | :If ∨/m←~p.HasExited 254 | r←(kids[;1]∊m/p.Id)⌿kids 255 | :EndIf 256 | :EndIf 257 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 258 | ∘∘∘ 259 | :Else 260 | mask←(⍬⍴⍴kids)⍴0 261 | :For i :In ⍳⍴mask 262 | mask[i]←Shoot kids[i;1] 263 | :EndFor 264 | r←(~mask)⌿kids 265 | :EndIf 266 | :EndIf 267 | ∇ 268 | 269 | ∇ r←{all}ListProcesses procName;me;⎕USING;procs;unames;names;name;i;pn;kid;parent;mask;n;cmd;t 270 | :Access Public Shared 271 | ⍝ returns either my child processes or all processes 272 | ⍝ procName is either '' for all children, or the name of a process 273 | ⍝ r[;1] - child process number (Id) 274 | ⍝ r[;2] - child process name 275 | me←GetCurrentProcessId 276 | r←0 2⍴0 '' 277 | procName←,procName 278 | all←{6::⍵ ⋄ all}0 ⍝ default to just my childen 279 | 280 | :If IsWin 281 | ⎕USING←UsingSystemDiagnostics 282 | 283 | :If 0∊⍴procName ⋄ procs←Diagnostics.Process.GetProcesses'' 284 | :Else ⋄ procs←Diagnostics.Process.GetProcessesByName⊂procName ⋄ :EndIf 285 | :If all 286 | r←↑procs.(Id ProcessName) 287 | r⌿⍨←r[;1]≠me 288 | :Else 289 | :If 0<⍴procs 290 | unames←∪names←procs.ProcessName 291 | :For name :In unames 292 | :For i :In ⍳n←1+.=(,⊂name)⍳names 293 | pn←name,(n≠1)/'#',⍕i 294 | :Trap 0 ⍝ trap here just in case a process disappeared before we get to it 295 | parent←⎕NEW Diagnostics.PerformanceCounter('Process' 'Creating Process Id'pn) 296 | :If me=parent.NextValue 297 | kid←⎕NEW Diagnostics.PerformanceCounter('Process' 'Id Process'pn) 298 | r⍪←(kid.NextValue)name 299 | :EndIf 300 | :EndTrap 301 | :EndFor 302 | :EndFor 303 | :EndIf 304 | :EndIf 305 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 306 | ∘∘∘ 307 | :Else ⍝ Linux 308 | ⍝ unfortunately, Ubuntu (and perhaps others) report the PPID of tasks started via ⎕SH as 1 309 | ⍝ so, the best we can do at this point is identify processes that we tagged with APLppid= 310 | cmd←'ps -eo pid,args | sed -n ''2,$p''' ⍝ list process id and command line (with arguments) 311 | cmd,←(~all)/' | grep APLppid=',⍕me ⍝ is not selecting all, limit to APLProcess's my process started 312 | cmd,←(t←~0∊⍴procName)/' | grep ',procName ⍝ limit to entries with procName if it exists 313 | cmd,←' | grep -v grep' ⍝ remove "grep" entries 314 | procs←_SH cmd 315 | procs←↑(2 part deb)¨procs 316 | procs[;1]←(⊃tonum)¨procs[;1] 317 | procs⌿⍨←me≠procs[;1] ⍝ remove my task 318 | mask←1 319 | :If t 320 | mask←∨/¨(procName,' ')∘⍷¨procs[;2],¨' ' 321 | :EndIf 322 | r←mask⌿procs 323 | :EndIf 324 | ∇ 325 | 326 | ∇ r←Kill;delay 327 | :Access Public Instance 328 | r←0 ⋄ delay←0.1 329 | :Trap 0 330 | :If IsWin 331 | :If IsNetCore ⋄ Proc.Kill ⍬ ⋄ :Else ⋄ Proc.Kill ⋄ :EndIf ⍝ In .Net Core, Kill takes an argument 332 | :Repeat 333 | ⎕DL delay×~Proc.HasExited 334 | delay+←delay 335 | :Until (delay>10)∨Proc.HasExited 336 | :ElseIf {2::0 ⋄ IsSsh}'' 337 | ∘∘∘ 338 | :Else ⍝ Local UNIX 339 | {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt 340 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 341 | :If ~Proc.HasExited←~UNIXIsRunning Proc.Id 342 | {}UNIXIssueKill 9 Proc.Id ⍝ issue strong interrupt 343 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 344 | :AndIf ~Proc.HasExited←~UNIXIsRunning Proc.Id 345 | :Repeat 346 | ⎕DL delay 347 | delay+←delay 348 | :Until (delay>10)∨Proc.HasExited←~UNIXIsRunning Proc.Id 349 | :EndIf 350 | :EndIf 351 | r←Proc.HasExited 352 | :EndTrap 353 | ∇ 354 | 355 | ∇ r←Shoot Proc;MAX;res 356 | MAX←100 357 | r←0 358 | :If 0≠⎕NC⊂'Proc.HasExited' 359 | :Repeat 360 | :If ~Proc.HasExited 361 | :If IsWin 362 | :If IsNetCore ⋄ Proc.Kill ⍬ ⋄ :Else ⋄ Proc.Kill ⋄ :EndIf 363 | ⎕DL 0.2 364 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 365 | ∘∘∘ 366 | :Else 367 | {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt AWS 368 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 369 | Proc.HasExited←~UNIXIsRunning Proc.Id ⍝ AWS 370 | :EndIf 371 | :EndIf 372 | MAX-←1 373 | :Until Proc.HasExited∨MAX≤0 374 | r←Proc.HasExited 375 | :ElseIf 2=⎕NC'Proc' ⍝ just a process id? 376 | {}UNIXIssueKill 9 Proc 377 | {}⎕DL 2 378 | r←~UNIXIsRunning Proc 379 | :EndIf 380 | ∇ 381 | 382 | ∇ r←HasExited 383 | :Access public instance 384 | :If IsWin∨{2::0 ⋄ IsSsh}'' 385 | r←{0::⍵ ⋄ Proc.HasExited}1 386 | :Else 387 | Proc.HasExited←r←~UNIXIsRunning Proc.Id ⍝ AWS 388 | :EndIf 389 | ∇ 390 | 391 | ∇ r←GetExitCode 392 | :Access public Instance 393 | ⍝ *** EXPERIMENTAL *** 394 | ⍝ query exit code of process. Attempt to do it in a cross platform way relying on .Net Core. Unfortunetaly 395 | ⍝ we only use it on Windows atm, so this method can only be used on Windows. 396 | r←'' ⍝ '' indicates "can't check" (for example, because it is still running) or non-windows platform 397 | :If HasExited 398 | :If IsWin 399 | r←Proc.ExitCode 400 | :Else 401 | :EndIf 402 | :EndIf 403 | ∇ 404 | 405 | ∇ r←IsRunning args;⎕USING;start;exe;pid;proc;diff;res 406 | :Access public shared 407 | ⍝ args - pid {exe} {startTS} 408 | r←0 409 | args←eis args 410 | (pid exe start)←3↑args,(⍴args)↓0 ''⍬ 411 | :If IsWin 412 | ⎕USING←UsingSystemDiagnostics 413 | :Trap 0 414 | proc←Diagnostics.Process.GetProcessById pid 415 | r←~proc.HasExited 416 | :Else 417 | :Return 418 | :EndTrap 419 | :If ''≢exe 420 | r∧←exe≡proc.ProcessName 421 | :EndIf 422 | :If ⍬≢start 423 | :Trap 90 424 | diff←|-/DateToIDN¨start(proc.StartTime.(Year Month Day Hour Minute Second Millisecond)) 425 | r∧←diff≤24 60 60 1000⊥0 1 0 0÷×/24 60 60 1000 ⍝ consider it a match within a 1 minute window 426 | :Else 427 | r←0 428 | :EndTrap 429 | :EndIf 430 | :ElseIf {2::0 ⋄ IsSsh}'' 431 | ∘∘∘ 432 | :Else 433 | r←UNIXIsRunning pid 434 | :EndIf 435 | ∇ 436 | 437 | ∇ r←Stop pid;proc 438 | :Access public shared 439 | ⍝ attempts to stop the process with processID pid 440 | :If IsWin 441 | ⎕USING←UsingSystemDiagnostics 442 | :Trap 0 443 | proc←Diagnostics.Process.GetProcessById pid 444 | :Else 445 | r←1 446 | :Return 447 | :EndTrap 448 | :If IsNetCore ⋄ proc.Kill ⍬ ⋄ :Else ⋄ proc.Kill ⋄ :EndIf 449 | {}⎕DL 0.5 450 | r←~##.APLProcess.IsRunning pid 451 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 452 | ∘∘∘ 453 | :Else 454 | {}UNIXIssueKill 3 pid ⍝ issue strong interrupt 455 | :EndIf 456 | ∇ 457 | 458 | ∇ r←UNIXIsRunning pid;txt 459 | :Access public shared 460 | ⍝ Return 1 if the process is in the process table and is not a defunct 461 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 462 | ∘∘∘ 463 | :Else 464 | →(r←' '∨.≠txt←⊃_SH'ps -o comm -p ',(⍕pid),' | sed -n ''2,$p''')↓0 465 | r←~∨/''⍷txt 466 | :EndIf 467 | ∇ 468 | 469 | ∇ {r}←UNIXIssueKill(signal pid) 470 | signal pid←⍕¨signal pid 471 | cmd←'kill -',signal,' ',pid,' >/dev/null 2>&1 ; echo $?' 472 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 473 | ∘∘∘ 474 | :Else 475 | r←⎕SH cmd 476 | :EndIf 477 | ∇ 478 | 479 | ∇ r←UNIXGetShortCmd pid 480 | ⍝ Retrieve short form of cmd used to start process 481 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 482 | ∘∘∘ 483 | :Else 484 | :Trap 11 485 | r←⊃1↓_SH'ps -o comm -p ',⍕pid 486 | :Else 487 | r←'' 488 | :EndTrap 489 | :EndIf 490 | ∇ 491 | 492 | ∇ r←{quietly}_SH cmd 493 | :Access public shared 494 | :If 0=⎕NC'quietly' ⋄ quietly←0 ⋄ :EndIf 495 | :If quietly 496 | cmd←cmd,' &1' 497 | :EndIf 498 | r←{0::'' ⋄ ⎕SH ⍵}cmd 499 | ∇ 500 | 501 | :Class Time 502 | :Field Public Year 503 | :Field Public Month 504 | :Field Public Day 505 | :Field Public Hour 506 | :Field Public Minute 507 | :Field Public Second 508 | :Field Public Millisecond 509 | 510 | ∇ make ts 511 | :Implements Constructor 512 | :Access Public 513 | (Year Month Day Hour Minute Second Millisecond)←7↑ts 514 | ⎕DF(⍕¯2↑'00',⍕Day),'-',((12 3⍴'JanFebMarAprMayJunJulAugSepOctNovDec')[⍬⍴Month;]),'-',(⍕100|Year),' ',1↓⊃,/{':',¯2↑'00',⍕⍵}¨Hour Minute Second 515 | ∇ 516 | 517 | :EndClass 518 | 519 | ∇ r←ProcessUsingPort port;t 520 | ⍝ return the process ID of the process (if any) using a port 521 | :Access public shared 522 | r←⍬ 523 | :If IsWin 524 | :If ~0∊⍴t←_SH'netstat -a -n -o' 525 | :AndIf ~0∊⍴t/⍨←∨/¨'LISTENING'∘⍷¨t 526 | :AndIf ~0∊⍴t/⍨←∨/¨((':',⍕port),' ')∘⍷¨t 527 | r←∪∊¯1↑¨(//)∘⎕VFI¨t 528 | :EndIf 529 | :Else 530 | :If ~0∊⍴t←_SH'netstat -l -n -p 2>/dev/null | grep '':',(⍕port),' ''' 531 | r←∪∊{⊃(//)⎕VFI{(∧\⍵∊⎕D)/⍵}⊃¯1↑{⎕ML←3 ⋄ (' '≠⍵)⊂⍵}⍵}¨t 532 | :EndIf 533 | :EndIf 534 | ∇ 535 | 536 | ∇ r←MyDNSName;GCN 537 | :Access Public Shared 538 | 539 | :If IsWin 540 | 'GCN'⎕NA'I4 Kernel32|GetComputerNameEx* U4 >0T =U4' 541 | r←2⊃GCN 7 255 255 542 | :Return 543 | ⍝ ComputerNameNetBIOS = 0 544 | ⍝ ComputerNameDnsHostname = 1 545 | ⍝ ComputerNameDnsDomain = 2 546 | ⍝ ComputerNameDnsFullyQualified = 3 547 | ⍝ ComputerNamePhysicalNetBIOS = 4 548 | ⍝ ComputerNamePhysicalDnsHostname = 5 549 | ⍝ ComputerNamePhysicalDnsDomain = 6 550 | ⍝ ComputerNamePhysicalDnsFullyQualified = 7 <<< 551 | ⍝ ComputerNameMax = 8 552 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 553 | ∘∘∘ ⍝ Not supported 554 | :Else 555 | r←⊃_SH'hostname' 556 | :EndIf 557 | ∇ 558 | 559 | ∇ idn←DateToIDN ts 560 | :Access Public Shared 561 | idn←(2 ⎕NQ'.' 'DateToIDN'(3↑ts))+(24 60 60 1000⊥4↑3↓ts)÷86400000 562 | ∇ 563 | 564 | ∇ Proc←SshProc(host user keyfile cmd);conn;z;kf;allpids;guid;listpids;pids;⎕USING;pid;tid 565 | ⎕USING←'Renci.SshNet,',PATH,'/Renci.SshNet.dll' 566 | kf←⎕NEW PrivateKeyFile(,⊂keyfile) 567 | conn←⎕NEW SshClient(host 22 user(,kf)) 568 | 569 | :Trap 0 570 | conn.Connect ⍝ This is defined to be a void() 571 | :Case 90 ⋄ ('Error creating ssh client instance: ',⎕EXCEPTION.Message)⎕SIGNAL 11 572 | :Else ⋄ 'Unexpected error creating ssh client instance'⎕SIGNAL 11 573 | :EndTrap 574 | 575 | listpids←{0~⍨2⊃(⎕UCS 10)⎕VFI(conn.RunCommand⊂'ps -u ',user,' | grep dyalog | grep -v grep | awk ''{print $2}''').Result} 576 | guid←'dyalog-ssh-',(⍕⎕TS)~' ' 577 | pids←listpids ⍬ 578 | Proc←⎕NS'' 579 | Proc.SshConn←conn 580 | Proc.HasExited←0 581 | tid←{SshRun conn ⍵ Proc}&⊂cmd 582 | Proc.tid←tid 583 | ⎕DL 1 584 | :If 1=⍴pid←(listpids ⍬)~pids ⋄ pid←⊃pid 585 | :Else ⋄ ∘∘∘ ⋄ :EndIf ⍝ failed to start 586 | Proc.Pid←pid 587 | ∇ 588 | 589 | ∇ SshRun(conn cmd proc) 590 | ⍝ Wait until APL exits, then set HasExited←1 591 | conn.RunCommand cmd 592 | proc.HasExited←1 593 | ∇ 594 | 595 | :EndClass 596 | --------------------------------------------------------------------------------