├── .gitignore ├── tests ├── rpc.dws ├── waiter.dws ├── assertFail.dyalog ├── RPCInit.aplf ├── assert.dyalog ├── RPCDo.aplf ├── setup.dyalog ├── test_APLProcess.dyalog └── LoadConga.aplf ├── docs ├── img │ ├── avatar.png │ ├── SquidError.PNG │ ├── favicon-32.png │ └── dyalog-white.svg ├── assets │ └── apl385.ttf ├── release-notes.md ├── LICENSE.md ├── css │ └── main.css ├── index.md ├── userguide.md ├── samples.md ├── shared-methods.md └── instance.md ├── samples └── rpc │ ├── rpc.dws │ ├── GetEnv.aplf │ ├── Log.aplf │ ├── LX.aplf │ ├── Stop.aplf │ ├── Do.aplf │ ├── CongaInit.aplf │ ├── Start.aplf │ └── Server.aplf ├── README.md ├── apl-package.json ├── LICENSE ├── mkdocs.yml └── source └── APLProcess.dyalog /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | -------------------------------------------------------------------------------- /tests/rpc.dws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/tests/rpc.dws -------------------------------------------------------------------------------- /tests/waiter.dws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/tests/waiter.dws -------------------------------------------------------------------------------- /docs/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/docs/img/avatar.png -------------------------------------------------------------------------------- /samples/rpc/rpc.dws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/samples/rpc/rpc.dws -------------------------------------------------------------------------------- /docs/assets/apl385.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/docs/assets/apl385.ttf -------------------------------------------------------------------------------- /docs/img/SquidError.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/docs/img/SquidError.PNG -------------------------------------------------------------------------------- /docs/img/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dyalog/APLProcess/main/docs/img/favicon-32.png -------------------------------------------------------------------------------- /tests/assertFail.dyalog: -------------------------------------------------------------------------------- 1 | assertFail msg 2 | :If ~0∊⍴msg 3 | 223 ⎕SIGNAL⍨⎕←msg 4 | :EndIf 5 | -------------------------------------------------------------------------------- /samples/rpc/GetEnv.aplf: -------------------------------------------------------------------------------- 1 | r←GetEnv name 2 | ⍝ retrieve value of environment variable "name" 3 | r←2 ⎕NQ'.' 'GetEnvironment'name 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APLProcess 2 | 3 | APLProcess is a utility to launch a new instance of Dyalog APL in another process. 4 | 5 | Documentation can be found at . 6 | -------------------------------------------------------------------------------- /samples/rpc/Log.aplf: -------------------------------------------------------------------------------- 1 | Log msg 2 | ⍝ All intentional RPC output uses Log 3 | ⍝ Log can be modified to send output to a file for debugging, especially when using a runtime interpreter 4 | ⎕←msg 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/release-notes.md: -------------------------------------------------------------------------------- 1 | ## Version 2.4 2 | * Added `Load` and `Lx` parameters 3 | 4 | ## Version 2.3 5 | * First version of `APLProcess` residing in its own repository. 6 | * Added `Id` property 7 | * Allow a namespace as constructor argument -------------------------------------------------------------------------------- /samples/rpc/LX.aplf: -------------------------------------------------------------------------------- 1 | LX 2 | :If 0∊⍴GetEnv'RPC_Port' 3 | Log'Sample Remote Procedure Call (RPC) application demonstrating the use of APLProcess' 4 | Log'Use Start '''' to start the application with default settings.' 5 | :Else 6 | Server'' 7 | :EndIf 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /samples/rpc/Stop.aplf: -------------------------------------------------------------------------------- 1 | Stop;killed 2 | ⍝ Stops RPC and cleans up 3 | :If 0≠⎕NC'RPCClt' 4 | {0::{}RPCClt.Close'.'}'' 5 | ⎕EX'RPCClt' 6 | Log'RPC Client closed and expunged' 7 | :EndIf 8 | :If 9=⎕NC'RPC_Process' 9 | killed←RPC_Process.Kill 10 | Log'RPC Server process ',(4×killed)↓'NOT terminated' 11 | ⎕EX killed/'RPC_Process' 12 | :EndIf 13 | :If 9=⎕NC'Conga' 14 | :If 0∊⍴Conga.RootNames 15 | Conga.UnloadSharedLib 16 | ⎕EX'Conga' 17 | Log'Conga shared library unloaded and Conga was erased' 18 | :EndIf 19 | :EndIf 20 | -------------------------------------------------------------------------------- /samples/rpc/Do.aplf: -------------------------------------------------------------------------------- 1 | r←Do stmt;rc;obj;evt;data;resp 2 | ⍝ send an RPC statement to be executed remotely 3 | :If 0=⎕NC'RPCClt' 4 | :OrIf {0::1 ⋄ 0≠⊃RPCClt.Describe'.'}'' 5 | 'RPC is not initialized. Use ∇Run'⎕SIGNAL 223 6 | :EndIf 7 | :If 0≠⊃rc←RPCClt.Send'RPCClient'stmt 8 | ('Error sending "',stmt,'": ',⍕rc)⎕SIGNAL 224 9 | :EndIf 10 | :If 0≠⊃(rc obj evt data)←4↑resp←RPCClt.Wait'RPCClient' 10000 ⍝ wait up to 10 seconds for response 11 | :OrIf 'Timeout'≡evt 12 | ('Error waiting on "',stmt,'": ',⍕resp)⎕SIGNAL 225 13 | :EndIf 14 | 15 | r←data ⍝ data is (⎕DM if error occurred)(result of ⍎stmt) 16 | -------------------------------------------------------------------------------- /apl-package.json: -------------------------------------------------------------------------------- 1 | { 2 | api: "APLProcess", 3 | assets: "", 4 | description: "Utility to start APL processes", 5 | documentation: "https://dyalog.github.io/APLProcess", 6 | files: "", 7 | group: "dyalog", 8 | io: 1, 9 | license: "MIT", 10 | lx: "", 11 | maintainer: "", 12 | minimumAplVersion: "17.1", 13 | ml: 1, 14 | name: "APLProcess", 15 | os_lin: 1, 16 | os_mac: 1, 17 | os_win: 1, 18 | project_url: "https://github.com/Dyalog/APLProcess", 19 | source: "source/APLProcess.dyalog", 20 | tags: "mac-os,windows,linux,dyalog,pi", 21 | userCommandScript: "", 22 | version: "2.4.0", 23 | } 24 | -------------------------------------------------------------------------------- /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) 2024 Dyalog 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 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dyalog 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 | :EndIf 16 | :Else 17 | :Trap 11 18 | 'Conga'##.⎕CY'conga' 19 | ##.DRC←##.Conga.Init'' 20 | rc←0 21 | :Else 22 | :Trap 11 23 | 'DRC'##.⎕CY'conga' 24 | ##.DRC.Init'' 25 | rc←0 26 | :Else 27 | ⎕←'Could not copy Conga or DRC from the Conga workspace' 28 | :EndTrap 29 | :EndTrap 30 | :EndIf 31 | :If 0≠rc←{0::1 ⋄ ⊃##.DRC.Describe'.'}'' 32 | ⎕←'Could not initialize Conga' 33 | :EndIf 34 | :Else 35 | rc←-⎕EN 36 | ⎕←'Unexpected error attempting to load Conga' 37 | ⎕←↑⎕DM 38 | :EndTrap 39 | -------------------------------------------------------------------------------- /samples/rpc/CongaInit.aplf: -------------------------------------------------------------------------------- 1 | (rc msg)←CongaInit rootname;r;⎕IO;⎕ML;path;ws 2 | (⎕IO ⎕ML)←1 3 | 4 | rc←¯1 ⋄ msg←'' 5 | 6 | :Select ⎕NC⊂,rootname 7 | :Case 0 ⍝ if rootname doesn't exist 8 | :If 0=⎕NC'Conga' ⍝ does Conga exist? 9 | path←(⊃1 ⎕NPARTS⊃2 ⎕NQ'.' 'GetCommandLineArgs'),'/ws/' ⍝ use full path as runtime interpreters do not necessarily have WSPath set 10 | :Trap 11 11 | 'Conga'⎕CY ws←path,'conga' ⍝ copy in Conga 12 | :Else 13 | msg←'Could not copy Conga from "',ws,'" workspace due to ',⎕DMX.(EM,': ',Message) 14 | →0 15 | :EndTrap 16 | :EndIf 17 | 18 | :Trap 999 ⍝ 999 signalled by Conga.Init 19 | ⍎rootname,'←Conga.Init ''',rootname,'''' 20 | msg←'Conga root "',rootname,'" initialized' 21 | :Else 22 | msg←'Could not initialize Conga root "',rootname,'" due to ',⎕DMX.(EM,': ',Message) 23 | →0 24 | :EndTrap 25 | :If 0≠⊃r←(⍎rootname).SetProp'.' 'EventMode' 1 26 | msg←'Error setting EventMode on "',rootname,'": ',∊⍕(4↑r)[1 3 4] 27 | →0 28 | :EndIf 29 | rc←0 30 | :Case 9.2 ⍝ root already exists 31 | :If 0≠rc←{0::¯1 ⋄ ⊃((⍎⍵).Describe'.')}rootname 32 | msg←'"',rootname,'" is not properly initialized' 33 | :EndIf 34 | :Else 35 | msg←'"',rootname,'" is not an instance of Conga.LIB?' 36 | :EndSelect 37 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: APL; 3 | src: local("APL385 Unicode"), url("../assets/apl385.ttf"); 4 | } 5 | .language-APL { 6 | font-family: APL!important; 7 | line-height: 1.2em!important; 8 | } 9 | code { 10 | font-family: APL; 11 | font-size: 1em!important; 12 | padding-left: 0em!important; 13 | padding-right: 0em!important; 14 | white-space: pre-wrap; 15 | } 16 | .md-logo > img{ 17 | width: 7rem!important; 18 | height: 1.2rem!important; 19 | } 20 | @media screen and (max-width: 76.1875em) { 21 | .md-logo > img{ 22 | width: 10rem!important; 23 | height: 1.8rem!important; 24 | } 25 | } 26 | td:first-child { 27 | white-space: nowrap; 28 | } 29 | table.scrollable { 30 | display:block; 31 | overflow-x: auto; 32 | white-space: nowrap; 33 | } 34 | 35 | /* Custom colors */ 36 | :root { 37 | --md-primary-fg-color: #ED7F00; 38 | --md-primary-fg-color--dark: #563336; 39 | --md-default-bg-color: #F8F8F8; 40 | } 41 | /* Code copy only input (see CONTRIBUTING.md) */ 42 | pre + pre > button, pre + hr { 43 | display: none!important; 44 | } 45 | pre > code { 46 | padding-bottom: 1em!important; 47 | } 48 | hr + pre > button { 49 | top: -0.2em!important; 50 | } 51 | pre + pre , hr + pre { 52 | margin-top: -1.8em!important; 53 | } 54 | pre + pre > code, hr + pre > code { 55 | padding-top: 0.2em!important; 56 | } 57 | 58 | /* make admonition styling a la Dyalog */ 59 | .md-typeset .admonition, 60 | .md-typeset details { 61 | border-color: #ED7F00 !important; 62 | box-shadow: #ED7F00 !important; 63 | } 64 | 65 | .md-typeset .admonition-title { 66 | background-color: #F3AD5b !important; 67 | } 68 | 69 | .md-typeset .admonition-title:before, 70 | .md-typeset summary:before{ 71 | background-color: #F8F8F8 !important; 72 | } 73 | 74 | .md-typeset pre > code { 75 | overflow: scroll !important; 76 | scrollbar-color: black; 77 | scrollbar-width: thin; 78 | } 79 | 80 | .md-copyright { 81 | width: 100%; 82 | } 83 | 84 | .md-typeset dd { 85 | margin-top: 0em; 86 | } 87 | 88 | .left { 89 | float: left; 90 | } 91 | 92 | .right { 93 | float: right; 94 | } -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: APLProcess 2 | repo_url: https://github.com/dyalog/APLProcess 3 | repo_name: dyalog/APLProcess 4 | dev_addr: 127.0.0.1:22222 5 | copyright:
Made with Material for MkDocs
Contents copyright ©2015-2024 Dyalog, LTD
6 | nav: 7 | - Overview: 'index.md' # complete 8 | - 'User Guide': 'userguide.md' 9 | - Reference: 10 | - 'APLProcess Instance': 'instance.md' 11 | - 'Shared Methods': 'shared-methods.md' 12 | - Samples: 13 | - 'Sample Uses': 'samples.md' 14 | - About: 15 | - License: 'LICENSE.md' # complete 16 | - 'Release Notes': 'release-notes.md' 17 | 18 | theme: 19 | favicon: 'img/favicon-32.png' 20 | logo: 'img/dyalog-white.svg' 21 | features: 22 | - navigation.sections 23 | - navigation.instant 24 | name: material 25 | 26 | extra: 27 | generator: false 28 | version: 29 | provider: mike 30 | 31 | extra_css: 32 | - css/main.css 33 | 34 | plugins: 35 | - search 36 | - print-site: 37 | add_to_navigation: true 38 | print_page_title: 'Print' 39 | add_print_site_banner: false 40 | # Table of contents 41 | add_table_of_contents: false 42 | toc_title: 'Table of Contents' 43 | toc_depth: 6 44 | # Content-related 45 | add_full_urls: false 46 | enumerate_headings: false 47 | enumerate_figures: false 48 | add_cover_page: true 49 | cover_page_template: "" 50 | path_to_pdf: "" 51 | include_css: true 52 | enabled: true 53 | exclude: 54 | 55 | markdown_extensions: 56 | - admonition 57 | - abbr 58 | - footnotes 59 | - attr_list 60 | - def_list 61 | - markdown_tables_extended 62 | - pymdownx.tasklist: 63 | custom_checkbox: true 64 | - pymdownx.emoji: 65 | emoji_index: !!python/name:material.extensions.emoji.twemoji 66 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 67 | - toc: 68 | title: On this page -------------------------------------------------------------------------------- /samples/rpc/Start.aplf: -------------------------------------------------------------------------------- 1 | (rc msg)←Start args;rc;msg;port;threaded;runtime;rideable;detach;⎕ML;⎕IO 2 | ⍝ simple Remote Procedure Call (RPC) example using APLProcess 3 | ⍝ args is: 4 | ⍝ [1] the port for the child process to start a command mode Conga server on (default 8888) 5 | ⍝ [2] should the Conga listening loop in the child process run in a separate thread? (default 0 = no) 6 | ⍝ [3] should the child process allow RIDE connections? either a Boolean (0/1) or a port number is allowed (default 0 = no RIDE) 7 | ⍝ [4] should the child process terminate when the APLProcess instance goes away? (default 0) 8 | ⍝ [5] use a runtime interpreter? (default 0 = no) 9 | ⍝ 10 | (⎕IO ⎕ML)←1 11 | (rc msg)←¯1 '' 12 | :If 0≠⊃(rc msg)←CongaInit'RPCClt' 13 | msg←'Could not initialize Conga due to: ',∊⍕msg 14 | →0 15 | :EndIf 16 | 17 | (port threaded rideable detach runtime)←5↑args,(≢args)↓8888 0 0 0 0 18 | :If threaded∧runtime 19 | msg←'Cannot run threaded listener with a runtime interpreter' 20 | →0 21 | :EndIf 22 | 23 | ⍝ create and configure the instance of APLProcess 24 | RPC_Process←APLProcess.New'' 25 | RPC_Process.Ws←⎕WSID ⍝ workspace to load 26 | RPC_Process.Args←'RPC_Port=',⍕port ⍝ port to use 27 | RPC_Process.Args,←' RPC_Threaded=',⍕threaded ⍝ run the server in a thread in the child process? 28 | RPC_Process.RunTime←runtime ⍝ use runtime interpreter? 29 | RPC_Process.RideInit←''(port+1)rideable⊃⍨0 1⍸rideable ⍝ RIDE port 30 | RPC_Process.Detach←~detach ⍝ don't kill child process when p goes away 31 | 32 | ⍝ try to launch the child process 33 | :If 0≠⊃(rc msg)←RPC_Process.Run 34 | msg←'Failed to start APL process due to: ',∊⍕msg 35 | →0 36 | :EndIf 37 | 38 | ⎕DL 2 ⍝ pause to let child process initialize 39 | :If ~RPC_Process.HasExited ⍝ check to make sure the child process is there 40 | :If 0=⊃rc←RPCClt.Clt'RPCClient' 'localhost'port'Command' 41 | msg←'Connected to RPC Server on port ',⍕port 42 | msg,←rideable/(⎕UCS 13),'RIDE available on port ',⍕1+port 43 | rc←0 44 | :Else 45 | msg←'Could not connect to RPC Server on port ',(⍕port),' due to ',∊⍕(4↑rc)[1 3 4] 46 | :EndIf 47 | :EndIf 48 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | `APLProcess` is a cross-platform utility that enables you to easily start additional processes running Dyalog APL. This can be useful in a number of scenarios including: 2 | 3 | * to distribute the processing load across multiple processors. This is what the isolate workspace does. 4 | * to delegate processing to another process so that if there is some sort of failure, the primary process is not impacted. 5 | 6 | `APLProcess` runs on Windows, Linux and MacOS. ssh support, which will enable you to start an APL process on another platform using the ssh protocol, is forthcoming. 7 | 8 | ## Terminology 9 | Parent Process 10 | : This is the process from where `APLProcess` is used to start "child" processes. 11 | 12 | Child Process 13 | : This is a process started by a "parent" process. Each child process has a reference back to the "parent" process in the form of a "parent process ID" (PPID). 14 | 15 | !!! note 16 | While `APLProcess` itself is `⎕IO` and `⎕ML` insensitive, the examples in this documentation assume an environment of `(⎕IO ⎕ML)←1`. 17 | 18 | ## Obtaining `APLProcess` 19 | `APLProcess` is included with your Dyalog APL installation. To bring it into your workspace: 20 | 21 | ### Dyalog APL Version 19.0 and later (using `Link`) 22 | ```APL 23 | ]import [ns] APLProcess 24 | ``` 25 | or, under program control, do: 26 | ```APL 27 | ⎕SE.Link.Import [ns] 'APLProcess' 28 | ``` 29 | where `ns` is an optional namespace name or reference in which to load `APLProcess`. If `ns` is not specified, `APLProcess` will be loaded into the current namespace. 30 | 31 | ### Dyalog APL versions before 19.0 (using `SALT`) 32 | ```APL 33 | ]load APLProcess [-target=ns] 34 | ``` 35 | or, under program control, do: 36 | ```APL 37 | ⎕SE.SALT.Load 'APLProcess [-target=ns]' 38 | ``` 39 | `-target=ns` optionally specifies the namespace in which to load `APLProcess` where `ns` is the namespace name. If not specified, `APLProcess` will be loaded into the current namespace. 40 | 41 | ## Upgrading to the Latest `APLProcess` 42 | The documentation describes the latest released version of `APLProcess` which may be more recent than the version that came pre-installed with your Dyalog APL. Use `APLProcess.Version` to check the `APLProcess` version number. 43 | 44 | The latest released version of `APLProcess` can be downloaded from the Assets section of [the latest release page](https://github.com/dyalog/aplprocess/releases/latest) on GitHub. 45 | 46 | ## `APLProcess` is Available as a Tatin Package 47 | [Tatin](https://tatin.dev) is a package manager for APL-based packages. `APLProcess` is available as a Tatin package. If you have the Tatin client installed, you can load `APLProcess` using: 48 | ```APL 49 | ]TATIN.LoadPackages APLProcess 50 | ``` 51 | The Tatin client will be included in your Dyalog installation beginning with Dyalog version 19.0. For earlier versions of Dyalog, refer to the [Tatin website](https://tatin.dev) for instructions on installing the Tatin client. -------------------------------------------------------------------------------- /samples/rpc/Server.aplf: -------------------------------------------------------------------------------- 1 | Server args;⎕IO;⎕ML;Port;rc;data;event;obj;wait;threaded;t;exec;msg;port 2 | ⍝ Light-weight RPC command mode server for testing APLProcess 3 | ⍝ args is [port] [threaded] 4 | ⍝ port is the port number on which to start the RPC server 5 | ⍝ threaded is a Boolean indicating whether to run the listener in a separate threaded 6 | ⍝ If args is empty, we look for environment variables "RPC_Port" and "RPC_Threaded" 7 | ⍝ If no port is defined either in args or via RPC_Port, the user has 10 seconds to press enter to keep APL from closing 8 | ⍝ 9 | ⍝ N.B. the client sends statements to execute via Conga "command" mode 10 | ⍝ no checking is done on the statements, so things like '→' or '⎕OFF' will have the expected effect 11 | 12 | (⎕IO ⎕ML)←1 13 | (port threaded)←2↑args,(≢args)↓⍬ 0 14 | :Trap 0 15 | :If 0∊⍴port ⍝ port supplied? 16 | :If 0∊⍴port←GetEnv'RPC_Port' ⍝ if not, is there an environment variable? 17 | Log'No port specified' ⍝ if not, bail out 18 | →∆END 19 | :EndIf 20 | threaded←⊃⊃(//)⎕VFI GetEnv'RPC_Threaded' 21 | :EndIf 22 | 23 | :If threaded≠2 ⍝ 2 indicates that we've started in a thread and jump to listening 24 | :If 0∊⍴Port←⊃(//)⎕VFI⍕port ⍝ do some minimal checking on the port 25 | Log'Port "',port,'" is not valid' 26 | →∆END 27 | :EndIf 28 | 29 | :If 0≠⊃(rc msg)←CongaInit'RPCSrv' 30 | Log'Could not initialize Conga due to: ',∊⍕msg 31 | →∆END 32 | :EndIf 33 | 34 | :If 0≠⊃rc←##.RPCSrv.Srv'RPCServer' ''Port'Command' ⍝ start the server 35 | Log'Unable to start RPC server: ',⍕rc 36 | →∆END 37 | :Else 38 | Log'Command Mode RPC Server started on port ',⍕port 39 | :EndIf 40 | ##.STOP←0 41 | :If threaded=1 42 | Server&Port 2 43 | →0 44 | :EndIf 45 | :EndIf 46 | 47 | exec←{0::(↑1 7 7↓¨⎕DM)(⍕⍵) ⋄ ''(##.⍎⍕⍵)} 48 | :While ~##.STOP 49 | (rc obj event data)←4↑wait←##.RPCSrv.Wait'RPCServer' 1000 ⍝ Time out now and again 50 | :Select rc 51 | :Case 0 52 | :Select event 53 | :Case 'Connect' ⍝ ignore 54 | :Case 'Receive' 55 | :If 0≠⊃rc←##.RPCSrv.Respond obj(exec data) 56 | Log'Error in RPCSrv.Respond: ',∊⍕rc 57 | →∆WAIT 58 | :EndIf 59 | :Case 'Timeout' 60 | :Else ⍝ Anything unexpected 61 | Log'Unexpected "',event,'" event in RPCSrv.Wait, closing connection.' 62 | {}##.RPCSrv.Close obj 63 | :EndSelect 64 | :Else 65 | Log'Error in RPCSrv.Wait: ',∊⍕wait 66 | :EndSelect 67 | :EndWhile 68 | {}##.RPCSrv.Close'RPCServer' 69 | Log'RPCServer terminated.' 70 | :Else ⍝ :Trap 71 | Log'Unexpected error:' 72 | Log(↑⎕DM) 73 | {0:: ⋄ ##.RPCSrv.Close'RPCServer'}⍬ 74 | :EndTrap 75 | 76 | ∆END: 77 | ⎕RTL←10 78 | ⍞←'Press Enter within 10 seconds to keep APL from closing...' 79 | :Trap 1006 80 | {}⍞ 81 | :Else 82 | ⎕OFF 83 | :EndTrap 84 | -------------------------------------------------------------------------------- /docs/userguide.md: -------------------------------------------------------------------------------- 1 | `APLProcess` starts a new Dyalog APL interpreter in another process. What that process does is entirely up to you. Common uses include: 2 | 3 | * setting up a Remote Procedure Call (RPC) service to distribute processing load between multiple APL processes. 4 | * delegating a long-running task to another APL process 5 | * delegating processing to another APL process to reduce the potential impact of code failure. If the code running in another process fails, it's less likely to impact the processing in the current APL session. 6 | 7 | !!! Note 8 | Currently `APLProcess` can start new APL processes only on the same platform. As most processors have multiple cores, this can still help distribute processing load and increase throughput. The ability to start new APL processes on other platforms using ssh is under development. 9 | 10 | ### Terminology 11 | Even though `APLProcess` is implemented as a Dyalog APL class, you do not need to know much about the object-oriented programming (OOP) features of Dyalog APL. 12 | 13 | Instance 14 | : An "instance" of `APLProcess` is created when you call `APLProcess.New` or the use the `⎕NEW` system function. Instances are independent of one another. 15 | 16 | Method 17 | : In OOP terminology, APL functions within classes are called "methods". An individual method can either apply only to an instance (this is called an "instance method"), or to the class itself (this is called a "shared method"). 18 | 19 | Field or Setting 20 | : In OOP terminology, variables are called "fields", but for this documentation, we'll refer to them as "settings". 21 | 22 | Throughout this document we will use the following terms with the following meanings: 23 | 24 | Parent Process 25 | : This is the process from where `APLProcess` is used to start "child" processes. 26 | 27 | Child Process 28 | : This is an APL process started by a "parent" process. Each child process has a reference back to the "parent" process in the form of a "parent process ID" (PPID). 29 | 30 | `p` 31 | : An instance of `APLProcess` which is the result of running `APLProcess.New` or using `⎕NEW`. Of course, you can use whatever name you prefer in your code. 32 | 33 | ### Creating and starting APLProcess 34 | There are three ways to create and run an instance of `APLProcess`. 35 | 36 | #### Use `⎕NEW` with constructor arguments 37 | This will start an APL process and return an `APLProcess` instance. 38 | ``` 39 | p←⎕NEW APLProcess ('/myApp' '' 0 'SERVE:*:4502') 40 | ``` 41 | 42 | #### Use `⎕NEW` without constructor arguments 43 | This will create and return an instance of `APLProcess`. You can then set whatever parameters you need and start the APL process using the `Run` method. 44 | ``` 45 | p←⎕NEW APLProcess 46 | p.(Ws RideInit)←'/myApp' 'SERVE:*:4502' 47 | p.Run 48 | ``` 49 | #### Use `APLProcess.New` 50 | `APLProcess.New` is a shared method that will create and return an instance of `APLProcess`, applying any arguments as parameters. You can then start the APL process using the `Run` method. 51 | Supplying `''` as the argument to `APLProcess.New` is equivalent of using `⎕NEW APLProcess` without any arguments. 52 | ``` 53 | p←APLProcess.New '/myApp' '' 0 'SERVE:*:4502' 54 | p.Run 55 | ``` 56 | 57 | ### Stopping an APL Process 58 | 59 | Use `p.Kill` to terminate the child APL process. `p.kill` will return `1` if the child process was successfully terminated, `0` otherwise. 60 | 61 | ### Checking if an APL Process is Running 62 | 63 | Use `p.HasExited` to check if a child process is running. `p.HasExited` will return `1` is the child process is not running and `0` if the child process is running. 64 | 65 | ### Cross-Platform Functionality 66 | To the extent possible, `APLProcess` presents a consistent API across all platforms on which Dyalog APL runs. This way you shouldn't have to change your application code that uses `APLProcess` if you're using multiple platforms. 67 | 68 | Once the child process has been started, the `APLProcess` instance contains an object named `Proc`. Under Windows, `Proc` is an instance of the .NET System.Diagnostics.Process class. On non-Windows platforms, `Proc` is a namespace with elements named the same as a selected subset of the features of the .NET class. -------------------------------------------------------------------------------- /docs/samples.md: -------------------------------------------------------------------------------- 1 | ## Remote Procedure Call (RPC) 2 | 3 | The sample RPC application is located in the `/samples/rpc` folder. The basic flow for the RPC application is: 4 | 5 | 1. Load the `rpc` workspace. 6 | 1. Run `Start` to start a child process which also loads the `rpc` workspace and starts a Conga "command mode" server. 7 | 1. After a brief pause to allow the RPC server to come up, connect to the server. 8 | 1. Use `Do` to send statements to the RPC server to be executed. 9 | 1. `Do` returns a 2-element vector. 10 | * The first element is the `⎕DM` for the error, if any, caused by executing the statement. If no error occurred, `''` is returned. 11 | * The second element is the result of executing the statement, if no error has occurred in doing so, otherwise the offending statement is returned. 12 | 1. Use `Stop` to stop the RPC server and tidy up the client environment. 13 | 14 | These steps are seen below (`]BOX on` is used to show the structure of results): 15 | ``` 16 | )load /git/APLProcess/samples/rpc/rpc 17 | /git/APLProcess/samples/rpc/rpc.dws saved ... 18 | Sample Remote Procedure Call (RPC) application demonstrating the use of APLProcess 19 | Use Start '' to start the application with default settings. 20 | 21 | Start '' 22 | ┌─┬────────────────────────────────────┐ 23 | │0│Connected to RPC Server on port 8888│ 24 | └─┴────────────────────────────────────┘ 25 | Do '⍳3 3' 26 | ┌┬─────────────┐ 27 | ││┌───┬───┬───┐│ 28 | │││1 1│1 2│1 3││ 29 | ││├───┼───┼───┤│ 30 | │││2 1│2 2│2 3││ 31 | ││├───┼───┼───┤│ 32 | │││3 1│3 2│3 3││ 33 | ││└───┴───┴───┘│ 34 | └┴─────────────┘ 35 | Do '÷0' 36 | ┌────────────┬──┐ 37 | │DOMAIN ERROR│÷0│ 38 | │÷0 │ │ 39 | │∧ │ │ 40 | └────────────┴──┘ 41 | Stop 42 | RPC Client closed and expunged 43 | RPC Server process terminated 44 | Conga shared library unloaded and Conga was erased 45 | ``` 46 | The relevant lines in `Start` which use `APLProcess` are shown below: 47 | ``` 48 | ⍝ create and configure the instance of APLProcess 49 | RPC_Process←APLProcess.New'' 50 | RPC_Process.Ws←⎕WSID ⍝ workspace to load 51 | RPC_Process.Args←'RPC_Port=',⍕port ⍝ port to use 52 | RPC_Process.Args,←' RPC_Threaded=',⍕threaded ⍝ run the server in a thread in the child process? 53 | RPC_Process.RunTime←runtime ⍝ use runtime interpreter? 54 | RPC_Process.RideInit←''(port+1)rideable⊃⍨0 1⍸rideable ⍝ RIDE port 55 | RPC_Process.Detach←~detach ⍝ don't kill child process when p goes away 56 | 57 | ⍝ try to launch the child process 58 | :If 0≠⊃(rc msg)←RPC_Process.Run 59 | msg←'Failed to start APL process due to: ',∊⍕msg 60 | →0 61 | :EndIf 62 | 63 | ⎕DL 2 ⍝ pause to let child process initialize 64 | :If ~RPC_Process.HasExited ⍝ check to make sure the child process is there 65 | :If 0=⊃rc←RPCClt.Clt'RPCClient' 'localhost'port'Command' 66 | msg←'Connected to RPC Server on port ',⍕port 67 | msg,←rideable/(⎕UCS 13),'RIDE available on port ',⍕1+port 68 | rc←0 69 | :Else 70 | msg←'Could not connect to RPC Server on port ',(⍕port),' due to ',∊⍕(4↑rc)[1 3 4] 71 | :EndIf 72 | :EndIf 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/shared-methods.md: -------------------------------------------------------------------------------- 1 | Shared methods are functions that can be accessed from the `APLProcess` class itself as well as from an instance of `APLProcess`. Most of the shared methods are used for diagnostic and informational purposes. The two most-commonly-used methods, `New` and `Version`, are presented first. The remaining methods are primarily intended for diagnostic and debugging and are presented in alphbetical order. 2 | 3 | ### `New` 4 | |--|--| 5 | | Description | `New` creates and returns a new instance of `APLProcess`.| 6 | | Syntax | `p←APLProcess.New args` where | 7 | | Examples | Using positional arguments:
`p←APLProcess.New '/myApp' 'DYALOG_NETCORE=1' 0 4502`

Using a namespace argument:
`ns←⎕NS ''`
`ns.(Ws Args RideInit)←'/myApp' 'DYALOG_NETCORE' 4502`
`p←APLProcess.New ns` | 8 | | Notes | `APLProcess.New` is an alternative to using `⎕NEW`. The difference between using `⎕NEW` and `APLProcess.New` with arguments is that `⎕NEW` will automatically the child APL process whereas `APLProcess.New` will not.| 9 | 10 | ### `Version` 11 | |--|--| 12 | | Description | `Version` returns version information for `APLProcess`. It returns a 3-element array of [1] `'APLProcess'` [2] The version number in the format major.minor.patch [3] The date of the version in the format YYYY-MM-DD| 13 | | Syntax | `ver←APLProcess.Version` | 14 | | Examples |      `APLProcess.Version`
 `APLProcess 2.3.0 2024-07-06`| 15 | 16 | ### `GetCurrentExecutable` 17 | |--|--| 18 | | Description | `GetCurrentExecutable` returns the full path to the current Dyalog APL executable.| 19 | | Syntax | `ver←APLProcess.Version` | 20 | | Examples |      `APLProcess.GetCurrentExecutable`
`C:\Program Files\Dyalog\Dyalog APL-64 19.0 Unicode\dyalog.exe`| 21 | 22 | ### `GetCurrentProcessId` 23 | |--|--| 24 | | Description | `GetCurrentProcessId` returns the process ID for the current process.| 25 | | Syntax | `ver←APLProcess.GetCurrentProcessId` | 26 | | Examples |      `APLProcess.GetCurrentProcessId`
`28288`| 27 | 28 | ### `IsAIX` 29 | |--|--| 30 | | Description | `IsAIX` returns `1` if the current platform is AIX, `0` otherwise. | 31 | | Syntax | `ver←APLProcess.IsAIX` | 32 | 33 | ### `IsMac` 34 | |--|--| 35 | | Description | `IsMac` returns `1` if the current platform is macOS, `0` otherwise.| 36 | | Syntax | `ver←APLProcess.IsMac` | 37 | 38 | ### `IsNetCore` 39 | |--|--| 40 | | Description | `IsNetCore` returns `1` if the current platform is using .NET, `0` otherwise. | 41 | | Syntax | `ver←APLProcess.IsNetCore` | 42 | | Notes | Given the schizophrenic naming of .NET by Microsoft, .NET Core is now known as just .NET, not to be confused .NET Framework, which sounds like it should also be .NET, but it's not. Got it?| 43 | 44 | ### `IsRunning` 45 | |--|--| 46 | | Description | `IsRunning` returns `1` if the process identified by its arguments is currently running, `0` otherwise. | 47 | | Syntax | `APLProcess.IsRunning pid [exe] [startTS]` where | 48 | | Examples |       `APLProcess.IsRunning 28288 'dyalog' ⍝ Windows only`
`1`
      `APLProcess.(IsRunning GetCurrentProcessId) ⍝ should always return 1`
`1` | 49 | | Notes | On non-Windows platforms, the `pid` is the only argument available. | 50 | 51 | ### `IsWin` 52 | |--|--| 53 | | Description | `IsWin` returns `1` if the current platform is Windows, `0` otherwise. | 54 | | Syntax | `ver←APLProcess.Version` | 55 | 56 | ### `KillChildren` 57 | |--|--| 58 | | Description | `KillChildren` will attempt to terminate all child processes running a specific executable process name. | 59 | | Syntax | `r←APLProcess.KillChildren exe` where | 60 | | Examples |       `APLProcess.KillChildren 'dyalog' ⍝ terminal all dyalog processes started by this process` | 61 | | Notes | `KillChildren` is intended to be a utility to help clean up child processes that are still running after their `APLProcess` instance has been expunged as would be the case if [`Detach`](./instance.md#detach) is set to `1`. | 62 | 63 | ### `ListProcesses` 64 | |--|--| 65 | | Description | `ListProcesses` returns process ids and names. | 66 | | Syntax | `r←{all} APLProcess.ListProcesses procName` where | 67 | | Examples |       `APLProcess.ListProcesses 'dyalog'`
`31568 dyalog`
      `≢1 APLProcess.ListProcesses '' ⍝ how many processes running?`
`350`| 68 | | Notes | `ListProcesses` excludes the current process from its result. | 69 | 70 | ### `MyDNSName` 71 | |--|--| 72 | | Description | `MyDNSName` returns the computer name when running on Windows or host name when running on non-Windows platforms. | 73 | | Syntax | `r←APLProcess.MyDNSName` | 74 | | Examples |      `APLProcess.MyDNSName`
`my-computer-name` | 75 | 76 | ### `Platform` 77 | |--|--| 78 | | Description | `Platform` returns a 3-character vector indicating the current platform. It is literally `3↑⊃#.⎕WG 'APLVersion'`. | 79 | | Syntax | `r←APLProcess.Platform` | 80 | 81 | ### `ProcessUsingPort` 82 | |--|--| 83 | | Description | `ProcessUsingPort` returns the process id that is listening on the port number passed as the argument. | 84 | | Syntax | `r←APLProcess.ProcessUsingPort port` where | 85 | | Examples |       `APLProcess.ProcessUsingPort 8080`
`26702` | 86 | | Notes | `ProcessUsingPort` can be useful for identifying a process that's already using a port you expected to be available. | 87 | 88 | ### `Stop` 89 | |--|--| 90 | | Description | `Stop` attempts to terminate the process identified by the process id passed as the argument; returning `1` if the process was terminated, `0` otherwise. | 91 | | Syntax | `r←APLProcess.Stop pid` where | 92 | | Examples |       `APLProcess.Stop 12306`
`1` | 93 | | Notes | `Stop` will return `1` if there no process with an id matching `pid`. | 94 | 95 | ### `UsingSystemDiagnostics` 96 | |--|--| 97 | | Description | `UsingSystemDiagnostics` returns the .NET namespace and dll that will be used under Windows. | 98 | | Syntax | `ver←APLProcess.UsingSystemDiagnostics` | 99 | | Examples |       `APLProcess.UsingSystemDiagnostics ⍝ under .NET Framework`
`System,System.dll`
      `APLProcess.UsingSystemDiagnostics ⍝ under .NET`
`System,System.Diagnostics.Process` | 100 | | Notes | The result of `UsingSystemDiagnostics` is used to set `⎕USING` within `APLProcess` | 101 | 102 | ### `_SH` 103 | |--|--| 104 | | Description | `_SH` is a cover for `⎕SH` and is used within `APLProcess` primarily for non-Windows commands. | 105 | | Syntax | `r←APLProcess._SH cmd` where | 106 | | Examples |       `APLProcess._SH 'whoami'`
` pi`| 107 | | Notes | If a trappable error occurs when executing the command, `r` will be an empty vector.| -------------------------------------------------------------------------------- /docs/instance.md: -------------------------------------------------------------------------------- 1 | You create an instance of `APLProcess` using either `⎕NEW` or [`APLProcess.New`](shared-methods.md#new) as described in the [Creating and Starting APLProcess](./userguide.md#creating-and-starting-aplprocess). 2 | 3 | ## Settings 4 | `APLProcess`'s settings can be provided as arguments to `APLProcess.New` or `⎕NEW`. You can also specify settings in the `APLProcess` instance before launching the new process. When provided as arguments, the settings can be specified in two ways. 5 | 6 | 1. A vector of positional settings in the following order:
`Ws Args RunTime RideInit OutFile WorkingDir Detach`
If you want to supply settings with intermediate unspecified setting, you will need to supply the default value for the intermediate settings. For example, to specify the `Ws` and `RideInit` settings, you would need to specify `/myApp '' 0 'SERVE:*:4502`, using the default values for `Args` and `RunTime`. 7 | 1. A namespace containing named variables for the settings you wish to set. 8 | ``` 9 | ns←⎕NS '' 10 | ns.(Ws RideInit)←'/myApp' 'SERVE:*:4502' 11 | p←APLProcess.New ns 12 | ``` 13 | !!! Note 14 | Remember that using `⎕NEW` with arguments, in either positional or namespace form, will start the APL process automatically. 15 | 16 | ### `Ws` 17 | |--|--| 18 | | Description | `Ws` is the name of the workspace that the child process will load | 19 | | Default | `''`| 20 | | Examples | `p.Ws←⎕WSID ⍝ have the child process load the current workspace` | 21 | | Notes | An alternative to specifying `Ws` could be to use the `Load` and `LX` command line parameters. If you specify neither `Ws` nor the `Load`/`LX` command line parameters, the child process will sit there, languishing and doing nothing.| 22 | 23 | ### `Args` 24 | |--|--| 25 | | Description | `Args` is a character vector which represents the Dyalog command line parameters, if any, to be passed to the interpreter in the child process. | 26 | | Default | `''`| 27 | | Examples | `p.Args←'Load=/myApp LX=Start' ⍝ load code from the /myApp folder and run the Start function` | 28 | 29 | ### `RunTime` 30 | |--|--| 31 | | Description | `RunTime` allows you to control the Dyalog interpreter that the child process will run. `RunTime` can have the following values:
| 32 | | Default | `0` meaning do not use a runtime interpreter. | 33 | | Examples | `p.RunTime←1 ⍝ use a runtime interpreter`
`p.RunTime←'/opt/mdyalog/19.0/64/unicode/dyalog' ⍝ use a specific interpreter` | 34 | | Notes | `RunTime` is a somewhat overloaded setting that has evolved over time to take on additional meanings other than just whether to use a runtime interpreter. There is a pending capability to use `RunTime` to specify parameters in order to start an APL process on another maching using ssh, but that functionality is currently under development. | 35 | 36 | ### `RideInit` 37 | |--|--| 38 | | Description | `RideInit` specifies the `RIDE_INIT` command line parameter to use if you want to be able to RIDE into the child process's APL session. `RideInit` can be either:
  • the integer port number for RIDE to listen on in SERVE mode. `4504` would be the equivalent of specifying `'SERVE:*:4504'`| 39 | | Default | `''` which means RIDE access is not enabled for the child process | 40 | | Examples | `p.RideInit←4504 ⍝ run with SERVE:*:4504`
    `p.RideInit←'HTTP:*:4504 ⍝ run with zero-footprint RIDE on port 4504` | 41 | | Notes | `APLProcess` will always set the `RIDE_INIT` command line parameter in order to not inherit the setting from the parent APL process. | 42 | 43 | ### `Load` 44 | |--|--| 45 | | Description | `Load` specifies the `LOAD` parameter for the child process. `Load` specifies the workspace, folder, or file to be loaded and run by the child process. `Load`, if specified, overrides the workspace specified by `Ws`. | 46 | | Default | `''` | 47 | | Examples | `p.Load←'/home/user/myapp'` | 48 | | Notes | `APLProcess` will always set the `LOAD` command line parameter in order to not inherit the setting from the parent APL process.| 49 | 50 | ### `Lx` 51 | |--|--| 52 | | Description | `Lx` specifies the `LX` parameter for the child process. `Lx`, if specified, overrides `⎕LX` in the workspace, if a workspace is either specified by `Ws` or `Load`. | 53 | | Default | `''` which means that the child process will inherit the `LX` parameter, if any, set for the parent process. | 54 | | Examples | `p.Lx←'Start'` | 55 | | Notes | If `LX` is set for the parent process and `Lx` is not specified for the child process, the child process will inherit the parent's setting. This is likely to not be what you want.| 56 | 57 | 58 | ### `OutFile` 59 | |--|--| 60 | | Description | `OutFile` specifies the name of a file to which the session output of the child APL process will be written. | 61 | | Default | `''` meaning session output will not be written to file. | 62 | | Examples | `p.OutFile←'/home/me/Desktop/output.txt'` | 63 | 64 | ### `WorkingDir` 65 | |--|--| 66 | | Description | `WorkingDir` specifies the working directory for the child APL process | 67 | | Default | `''` meaning use the default working directory for the child process's APL interpreter | 68 | | Examples | `p.WorkingDir←'/home/me/Desktop'` | 69 | 70 | ### `Detach` 71 | |--|--| 72 | | Description |`Detach` is a Boolean which indicates whether child process(es) should be terminated when the parent `APLProcess` instance is expunged.
    `0` indicates that the child process(es) should be terminated.
    `1` indicates that the child process(es) should not be terminated.| 73 | | Default |`0`| 74 | | Examples | `p.Detach←1 ⍝ do not close the child APL process when the parent's APLProcess instance is expunged.` | 75 | | Notes | If `Detach` is set to `0`, the child APL process will be terminated if the parent APL process exits; when set to `1` the child process will continue to run even if the parent APL process exits. | 76 | 77 | ### `Id` 78 | |--|--| 79 | | Description | `Id` is a read-only setting that returns the process ID of the child APL process. If the child APL process has not been started, `Id` is set to `''`.| 80 | | Default | `''` | 81 | | Examples |       `(p←APLProcess.New '/myApp' '' 0 4505).Run`
    `0`
          `p.Id`
    `18212`| 82 | 83 | ### `Proc` 84 | |--|--| 85 | | Description | `Proc` is not really a setting but rather an attempt to present consistent cross-platform interface. `Proc` is created when the child APL process is started.

    On Windows `Proc` is an instance of the .NET System.Diagnostic.Process class which has many more properties and methods than we expose in `APLProcess`.

    On non-Windows platforms, `Proc` is a namespace which emulates a subset of the System.Diagnostic.Process class which we deem necessary to start and manage the child APL process. | 86 | 87 | ## Methods 88 | ### `Run` 89 | |--|--| 90 | | Description | `Run` starts the child APL process.| 91 | | Syntax | `(rc msg)←p.Run` where `rc` and `msg` are `0` and `''` respectively if no error occurred when starting the child APL process; otherwise `rc` and `msg` are the APL error number and message.| 92 | | Examples |       `(ns←⎕NS '').(Ws RideInit)←'/myApp' 4505`
          `p←APLProcess.New ns ⍝ load /myApp and RIDE on port 4505`
          `p.Run`
    `1`| 93 | 94 | ### `Kill` 95 | |--|--| 96 | | Description | `Kill` will attempt to terminate the child APL process.| 97 | | Syntax | `r←p.Kill` where `r` is `1` if the child APL process was terminated, `0` if the child APLProcess is still running| 98 | | Examples |       `p←⎕NEW APLProcess ('myApp' '' 0 4505)`
          `p.Kill`
    `1`| 99 | 100 | ### `HasExited` 101 | |--|--| 102 | | Description | `HasExited` reports whether the child APL process is running | 103 | | Syntax | `r←p.HasExited` where `r` is a Boolean where `1` indicates the child process is not running, `0` indicates the child process is running.| 104 | | Examples |       `p←APLProcess.New '/myApp' '' 0 4505`
          `r.Run`
    `1`
          `p.HasExited`
    `0`
          `p.Kill`
    `1`
          `p.HasExited`
    `1`| -------------------------------------------------------------------------------- /docs/img/dyalog-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 23 | 27 | 28 | 31 | 35 | 36 | 37 | 59 | 61 | 62 | 64 | image/svg+xml 65 | 67 | 68 | 69 | 70 | 71 | 77 | 80 | 83 | 88 | 89 | 92 | 97 | 98 | 101 | 106 | 107 | 110 | 115 | 116 | 119 | 124 | 125 | 128 | 133 | 134 | 135 | 136 | 142 | 146 | 150 | 155 | 156 | 160 | 165 | 166 | 170 | 175 | 176 | 180 | 185 | 186 | 191 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /source/APLProcess.dyalog: -------------------------------------------------------------------------------- 1 | :Class APLProcess 2 | ⍝ Start (and eventually dispose of) an APL process 3 | 4 | (⎕IO ⎕ML)←1 1 5 | 6 | ∇ r←Version 7 | :Access Public Shared 8 | r←'APLProcess' '2.4.0' '2025-12-09' 9 | ∇ 10 | 11 | :Field Public Args←'' 12 | :Field Public Ws←'' 13 | :Field Public Exe←'' 14 | :Field Public Proc 15 | :Field Public onExit←'' 16 | :Field Public RunTime←0 ⍝ Boolean or name of runtime executable 17 | :Field Public IsSsh 18 | :Field Public RideInit←'' 19 | :Field Public Load←'' 20 | :Field Public Lx←'' 21 | :Field Public OutFile←'' 22 | :Field Public WorkingDir←'' 23 | :Field Public Detach←0 24 | 25 | :property Id 26 | :Access public 27 | ∇ r←Get ipa 28 | r←{6::'' ⋄ Proc.Id}'' 29 | ∇ 30 | :endproperty 31 | 32 | ∇ make 33 | :Access public instance 34 | :Implements constructor 35 | make_common 36 | ∇ 37 | 38 | ∇ make1 args;rt;cmd;ws;params;ns;invalid;settings 39 | :Access Public Instance 40 | :Implements Constructor 41 | ⍝ args is: 42 | ⍝ [1] the workspace to load 43 | ⍝ [2] any command line arguments 44 | ⍝ {[3]} if present, a Boolean indicating whether to use the runtime version, OR a character vector of the executable name to run 45 | ⍝ {[4]} if present, the RIDE_INIT parameters to use or RIDE port for SERVE mode 46 | ⍝ {[5]} if present, a log-file prefix for process output 47 | ⍝ {[6]} if present, the "current directory" when APL is started 48 | ⍝ {[7]} if present, and set to 1, do not kill spawned process in destructor 49 | ⍝ {[8]} if present, is the LOAD= setting for spawned process 50 | ⍝ {[9]} if present, is the LX= setting for the spawned process 51 | make_common 52 | args←(eis⍣({9.1≠⎕NC⊂,'⍵'}⊃args)⊢args) 53 | :Select {⊃⎕NC⊂,'⍵'}⊃args 54 | :Case 2.1 ⍝ array 55 | (Ws Args RunTime RideInit OutFile WorkingDir Detach Load Lx)←9↑args,(⍴args)↓Ws Args RunTime RideInit OutFile WorkingDir Detach Load Lx 56 | :Case 9.1 ⍝ namespace 57 | :If 0∊⍴invalid←(settings←args.⎕NL ¯2.1 ¯9.1)~(⎕NEW⊃⊃⎕CLASS ⎕THIS).⎕NL ¯2.2 58 | args{⍎⍵,'←⍺⍎⍵'}¨settings 59 | :Else ⋄ ('Invalid APLProcess setting(s): ',,⍕invalid)⎕SIGNAL 11 60 | :EndIf 61 | :Else ⋄ 'Invalid constructor argument'⎕SIGNAL 11 62 | :EndSelect 63 | :If 'New'≢2⊃⎕SI,⊂'' ⍝ do not autostart if using APLProcess.New 64 | {}Run 65 | :EndIf 66 | ∇ 67 | 68 | ∇ make_common 69 | Proc←⎕NS'' ⍝ Do NOT do this in the field definition 70 | IsSsh←0 71 | WorkingDir←1⊃1 ⎕NPARTS'' ⍝ default directory 72 | PATH←SourcePath 73 | ∇ 74 | 75 | ∇ r←New args 76 | :Access public shared 77 | :If 0∊⍴args 78 | r←##.⎕NEW ⎕THIS 79 | :Else 80 | r←##.⎕NEW ⎕THIS args 81 | :EndIf 82 | ∇ 83 | 84 | ∇ (rc msg)←Run 85 | :Access Public Instance 86 | msg←'' 87 | :Trap rc←0 88 | Start(Ws Args RunTime) 89 | :Else 90 | (rc msg)←⎕DMX.(EN EM) 91 | :EndTrap 92 | ∇ 93 | 94 | ∇ Start(ws args rt);psi;pid;cmd;host;port;keyfile;exe;z;output 95 | (Ws Args)←ws args 96 | 97 | :If 0∊⍴RideInit ⍝ Always set RIDE_INIT so that started process does not inherit this process's setting 98 | args,←' RIDE_INIT=""' 99 | :ElseIf 3=10|⎕DR RideInit ⍝ port number? 100 | args,←' RIDE_INIT="SERVE:*:',(⍕RideInit),'" RIDE_SPAWNED=1' 101 | :Else 102 | args,←' RIDE_INIT="',RideInit,'" RIDE_SPAWNED=1' 103 | :EndIf 104 | 105 | args,←' LOAD="',Load,'"' ⍝ always set LOAD as to not inherit this process's setting 106 | 107 | :If ~0∊⍴Lx 108 | args,←' LX="',Lx,'"' 109 | :EndIf 110 | 111 | :If ~0 2 6∊⍨10|⎕DR rt ⍝ if rt is character or nested, it defines what to start 112 | Exe←(RunTimeName⍣rt)GetCurrentExecutable ⍝ else, deduce it 113 | :Else 114 | Exe←rt 115 | rt←0 116 | :EndIf 117 | 118 | IsSsh←326=⎕DR Exe ⍝ ssh is denoted by nested exe (host port keyfile exe) 119 | 120 | :If IsWin>IsSsh 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 ~Detach 162 | :If IsWin 163 | :AndIf ~0∊⍴OutFile 164 | WaitForKill 200 0.1 ⍝ don't run this in a separate thread if redirecting output on Windows 165 | :Trap 0 166 | out←Proc.StandardOutput.ReadToEnd 167 | (⊂out)⎕NPUT OutFile 1 168 | :EndTrap 169 | :Else 170 | WaitForKill&200 0.1 ⍝ otherwise run in a thread for improved throughput 171 | :EndIf 172 | :EndIf 173 | ∇ 174 | 175 | ∇ WaitForKill(limit interval);count 176 | :If (0≠⍴onExit)∧~HasExited ⍝ If the process is still alive 177 | :Trap 0 ⋄ ⍎onExit ⋄ :EndTrap ⍝ Try this 178 | 179 | count←0 180 | :While ~HasExited 181 | {}⎕DL interval 182 | count←count+1 183 | :Until count>limit 184 | :EndIf ⍝ OK, have it your own way 185 | 186 | {}Kill Proc 187 | ∇ 188 | 189 | ∇ r←GetCurrentProcessId;t 190 | :Access Public Shared 191 | :If IsWin 192 | r←⍎'t'⎕NA'U4 kernel32|GetCurrentProcessId' 193 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 194 | r←Proc.Pid 195 | :Else 196 | r←tonum⊃_SH'echo $PPID' 197 | :EndIf 198 | ∇ 199 | 200 | ∇ r←GetCurrentExecutable;⎕USING;t;gmfn 201 | :Access Public Shared 202 | :If IsWin 203 | r←'' 204 | :Trap 0 205 | 'gmfn'⎕NA'U4 kernel32|GetModuleFileName* P =T[] U4' 206 | r←⊃⍴/gmfn 0(1024⍴' ')1024 207 | :EndTrap 208 | :If 0∊⍴r 209 | ⎕USING←UsingSystemDiagnostics 210 | r←2 ⎕NQ'.' 'GetEnvironment' 'DYALOG' 211 | r←r,(~(¯1↑r)∊'\/')/'/' ⍝ Add separator if necessary 212 | r←r,(Diagnostics.Process.GetCurrentProcess.ProcessName),'.exe' 213 | :EndIf 214 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 215 | ∘∘∘ ⍝ Not supported 216 | :Else 217 | t←⊃1↓_SH'ps -o args -p ',⍕GetCurrentProcessId ⍝ AWS 218 | :If '"'''∊⍨⊃t ⍝ if command begins with ' or " 219 | r←{⍵/⍨{∧\⍵∨≠\⍵}⍵=⊃⍵}t 220 | :Else 221 | r←{⍵↑⍨¯1+1⍳⍨(¯1↓0,⍵='\')<⍵=' '}t ⍝ otherwise find first non-escaped space (this will fail on files that end with '\\') 222 | :EndIf 223 | :EndIf 224 | ∇ 225 | 226 | ∇ r←RunTimeName exe 227 | ⍝ Assumes that: 228 | ⍝ Windows runtime ends in "rt.exe" 229 | ⍝ *NIX runtime ends in ".rt" 230 | r←exe 231 | :If IsWin 232 | :If 'rt.exe'≢¯6↑{('rt.ex',⍵)[⍵⍳⍨'RT.EX',⍵]}exe ⍝ deal with case insensitivity 233 | r←'rt.exe',⍨{(~∨\⌽<\⌽'.'=⍵)/⍵}exe 234 | :EndIf 235 | :Else 236 | r←exe,('.rt'≢¯3↑exe)/'.rt' 237 | :EndIf 238 | ∇ 239 | 240 | 241 | ∇ r←KillChildren Exe;kids;⎕USING;p;m;i;mask 242 | :Access Public Shared 243 | ⍝ returns [;1] pid [;2] process name of any processes that were not killed 244 | r←0 2⍴0 '' 245 | :If ~0∊⍴kids←ListProcesses Exe ⍝ All child processes using the exe 246 | :If IsWin 247 | ⎕USING←UsingSystemDiagnostics 248 | p←Diagnostics.Process.GetProcessById¨kids[;1] 249 | p.Kill 250 | ⎕DL 1 251 | :If 0≠⍴p←(~p.HasExited)/p 252 | ⎕DL 1 253 | p.Kill 254 | ⎕DL 1 255 | :If ∨/m←~p.HasExited 256 | r←(kids[;1]∊m/p.Id)⌿kids 257 | :EndIf 258 | :EndIf 259 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 260 | ∘∘∘ 261 | :Else 262 | mask←(⍬⍴⍴kids)⍴0 263 | :For i :In ⍳⍴mask 264 | mask[i]←Shoot kids[i;1] 265 | :EndFor 266 | r←(~mask)⌿kids 267 | :EndIf 268 | :EndIf 269 | ∇ 270 | 271 | ∇ r←{all}ListProcesses procName;me;⎕USING;procs;unames;names;name;i;pn;kid;parent;mask;n;cmd;t 272 | :Access Public Shared 273 | ⍝ returns either my child processes or all processes 274 | ⍝ procName is either '' for all children, or the name of a process 275 | ⍝ r[;1] - child process number (Id) 276 | ⍝ r[;2] - child process name 277 | me←GetCurrentProcessId 278 | r←0 2⍴0 '' 279 | procName←,procName 280 | all←{6::⍵ ⋄ all}0 ⍝ default to just my childen 281 | 282 | :If IsWin 283 | ⎕USING←UsingSystemDiagnostics 284 | 285 | :If 0∊⍴procName ⋄ procs←Diagnostics.Process.GetProcesses'' 286 | :Else ⋄ procs←Diagnostics.Process.GetProcessesByName⊂procName ⋄ :EndIf 287 | :If all 288 | r←↑procs.(Id ProcessName) 289 | r⌿⍨←r[;1]≠me 290 | :Else 291 | :If 0<⍴procs 292 | unames←∪names←procs.ProcessName 293 | :For name :In unames 294 | :For i :In ⍳n←1+.=(,⊂name)⍳names 295 | pn←name,(n≠1)/'#',⍕i 296 | :Trap 0 ⍝ trap here just in case a process disappeared before we get to it 297 | parent←⎕NEW Diagnostics.PerformanceCounter('Process' 'Creating Process Id'pn) 298 | :If me=parent.NextValue 299 | kid←⎕NEW Diagnostics.PerformanceCounter('Process' 'Id Process'pn) 300 | r⍪←(kid.NextValue)name 301 | :EndIf 302 | :EndTrap 303 | :EndFor 304 | :EndFor 305 | :EndIf 306 | :EndIf 307 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 308 | ∘∘∘ 309 | :Else ⍝ Linux 310 | ⍝ unfortunately, Ubuntu (and perhaps others) report the PPID of tasks started via ⎕SH as 1 311 | ⍝ so, the best we can do at this point is identify processes that we tagged with APLppid= 312 | cmd←'ps -eo pid,args | sed -n ''2,$p''' ⍝ list process id and command line (with arguments) 313 | cmd,←(~all)/' | grep APLppid=',⍕me ⍝ is not selecting all, limit to APLProcess's my process started 314 | cmd,←(t←~0∊⍴procName)/' | grep ',procName ⍝ limit to entries with procName if it exists 315 | cmd,←' | grep -v grep' ⍝ remove "grep" entries 316 | procs←_SH cmd 317 | →0⍴⍨0∊⍴procs 318 | procs←↑(2 part deb)¨procs 319 | procs[;1]←(⊃tonum)¨procs[;1] 320 | procs⌿⍨←me≠procs[;1] ⍝ remove my task 321 | mask←1 322 | :If t 323 | mask←∨/¨(procName,' ')∘⍷¨procs[;2],¨' ' 324 | :EndIf 325 | r←mask⌿procs 326 | :EndIf 327 | ∇ 328 | 329 | ∇ r←Kill;delay 330 | :Access Public Instance 331 | r←0 ⋄ delay←0.1 332 | :Trap 0 333 | :If IsWin 334 | :If IsNetCore ⋄ Proc.Kill ⍬ ⋄ :Else ⋄ Proc.Kill ⋄ :EndIf ⍝ In .Net Core, Kill takes an argument 335 | :Repeat 336 | ⎕DL delay×~Proc.HasExited 337 | delay+←delay 338 | :Until (delay>10)∨Proc.HasExited 339 | :ElseIf {2::0 ⋄ IsSsh}'' 340 | ∘∘∘ 341 | :Else ⍝ Local UNIX 342 | {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt 343 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 344 | :If ~Proc.HasExited←~UNIXIsRunning Proc.Id 345 | {}UNIXIssueKill 9 Proc.Id ⍝ issue strong interrupt 346 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 347 | :AndIf ~Proc.HasExited←~UNIXIsRunning Proc.Id 348 | :Repeat 349 | ⎕DL delay 350 | delay+←delay 351 | :Until (delay>10)∨Proc.HasExited←~UNIXIsRunning Proc.Id 352 | :EndIf 353 | :EndIf 354 | r←Proc.HasExited 355 | :EndTrap 356 | ∇ 357 | 358 | ∇ r←Shoot Proc;MAX;res 359 | MAX←100 360 | r←0 361 | :If 0≠⎕NC⊂'Proc.HasExited' 362 | :Repeat 363 | :If ~Proc.HasExited 364 | :If IsWin 365 | :If IsNetCore ⋄ Proc.Kill ⍬ ⋄ :Else ⋄ Proc.Kill ⋄ :EndIf 366 | ⎕DL 0.2 367 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 368 | ∘∘∘ 369 | :Else 370 | {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt AWS 371 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 372 | Proc.HasExited←~UNIXIsRunning Proc.Id ⍝ AWS 373 | :EndIf 374 | :EndIf 375 | MAX-←1 376 | :Until Proc.HasExited∨MAX≤0 377 | r←Proc.HasExited 378 | :ElseIf 2=⎕NC'Proc' ⍝ just a process id? 379 | {}UNIXIssueKill 9 Proc 380 | {}⎕DL 2 381 | r←~UNIXIsRunning Proc 382 | :EndIf 383 | ∇ 384 | 385 | ∇ r←HasExited 386 | :Access public instance 387 | :If IsWin∨{2::0 ⋄ IsSsh}'' 388 | r←{0::⍵ ⋄ Proc.HasExited}1 389 | :Else 390 | Proc.HasExited←r←{0::⍵ ⋄ ~UNIXIsRunning Proc.Id}1 ⍝ AWS 391 | :EndIf 392 | ∇ 393 | 394 | ∇ r←GetExitCode 395 | :Access public Instance 396 | ⍝ *** EXPERIMENTAL *** 397 | ⍝ query exit code of process. Attempt to do it in a cross platform way relying on .Net Core. Unfortunetaly 398 | ⍝ we only use it on Windows atm, so this method can only be used on Windows. 399 | r←'' ⍝ '' indicates "can't check" (for example, because it is still running) or non-windows platform 400 | :If HasExited 401 | :If IsWin 402 | r←Proc.ExitCode 403 | :Else 404 | :EndIf 405 | :EndIf 406 | ∇ 407 | 408 | ∇ r←IsRunning args;⎕USING;start;exe;pid;proc;diff;res 409 | :Access public shared 410 | ⍝ args - pid {exe} {startTS} 411 | r←0 412 | args←eis args 413 | (pid exe start)←3↑args,(⍴args)↓0 ''⍬ 414 | :If IsWin 415 | ⎕USING←UsingSystemDiagnostics 416 | :Trap 0 417 | proc←Diagnostics.Process.GetProcessById pid 418 | r←~proc.HasExited 419 | :Else 420 | :Return 421 | :EndTrap 422 | :If ''≢exe 423 | r∧←exe≡proc.ProcessName 424 | :EndIf 425 | :If ⍬≢start 426 | :Trap 90 427 | diff←|-/DateToIDN¨start(proc.StartTime.(Year Month Day Hour Minute Second Millisecond)) 428 | r∧←diff≤24 60 60 1000⊥0 1 0 0÷×/24 60 60 1000 ⍝ consider it a match within a 1 minute window 429 | :Else 430 | r←0 431 | :EndTrap 432 | :EndIf 433 | :ElseIf {2::0 ⋄ IsSsh}'' 434 | ∘∘∘ 435 | :Else 436 | r←UNIXIsRunning pid 437 | :EndIf 438 | ∇ 439 | 440 | ∇ r←Stop pid;proc 441 | :Access public shared 442 | ⍝ attempts to stop the process with processID pid 443 | :If IsWin 444 | ⎕USING←UsingSystemDiagnostics 445 | :Trap 0 446 | proc←Diagnostics.Process.GetProcessById pid 447 | :Else 448 | r←1 449 | :Return 450 | :EndTrap 451 | :If IsNetCore ⋄ proc.Kill ⍬ ⋄ :Else ⋄ proc.Kill ⋄ :EndIf 452 | {}⎕DL 0.5 453 | r←~IsRunning pid 454 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 455 | ∘∘∘ 456 | :Else 457 | r←UnixKill pid 458 | :EndIf 459 | ∇ 460 | 461 | ∇ r←UNIXIsRunning pid;txt 462 | ⍝ Return 1 if the process is in the process table and is not a defunct 463 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 464 | ∘∘∘ 465 | :Else 466 | :If IsAIX 467 | txt←⊃_SH'ps -p "',(⍕pid),'" -o s=' 468 | :Else 469 | txt←⊃_SH'ps -p "',(⍕pid),'" -o state=' 470 | :EndIf 471 | r←(~'Z'∊txt)∧(∨/txt≠' ')∧0<≢txt 472 | :EndIf 473 | ∇ 474 | 475 | ∇ {r}←UnixKill pid;delay;t 476 | {}UNIXIssueKill 3 pid ⍝ issue strong interrupt 477 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 478 | :If t←UNIXIsRunning pid ⍝ still running? 479 | {}UNIXIssueKill 9 pid ⍝ bring out the heavy guns 480 | {}⎕DL 2 ⍝ wait a couple seconds for it to react 481 | :AndIf t←UNIXIsRunning pid 482 | delay←0.2 483 | :Repeat 484 | ⎕DL delay 485 | delay+←delay 486 | :Until (delay>5)∨t←UNIXIsRunning pid ⍝ keep checking (doubling the wait) up to 5 seconds 487 | :EndIf 488 | r←~t 489 | ∇ 490 | 491 | 492 | ∇ {r}←UNIXIssueKill(signal pid) 493 | signal pid←⍕¨signal pid 494 | cmd←'kill -',signal,' ',pid,' >/dev/null 2>&1 ; echo $?' 495 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 496 | ∘∘∘ 497 | :Else 498 | r←⎕SH cmd 499 | :EndIf 500 | ∇ 501 | 502 | ∇ r←UNIXGetShortCmd pid 503 | ⍝ Retrieve short form of cmd used to start process 504 | :If {2::0 ⋄ IsSsh}'' ⍝ instance? 505 | ∘∘∘ 506 | :Else 507 | :Trap 11 508 | r←⊃1↓_SH'ps -o comm -p ',⍕pid 509 | :Else 510 | r←'' 511 | :EndTrap 512 | :EndIf 513 | ∇ 514 | 515 | ∇ r←_SH cmd 516 | :Access public shared 517 | r←{0::'' ⋄ ⎕SH ⍵}cmd 518 | ∇ 519 | 520 | :Class Time 521 | :Field Public Year 522 | :Field Public Month 523 | :Field Public Day 524 | :Field Public Hour 525 | :Field Public Minute 526 | :Field Public Second 527 | :Field Public Millisecond 528 | 529 | ∇ make ts 530 | :Implements Constructor 531 | :Access Public 532 | (Year Month Day Hour Minute Second Millisecond)←7↑ts 533 | ⎕DF(⍕¯2↑'00',⍕Day),'-',((12 3⍴'JanFebMarAprMayJunJulAugSepOctNovDec')[⍬⍴Month;]),'-',(⍕100|Year),' ',1↓⊃,/{':',¯2↑'00',⍕⍵}¨Hour Minute Second 534 | ∇ 535 | 536 | :EndClass 537 | 538 | ∇ r←ProcessUsingPort port;t 539 | ⍝ return the process ID of the process (if any) using a port 540 | :Access public shared 541 | r←⍬ 542 | :If IsWin 543 | :If ~0∊⍴t←_SH'netstat -a -n -o' 544 | :AndIf ~0∊⍴t/⍨←∨/¨'LISTENING'∘⍷¨t 545 | :AndIf ~0∊⍴t/⍨←∨/¨((':',⍕port),' ')∘⍷¨t 546 | r←∪∊¯1↑¨(//)∘⎕VFI¨t 547 | :EndIf 548 | :Else 549 | :If ~0∊⍴t←_SH'netstat -l -n -p 2>/dev/null | grep '':',(⍕port),' ''' 550 | r←∪∊{⊃(//)⎕VFI{(∧\⍵∊⎕D)/⍵}⊃¯1↑{⎕ML←3 ⋄ (' '≠⍵)⊂⍵}⍵}¨t 551 | :EndIf 552 | :EndIf 553 | ∇ 554 | 555 | ∇ r←MyDNSName;GCN 556 | :Access Public Shared 557 | 558 | :If IsWin 559 | 'GCN'⎕NA'I4 Kernel32|GetComputerNameEx* U4 >0T =U4' 560 | r←2⊃GCN 7 255 255 561 | :Return 562 | ⍝ ComputerNameNetBIOS = 0 563 | ⍝ ComputerNameDnsHostname = 1 564 | ⍝ ComputerNameDnsDomain = 2 565 | ⍝ ComputerNameDnsFullyQualified = 3 566 | ⍝ ComputerNamePhysicalNetBIOS = 4 567 | ⍝ ComputerNamePhysicalDnsHostname = 5 568 | ⍝ ComputerNamePhysicalDnsDomain = 6 569 | ⍝ ComputerNamePhysicalDnsFullyQualified = 7 <<< 570 | ⍝ ComputerNameMax = 8 571 | :ElseIf {2::0 ⋄ IsSsh}'' ⍝ instance? 572 | ∘∘∘ ⍝ Not supported 573 | :Else 574 | r←⊃_SH'hostname' 575 | :EndIf 576 | ∇ 577 | 578 | ∇ idn←DateToIDN ts 579 | idn←(2 ⎕NQ'.' 'DateToIDN'(3↑ts))+(24 60 60 1000⊥4↑3↓ts)÷86400000 580 | ∇ 581 | 582 | ∇ Proc←SshProc(host user keyfile cmd);conn;z;kf;allpids;guid;listpids;pids;⎕USING;pid;tid 583 | ⎕USING←'Renci.SshNet,',PATH,'/Renci.SshNet.dll' 584 | kf←⎕NEW PrivateKeyFile(,⊂keyfile) 585 | conn←⎕NEW SshClient(host 22 user(,kf)) 586 | 587 | :Trap 0 588 | conn.Connect ⍝ This is defined to be a void() 589 | :Case 90 ⋄ ('Error creating ssh client instance: ',⎕EXCEPTION.Message)⎕SIGNAL 11 590 | :Else ⋄ 'Unexpected error creating ssh client instance'⎕SIGNAL 11 591 | :EndTrap 592 | 593 | listpids←{0~⍨2⊃(⎕UCS 10)⎕VFI(conn.RunCommand⊂'ps -u ',user,' | grep dyalog | grep -v grep | awk ''{print $2}''').Result} 594 | guid←'dyalog-ssh-',(⍕⎕TS)~' ' 595 | pids←listpids ⍬ 596 | Proc←⎕NS'' 597 | Proc.SshConn←conn 598 | Proc.HasExited←0 599 | tid←{SshRun conn ⍵ Proc}&⊂cmd 600 | Proc.tid←tid 601 | ⎕DL 1 602 | :If 1=⍴pid←(listpids ⍬)~pids ⋄ pid←⊃pid 603 | :Else ⋄ ∘∘∘ ⋄ :EndIf ⍝ failed to start 604 | Proc.Pid←pid 605 | ∇ 606 | 607 | ∇ SshRun(conn cmd proc) 608 | ⍝ Wait until APL exits, then set HasExited←1 609 | conn.RunCommand cmd 610 | proc.HasExited←1 611 | ∇ 612 | 613 | :section Utils 614 | 615 | endswith←{w←,⍵ ⋄ a←,⍺ ⋄ w≡(-(⍴a)⌊⍴w)↑a} 616 | tonum←{⊃⊃(//)⎕VFI ⍵} 617 | eis←{2>|≡⍵:,⊂⍵ ⋄ ⍵} ⍝ enclose if simple 618 | deb←{1↓¯1↓{⍵/⍨~' '⍷⍵}' ',⍵,' '} ⍝ delete extraneous blanks 619 | part←{⍵⊆⍨~⍺{⍵∧⍺>+\⍵}' '=⍵} ⍝ partition first ⍺ sections 620 | nameClass←{⎕NC⊂,'⍵'} ⍝ name class of argument 621 | 622 | ∇ r←IsWin 623 | :Access public shared 624 | r←'Win'≡Platform 625 | ∇ 626 | 627 | ∇ r←IsMac 628 | :Access public shared 629 | r←'Mac'≡Platform 630 | ∇ 631 | 632 | ∇ r←IsAIX 633 | :Access public shared 634 | r←'AIX'≡Platform 635 | ∇ 636 | 637 | ∇ r←Platform 638 | :Access public shared 639 | r←3↑⊃#.⎕WG'APLVersion' 640 | ∇ 641 | 642 | ∇ r←IsNetCore 643 | :Access public shared 644 | :Trap 11 ⍝ DOMAIN ERROR if 2250⌶ isn't available 645 | r←1 1≡2↑2250⌶0 ⍝ 2250⌶0 is the preferred mechanism to interrogate .NET availability 646 | :Else 647 | r←(,'1')≡2 ⎕NQ'.' 'GetEnvironment' 'DYALOG_NETCORE' 648 | :EndTrap 649 | ∇ 650 | 651 | ∇ r←UsingSystemDiagnostics 652 | :Access public shared 653 | r←(1+IsNetCore)⊃'System,System.dll' 'System,System.Diagnostics.Process' 654 | ∇ 655 | 656 | ∇ path←SourcePath;source 657 | ⍝ Determine the source path of the class 658 | :Trap 6 659 | source←⍎'(⊃⊃⎕CLASS ⎕THIS).SALT_Data.SourceFile' ⍝ ⍎ works around a bug 660 | :Else 661 | :If 0=⍴source←{((⊃¨⍵)⍳⊃⊃⎕CLASS ⎕THIS)⊃⍵,⊂''}5177⌶⍬ ⍝ 5177⌶⍬ - list loaded file objects 662 | source←⎕WSID 663 | :Else ⋄ source←4⊃source 664 | :EndIf 665 | :EndTrap 666 | path←{(-⌊/(⌽⍵)⍳'\/')↓⍵}source 667 | ∇ 668 | 669 | :endsection 670 | 671 | :EndClass 672 | --------------------------------------------------------------------------------