You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by al...@apache.org on 2020/03/31 18:26:59 UTC

[openwhisk-wskdebug] branch master updated (35cbe91 -> 397aa85)

This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git.


    from 35cbe91  remove node 8 in travis as we don't really support it and it keeps failing fsevents tests (#33)
     new 0097ab1  refactor huge debugger.js into separate files & classes
     new 7290fc6  add missing EOL
     new cd50058  guard this.* access in Debugger.shutdown()/stop()/kill()
     new c07093d  ensure latest yargs 15.3.0 is used as #17 depends on it
     new 8eebc66  add complete invocation test for ngrok (failing for now)
     new 2da4644  quick fix for ngrok agent requiring access to invoker
     new 397aa85  fix duplicated shutdown() issue on CTRL+C

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .eslintignore       |   1 +
 package-lock.json   |  47 +++-
 package.json        |   3 +-
 src/agentmgr.js     | 503 +++++++++++++++++++++++++++++++++++++
 src/agents/ngrok.js | 140 +++++++++++
 src/debugger.js     | 705 ++++------------------------------------------------
 src/watcher.js      | 123 +++++++++
 test/ngrok.test.js  | 133 +++++++++-
 test/test.js        |  85 +++++--
 9 files changed, 1049 insertions(+), 691 deletions(-)
 create mode 100644 .eslintignore
 create mode 100644 src/agentmgr.js
 create mode 100644 src/agents/ngrok.js
 create mode 100644 src/watcher.js


[openwhisk-wskdebug] 02/07: add missing EOL

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit 7290fc63e19c81dc073c0f84b947d4943a7a3106
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Sat Mar 28 23:51:43 2020 -0700

    add missing EOL
---
 src/agentmgr.js | 2 +-
 src/ngrok.js    | 2 +-
 src/watcher.js  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/agentmgr.js b/src/agentmgr.js
index cb0e5a9..3264cc1 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -502,4 +502,4 @@ class AgentMgr {
     }
 }
 
-module.exports = AgentMgr;
\ No newline at end of file
+module.exports = AgentMgr;
diff --git a/src/ngrok.js b/src/ngrok.js
index afa01d7..4551ef9 100644
--- a/src/ngrok.js
+++ b/src/ngrok.js
@@ -136,4 +136,4 @@ class NgrokAgent {
     }
 }
 
-module.exports = NgrokAgent;
\ No newline at end of file
+module.exports = NgrokAgent;
diff --git a/src/watcher.js b/src/watcher.js
index d69b554..8488ffa 100644
--- a/src/watcher.js
+++ b/src/watcher.js
@@ -120,4 +120,4 @@ class Watcher {
     }
 }
 
-module.exports = Watcher;
\ No newline at end of file
+module.exports = Watcher;


[openwhisk-wskdebug] 05/07: add complete invocation test for ngrok (failing for now)

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit 8eebc66262db5fed8c7387391c670adf61c9d646
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Mon Mar 30 22:35:24 2020 -0700

    add complete invocation test for ngrok (failing for now)
---
 package-lock.json         |  33 ++++++++++++
 package.json              |   1 +
 src/agentmgr.js           |   2 +-
 src/{ => agents}/ngrok.js |   2 +-
 test/ngrok.test.js        | 129 ++++++++++++++++++++++++++++++++++++++++++++--
 test/test.js              |  85 ++++++++++++++++++++++--------
 6 files changed, 225 insertions(+), 27 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f2f7890..64dbc66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2222,6 +2222,33 @@
                 }
             }
         },
+        "mock-require": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
+            "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
+            "dev": true,
+            "requires": {
+                "get-caller-file": "^1.0.2",
+                "normalize-path": "^2.1.1"
+            },
+            "dependencies": {
+                "get-caller-file": {
+                    "version": "1.0.3",
+                    "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+                    "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+                    "dev": true
+                },
+                "normalize-path": {
+                    "version": "2.1.1",
+                    "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+                    "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+                    "dev": true,
+                    "requires": {
+                        "remove-trailing-separator": "^1.0.1"
+                    }
+                }
+            }
+        },
         "ms": {
             "version": "2.1.2",
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2833,6 +2860,12 @@
                 "es6-error": "^4.0.1"
             }
         },
+        "remove-trailing-separator": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+            "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+            "dev": true
+        },
         "request": {
             "version": "2.88.2",
             "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
diff --git a/package.json b/package.json
index 4e43645..801767f 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
         "get-port": "^5.1.1",
         "mocha": "^7.1.0",
         "mocha-multi-reporters": "^1.1.7",
+        "mock-require": "^3.0.3",
         "nock": "^12.0.2",
         "nyc": "^15.0.0",
         "strip-ansi": "^6.0.0",
diff --git a/src/agentmgr.js b/src/agentmgr.js
index 3264cc1..c1aa673 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -17,7 +17,7 @@
 
 'use strict';
 
-const NgrokAgent = require('./ngrok');
+const NgrokAgent = require('./agents/ngrok');
 const fs = require('fs-extra');
 const sleep = require('util').promisify(setTimeout);
 
diff --git a/src/ngrok.js b/src/agents/ngrok.js
similarity index 98%
rename from src/ngrok.js
rename to src/agents/ngrok.js
index 4551ef9..d778b57 100644
--- a/src/ngrok.js
+++ b/src/agents/ngrok.js
@@ -63,7 +63,7 @@ class NgrokAgent {
 
         console.log(`Ngrok forwarding: ${ngrokUrl} => http://localhost:${this.ngrokServerPort} (auth: ${this.ngrokAuth})`);
 
-        return fs.readFileSync(`${__dirname}/../agent/agent-ngrok.js`, {encoding: 'utf8'});
+        return fs.readFileSync(`${__dirname}/../../agent/agent-ngrok.js`, {encoding: 'utf8'});
     }
 
     async stop() {
diff --git a/test/ngrok.test.js b/test/ngrok.test.js
index fc4a033..0ae21b3 100644
--- a/test/ngrok.test.js
+++ b/test/ngrok.test.js
@@ -19,11 +19,30 @@
 
 'use strict';
 
-const Debugger = require("../src/debugger");
-
 const test = require('./test');
+let Debugger = require("../src/debugger");
+
 const assert = require('assert');
 const nock = require('nock');
+const fetch = require('isomorphic-fetch');
+const mockRequire = require('mock-require');
+
+function mockNgrokLibrary(connect, kill) {
+    mockRequire("ngrok", {
+        connect: connect || function() {
+            console.log('ngrok.connect called');
+        },
+        kill: kill || function() {
+            console.log('ngrok.kill called');
+        }
+    });
+    // the modules have been loaded from another test file before,
+    // so we need to re-require them in the reverse order
+    // to make the mockRequire("ngrok") have an effect
+    mockRequire.reRequire("../src/agents/ngrok");
+    mockRequire.reRequire("../src/agentmgr");
+    Debugger = mockRequire.reRequire("../src/debugger");
+}
 
 describe('ngrok',  function() {
     this.timeout(30000);
@@ -67,11 +86,113 @@ describe('ngrok',  function() {
         const dbgr = new Debugger(argv);
         await dbgr.start();
         // no need to run() for this test
-        dbgr.run();
         await dbgr.stop();
 
         assert(ngrok.isDone(), "Expected these HTTP requests: " + ngrok.pendingMocks().join());
     });
 
-    // TODO: test ngrokHandler, POST to local server
+    /*
+
+    Runtime setup:
+
+        [ wskdebug ]------<start>-----+
+            ^                         |
+            |                         |
+         <handle>                     |
+            |                         v
+        [ local server ]<---------[ local ngrok ]
+                                      ^
+                                      |
+                                      |
+                                  [ ngrok.io ]
+                                      ^
+                                      |
+        [ openwhisk action ]----------+
+
+    Test setup:
+
+        [ wskdebug ]------<start>-----+
+            ^                         |
+            |                         |
+         <handle>                     |
+            |                         v
+        [ local server ]         [ MOCKED ngrok ]
+            ^                         |
+            |                    <pass on port>
+            |                         |
+            |                         |
+        [ MOCKED invocation call ] <--+
+    */
+
+    it("should handle action invocation using ngrok", async function() {
+        const actionName = "myaction";
+
+        // port of the local server started by wskdebug to be expecting calls from ngrok
+        // which we will do in this test
+        let ngrokServerPort;
+        mockNgrokLibrary(function(opts) {
+            ngrokServerPort = opts.addr;
+            return "https://UNIT_TEST.ngrok.io";
+        });
+
+        // should not use this code if we specify local sources which return CORRECT
+        const code = `const main = () => ({ msg: 'WRONG' });`;
+
+        let ngrokAuth;
+
+        test.mockAction(actionName, code);
+        test.mockCreateBackupAction(actionName);
+
+        // ngrok agent installation
+        // custom version instead of test.mockInstallAgent() to catch the ngrokAuth
+        test.openwhiskNock()
+            .put(
+                `${test.openwhiskApiUrlActions()}/${actionName}?overwrite=true`,
+                body => {
+                    ngrokAuth = body.parameters.find(e => e.key === "$ngrokAuth").value;
+                    return body.annotations.some(v => v.key === "wskdebug" && v.value === true);
+                }
+            )
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(actionName));
+
+
+        // wskdebug myaction --ngrok -p ${test.port}
+        const argv = {
+            port: test.port,
+            action: actionName,
+            sourcePath: "action.js",
+            ngrok: true
+        };
+        process.chdir("test/nodejs/plain-flat");
+
+        const dbgr = new Debugger(argv);
+        await dbgr.start();
+        dbgr.run();
+
+        // wait for everything to startup
+        await test.sleep(10);
+
+        try {
+
+            const response = await fetch(`http://127.0.0.1:${ngrokServerPort}`, {
+                method: "POST",
+                headers: {
+                    authorization: ngrokAuth
+                },
+                body: JSON.stringify({
+                    $activationId: "1234567890"
+                })
+            });
+
+            assert.strictEqual(response.status, 200);
+            const result = await response.json();
+            assert.strictEqual(result.msg, "CORRECT");
+
+        } finally {
+            await dbgr.stop();
+        }
+
+        assert(nock.isDone(), "Expected these HTTP requests: " + nock.pendingMocks().join());
+    });
 });
diff --git a/test/test.js b/test/test.js
index 81f5b23..5a266d7 100644
--- a/test/test.js
+++ b/test/test.js
@@ -70,6 +70,18 @@ function assertAllNocksInvoked() {
     );
 }
 
+function openwhiskNock() {
+    return openwhisk;
+}
+
+function openwhiskApiUrlActions() {
+    return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions`;
+}
+
+function openwhiskApiAuthHeader() {
+    return `Basic ${FAKE_OPENWHISK_AUTH}`;
+}
+
 function agentRetryResponse() {
     return {
         response: {
@@ -101,8 +113,8 @@ function agentExitResponse() {
 function mockAction(name, code, binary=false) {
     // reading action without code
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${name}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .query({"code":"false"})
         .reply(200, nodejsActionDescription(name, binary));
 
@@ -112,56 +124,78 @@ function mockAction(name, code, binary=false) {
 
     // reading action with code
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${name}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, action);
 }
 
-function expectAgent(name, code, binary=false) {
+function mockCreateBackupAction(name, binary=false) {
     const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // wskdebug creating the backup action
     openwhisk
-        .put(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}?overwrite=true`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .put(`${openwhiskApiUrlActions()}/${backupName}?overwrite=true`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(backupName, binary));
+}
 
-    // wskdebug creating the backup action
+function mockInstallAgent(name) {
+    // wskdebug overwriting the action with the agent
     openwhisk
         .put(
-            `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}?overwrite=true`,
+            `${openwhiskApiUrlActions()}/${name}?overwrite=true`,
             body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
         )
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(name));
+}
+
+function mockReadBackupAction(name, code, binary=false) {
+    const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // reading it later on restore
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${backupName}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, Object.assign(nodejsActionDescription(backupName, binary), { exec: { code } }));
+}
 
+function mockRestoreAction(name, code, binary=false) {
     // restoring action
     openwhisk
         .put(
-            `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}?overwrite=true`,
+            `${openwhiskApiUrlActions()}/${name}?overwrite=true`,
             body => body.exec && body.exec.code === code
         )
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(name, binary));
+}
+
+function mockRemoveBackupAction(name) {
+    const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // removing backup after restore
     openwhisk
-        .delete(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .delete(`${openwhiskApiUrlActions()}/${backupName}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200);
 }
 
+function expectAgent(name, code, binary=false) {
+    mockCreateBackupAction(name, binary);
+    mockInstallAgent(name);
+
+    // shutdown/restore process
+    mockReadBackupAction(name, code, binary);
+    mockRestoreAction(name, code, binary);
+    mockRemoveBackupAction(name);
+}
+
 function nockActivation(name, bodyFn) {
     return openwhisk
-        .post(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`, bodyFn)
+        .post(`${openwhiskApiUrlActions()}/${name}`, bodyFn)
         .query(true) // support both ?blocking=true and non blocking (no query params)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`);
+        .matchHeader("authorization", openwhiskApiAuthHeader());
 }
 
 function mockAgentPoll(name) {
@@ -328,7 +362,7 @@ function mockOpenwhiskSwagger(openwhisk) {
         .get('/')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, {
             "api_paths": ["/api/v1"],
             "description": "OpenWhisk",
@@ -394,7 +428,7 @@ function mockOpenwhiskSwagger(openwhisk) {
         .get('/api/v1')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200,{
             "api_version":"1.0.0",
             "api_version_path":"v1",
@@ -411,7 +445,7 @@ function mockOpenwhiskSwagger(openwhisk) {
         .get('/api/v1/api-docs')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, JSON.parse(fs.readFileSync("./test/openwhisk-swagger.json")));
 }
 
@@ -487,7 +521,16 @@ module.exports = {
     mockActionAndInvocation,
     mockActionDoubleInvocation,
     // advanced
+    openwhiskNock,
+    openwhiskApiUrlActions,
+    openwhiskApiAuthHeader,
     mockAction,
+    mockCreateBackupAction,
+    mockInstallAgent,
+    mockReadBackupAction,
+    mockRestoreAction,
+    mockRemoveBackupAction,
+    nodejsActionDescription,
     expectAgent,
     nockActivation,
     expectAgentInvocation,


[openwhisk-wskdebug] 06/07: quick fix for ngrok agent requiring access to invoker

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit 2da4644191a176d1abe4253f880d3599876e3048
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Mon Mar 30 22:47:58 2020 -0700

    quick fix for ngrok agent requiring access to invoker
---
 src/agentmgr.js     |  4 ++--
 src/agents/ngrok.js |  3 ++-
 src/debugger.js     |  2 +-
 test/ngrok.test.js  | 20 ++++++++++++--------
 4 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/agentmgr.js b/src/agentmgr.js
index c1aa673..8753f41 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -133,7 +133,7 @@ class AgentMgr {
         return {action, agentAlreadyInstalled };
     }
 
-    async installAgent(action) {
+    async installAgent(action, invoker) {
         this.agentInstalled = true;
 
         let agentName;
@@ -143,7 +143,7 @@ class AgentMgr {
         if (this.argv.ngrok) {
             // user manually requested ngrok
 
-            this.ngrokAgent = new NgrokAgent(this.argv);
+            this.ngrokAgent = new NgrokAgent(this.argv, invoker);
 
             // agent using ngrok for forwarding
             agentName = "ngrok";
diff --git a/src/agents/ngrok.js b/src/agents/ngrok.js
index d778b57..1ee2a45 100644
--- a/src/agents/ngrok.js
+++ b/src/agents/ngrok.js
@@ -25,8 +25,9 @@ const util = require('util');
 const crypto = require("crypto");
 
 class NgrokAgent {
-    constructor(argv) {
+    constructor(argv, invoker) {
         this.argv = argv;
+        this.invoker = invoker;
     }
 
     async getAgent(action) {
diff --git a/src/debugger.js b/src/debugger.js
index fc24b11..415bde9 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -82,7 +82,7 @@ class Debugger {
                 await this.agentMgr.restoreAction();
             }
 
-            await this.agentMgr.installAgent(action);
+            await this.agentMgr.installAgent(action, this.invoker);
 
             if (this.argv.onStart) {
                 console.log("On start:", this.argv.onStart);
diff --git a/test/ngrok.test.js b/test/ngrok.test.js
index 0ae21b3..a095659 100644
--- a/test/ngrok.test.js
+++ b/test/ngrok.test.js
@@ -126,20 +126,19 @@ describe('ngrok',  function() {
 
     it("should handle action invocation using ngrok", async function() {
         const actionName = "myaction";
+        // should not use this code if we specify local sources which return CORRECT
+        const code = `const main = () => ({ msg: 'WRONG' });`;
 
         // port of the local server started by wskdebug to be expecting calls from ngrok
         // which we will do in this test
-        let ngrokServerPort;
+        let ngrokServerPort, ngrokKillInvoked, ngrokAuth;
         mockNgrokLibrary(function(opts) {
             ngrokServerPort = opts.addr;
             return "https://UNIT_TEST.ngrok.io";
+        }, function() {
+            ngrokKillInvoked = true;
         });
 
-        // should not use this code if we specify local sources which return CORRECT
-        const code = `const main = () => ({ msg: 'WRONG' });`;
-
-        let ngrokAuth;
-
         test.mockAction(actionName, code);
         test.mockCreateBackupAction(actionName);
 
@@ -156,8 +155,11 @@ describe('ngrok',  function() {
             .matchHeader("authorization", test.openwhiskApiAuthHeader())
             .reply(200, test.nodejsActionDescription(actionName));
 
+        test.mockReadBackupAction(actionName);
+        test.mockRestoreAction(actionName);
+        test.mockRemoveBackupAction(actionName);
 
-        // wskdebug myaction --ngrok -p ${test.port}
+        // wskdebug myaction action.js --ngrok -p ${test.port}
         const argv = {
             port: test.port,
             action: actionName,
@@ -174,7 +176,7 @@ describe('ngrok',  function() {
         await test.sleep(10);
 
         try {
-
+            // simulate invocation coming in via ngrok forwarding
             const response = await fetch(`http://127.0.0.1:${ngrokServerPort}`, {
                 method: "POST",
                 headers: {
@@ -185,6 +187,7 @@ describe('ngrok',  function() {
                 })
             });
 
+            // ensure correct result
             assert.strictEqual(response.status, 200);
             const result = await response.json();
             assert.strictEqual(result.msg, "CORRECT");
@@ -193,6 +196,7 @@ describe('ngrok',  function() {
             await dbgr.stop();
         }
 
+        assert(ngrokKillInvoked);
         assert(nock.isDone(), "Expected these HTTP requests: " + nock.pendingMocks().join());
     });
 });


[openwhisk-wskdebug] 04/07: ensure latest yargs 15.3.0 is used as #17 depends on it

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit c07093d40b9312fa2c9273c9f6ea45630e1994e7
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Sun Mar 29 00:19:10 2020 -0700

    ensure latest yargs 15.3.0 is used as #17 depends on it
---
 package-lock.json | 14 +++++++-------
 package.json      |  2 +-
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 83451a2..f2f7890 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3524,9 +3524,9 @@
             "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
         },
         "yargs": {
-            "version": "15.1.0",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz",
-            "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==",
+            "version": "15.3.1",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
+            "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
             "requires": {
                 "cliui": "^6.0.0",
                 "decamelize": "^1.2.0",
@@ -3538,7 +3538,7 @@
                 "string-width": "^4.2.0",
                 "which-module": "^2.0.0",
                 "y18n": "^4.0.0",
-                "yargs-parser": "^16.1.0"
+                "yargs-parser": "^18.1.1"
             },
             "dependencies": {
                 "emoji-regex": {
@@ -3592,9 +3592,9 @@
                     }
                 },
                 "yargs-parser": {
-                    "version": "16.1.0",
-                    "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz",
-                    "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==",
+                    "version": "18.1.2",
+                    "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
+                    "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
                     "requires": {
                         "camelcase": "^5.0.0",
                         "decamelize": "^1.2.0"
diff --git a/package.json b/package.json
index 1149666..4e43645 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
         "manakin": "^0.5.2",
         "ngrok": "^3.2.7",
         "openwhisk": "^3.21.1",
-        "yargs": "^15.1.0"
+        "yargs": "^15.3.1"
     },
     "devDependencies": {
         "chmodr": "^1.2.0",


[openwhisk-wskdebug] 07/07: fix duplicated shutdown() issue on CTRL+C

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit 397aa85b1c1465116b9edcd0c791610c2f19fa39
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Tue Mar 31 00:33:36 2020 -0700

    fix duplicated shutdown() issue on CTRL+C
---
 src/agentmgr.js |  2 --
 src/debugger.js | 11 +++++++++--
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/agentmgr.js b/src/agentmgr.js
index 8753f41..8b59f28 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -72,8 +72,6 @@ async function deleteActionIfExists(wsk, name) {
 }
 
 
-// TODO: test wskdebug manually
-// TODO: openwhiskSupports() into separate shared class
 class AgentMgr {
 
     constructor(argv, wsk, actionName) {
diff --git a/src/debugger.js b/src/debugger.js
index 415bde9..8cf08fb 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -116,6 +116,7 @@ class Debugger {
     async _run() {
         try {
             this.running = true;
+            this.shuttingDown = false;
 
             // main blocking loop
             // abort if this.running is set to false
@@ -132,7 +133,6 @@ class Debugger {
                     // wait for activation, run it, complete, repeat
                     const activation = await this.agentMgr.waitForActivations();
                     if (!activation) {
-                        // this.running = false;
                         return;
                     }
 
@@ -149,7 +149,6 @@ class Debugger {
 
                     // pass on the local result to the agent in openwhisk
                     if (!await this.agentMgr.completeActivation(id, result, duration)) {
-                        // this.running = false;
                         return;
                     }
                 }
@@ -159,6 +158,7 @@ class Debugger {
         }
     }
 
+    // normal graceful stop() initiated by a client
     async stop() {
         this.running = false;
         if (this.agentMgr) {
@@ -174,6 +174,7 @@ class Debugger {
         }
     }
 
+    // fastest way to end, triggered by CTRL+C
     async kill() {
         this.running = false;
         if (this.agentMgr) {
@@ -184,6 +185,12 @@ class Debugger {
     }
 
     async shutdown() {
+        // avoid duplicate shutdown on CTRL+C
+        if (this.shuttingDown) {
+            return;
+        }
+        this.shuttingDown = true;
+
         // only log this if we started properly
         if (this.ready) {
             console.log();


[openwhisk-wskdebug] 01/07: refactor huge debugger.js into separate files & classes

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit 0097ab1b6a297179921c9b4d0d543aba5ab52c3c
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Sat Mar 28 23:49:00 2020 -0700

    refactor huge debugger.js into separate files & classes
---
 .eslintignore   |   1 +
 src/agentmgr.js | 505 +++++++++++++++++++++++++++++++++++++++++
 src/debugger.js | 692 +++-----------------------------------------------------
 src/ngrok.js    | 139 ++++++++++++
 src/watcher.js  | 123 ++++++++++
 5 files changed, 800 insertions(+), 660 deletions(-)

diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..c795b05
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+build
\ No newline at end of file
diff --git a/src/agentmgr.js b/src/agentmgr.js
new file mode 100644
index 0000000..cb0e5a9
--- /dev/null
+++ b/src/agentmgr.js
@@ -0,0 +1,505 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const NgrokAgent = require('./ngrok');
+const fs = require('fs-extra');
+const sleep = require('util').promisify(setTimeout);
+
+function getAnnotation(action, key) {
+    const a = action.annotations.find(a => a.key === key);
+    if (a) {
+        return a.value;
+    }
+}
+
+function getActionCopyName(name) {
+    return `${name}_wskdebug_original`;
+}
+
+function isAgent(action) {
+    return getAnnotation(action, "wskdebug") ||
+           (getAnnotation(action, "description") || "").startsWith("wskdebug agent.");
+}
+
+function getActivationError(e) {
+    if (e.error && e.error.response && e.error.response.result && e.error.response.result.error) {
+        return e.error.response.result.error;
+    }
+    return {};
+}
+
+async function getWskActionWithoutCode(wsk, actionName) {
+    try {
+        return await wsk.actions.get({name: actionName, code:false});
+    } catch (e) {
+        if (e.statusCode === 404) {
+            return null;
+        } else {
+            throw e;
+        }
+    }
+}
+
+async function actionExists(wsk, name) {
+    try {
+        await wsk.actions.get({name: name, code: false});
+        return true;
+    } catch (e) {
+        return false;
+    }
+}
+
+async function deleteActionIfExists(wsk, name) {
+    if (await actionExists(wsk, name)) {
+        await wsk.actions.delete(name);
+    }
+}
+
+
+// TODO: test wskdebug manually
+// TODO: openwhiskSupports() into separate shared class
+class AgentMgr {
+
+    constructor(argv, wsk, actionName) {
+        this.argv = argv;
+        this.wsk = wsk;
+        this.actionName = actionName;
+        this.polling = true;
+    }
+
+    async readAction() {
+        if (this.argv.verbose) {
+            console.log(`Getting action metadata from OpenWhisk: ${this.actionName}`);
+        }
+        let action = await getWskActionWithoutCode(this.wsk, this.actionName);
+        if (action === null) {
+            throw new Error(`Action not found: ${this.actionName}`);
+        }
+
+        let agentAlreadyInstalled = false;
+
+        // check if this actoin needs to
+        if (isAgent(action)) {
+            // ups, action is our agent, not the original
+            // happens if a previous wskdebug was killed and could not restore before it exited
+            const backupName = getActionCopyName(this.actionName);
+
+            // check the backup action
+            try {
+                const backup = await this.wsk.actions.get(backupName);
+
+                if (isAgent(backup)) {
+                    // backup is also an agent (should not happen)
+                    // backup is useless, delete it
+                    // await this.wsk.actions.delete(backupName);
+                    throw new Error(`Dang! Agent is already installed and action backup is broken (${backupName}).\n\nPlease redeploy your action first before running wskdebug again.`);
+
+                } else {
+                    console.warn("Agent was already installed, but backup is still present. All good.");
+
+                    // need to look at the original action
+                    action = backup;
+                    agentAlreadyInstalled = true;
+                    this.agentInstalled = true;
+                }
+
+            } catch (e) {
+                if (e.statusCode === 404) {
+                    // backup missing
+                    throw new Error(`Dang! Agent is already installed and action backup is gone (${backupName}).\n\nPlease redeploy your action first before running wskdebug again.`);
+
+                } else {
+                    // other error
+                    throw e;
+                }
+            }
+        }
+        return {action, agentAlreadyInstalled };
+    }
+
+    async installAgent(action) {
+        this.agentInstalled = true;
+
+        let agentName;
+
+        // choose the right agent implementation
+        let agentCode;
+        if (this.argv.ngrok) {
+            // user manually requested ngrok
+
+            this.ngrokAgent = new NgrokAgent(this.argv);
+
+            // agent using ngrok for forwarding
+            agentName = "ngrok";
+            agentCode = await this.ngrokAgent.getAgent(action);
+
+        } else {
+            this.concurrency = await this.openwhiskSupports("concurrency");
+            if (this.concurrency) {
+                // normal fast agent using concurrent node.js actions
+                agentName = "concurrency";
+                agentCode = await this.getConcurrencyAgent();
+
+            } else {
+                console.log("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
+
+                agentName = "polling activation db";
+                agentCode = await this.getPollingActivationRecordAgent();
+            }
+        }
+
+        const backupName = getActionCopyName(this.actionName);
+
+        if (this.argv.verbose) {
+            console.log(`Installing agent in OpenWhisk (${agentName})...`);
+        }
+
+        // create copy
+        await this.wsk.actions.update({
+            name: backupName,
+            action: action
+        });
+
+        if (this.argv.verbose) {
+            console.log(`Original action backed up at ${backupName}.`);
+        }
+
+        if (this.argv.condition) {
+            action.parameters.push({
+                key: "$condition",
+                value: this.argv.condition
+            });
+        }
+
+        await this.pushAgent(action, agentCode, backupName);
+
+        if (this.argv.verbose) {
+            console.log(`Agent installed.`);
+        }
+    }
+
+    stop() {
+        this.polling = false;
+    }
+
+    async shutdown() {
+        try {
+            await this.restoreAction();
+        } finally {
+            if (this.ngrokAgent) {
+                await this.ngrokAgent.stop();
+            }
+        }
+    }
+
+    // --------------------------------------< polling >-------------------
+
+    async waitForActivations() {
+        this.activationsSeen = this.activationsSeen || {};
+
+        // secondary loop to get next activation
+        // the $waitForActivation agent activation will block, but only until
+        // it times out, hence we need to retry when it fails
+        while (this.polling) {
+            if (this.argv.verbose) {
+                process.stdout.write(".");
+            }
+            try {
+                let activation;
+                if (this.concurrency) {
+                    // invoke - blocking for up to 1 minute
+                    activation = await this.wsk.actions.invoke({
+                        name: this.actionName,
+                        params: {
+                            $waitForActivation: true
+                        },
+                        blocking: true
+                    });
+
+                } else {
+                    // poll for the newest activation
+                    const since = Date.now();
+
+                    // older openwhisk only allows the name of an action when filtering activations
+                    // newer openwhisk versions want package/name
+                    let name = this.actionName;
+                    if (await this.openwhiskSupports("activationListFilterOnlyBasename")) {
+                        if (this.actionName.includes("/")) {
+                            name = this.actionName.substring(this.actionName.lastIndexOf("/") + 1);
+                        }
+                    }
+
+                    while (true) {
+                        if (this.argv.verbose) {
+                            process.stdout.write(".");
+                        }
+
+                        const activations = await this.wsk.activations.list({
+                            name: `${name}_wskdebug_invoked`,
+                            since: since,
+                            limit: 1, // get the most recent one only
+                            docs: true // include results
+                        });
+
+                        if (activations && activations.length >= 1) {
+                            const a = activations[0];
+                            if (a.response && a.response.result && !this.activationsSeen[a.activationId]) {
+                                activation = a;
+                                break;
+                            }
+                        }
+
+                        // need to limit load on openwhisk (activation list)
+                        await sleep(1000);
+                    }
+                }
+
+                // check for successful response with a new activation
+                if (activation && activation.response) {
+                    const params = activation.response.result;
+
+                    // mark this as seen so we don't reinvoke it
+                    this.activationsSeen[activation.activationId] = true;
+
+                    if (this.argv.verbose) {
+                        console.log();
+                        console.info(`Activation: ${params.$activationId}`);
+                        console.log(params);
+                    } else {
+                        console.info(`Activation: ${params.$activationId}`);
+                    }
+                    return params;
+
+                } else if (activation && activation.activationId) {
+                    // ignore this and retry.
+                    // usually means the action did not respond within one second,
+                    // which in turn is unlikely for the agent who should exit itself
+                    // after 50 seconds, so can only happen if there was some delay
+                    // outside the action itself
+
+                } else {
+                    // unexpected, just log and retry
+                    console.log("Unexpected empty response while waiting for new activations:", activation);
+                }
+
+            } catch (e) {
+                // look for special error codes from agent
+                const errorCode = getActivationError(e).code;
+                // 42 => retry
+                if (errorCode === 42) {
+                    // do nothing
+                } else if (errorCode === 43) {
+                    // 43 => graceful shutdown (for unit tests)
+                    console.log("Graceful shutdown requested by agent (only for unit tests)");
+                    return null;
+                } else {
+                    // otherwise log error and abort
+                    console.error();
+                    console.error("Unexpected error while polling agent for activation:");
+                    console.dir(e, { depth: null });
+                    throw new Error("Unexpected error while polling agent for activation.");
+                }
+            }
+
+            // some small wait to avoid too many requests in case things run amok
+            await sleep(100);
+        }
+    }
+
+    async completeActivation(activationId, result, duration) {
+        console.info(`Completed activation ${activationId} in ${duration/1000.0} sec`);
+        if (this.argv.verbose) {
+            console.log(result);
+        }
+
+        try {
+            result.$activationId = activationId;
+            await this.wsk.actions.invoke({
+                name: this.concurrency ? this.actionName : `${this.actionName}_wskdebug_completed`,
+                params: result,
+                blocking: true
+            });
+        } catch (e) {
+            // look for special error codes from agent
+            const errorCode = getActivationError(e).code;
+            // 42 => retry
+            if (errorCode === 42) {
+                // do nothing
+            } else if (errorCode === 43) {
+                // 43 => graceful shutdown (for unit tests)
+                console.log("Graceful shutdown requested by agent (only for unit tests)");
+                return false;
+            } else {
+                console.error("Unexpected error while completing activation:", e);
+            }
+        }
+        return true;
+    }
+
+    // --------------------------------------< restoring >------------------
+
+    async restoreAction() {
+        if (this.agentInstalled) {
+            if (this.argv.verbose) {
+                console.log();
+                console.log(`Restoring action`);
+            }
+
+            const copy = getActionCopyName(this.actionName);
+
+            try {
+                const original = await this.wsk.actions.get(copy);
+
+                // copy the backup (copy) to the regular action
+                await this.wsk.actions.update({
+                    name: this.actionName,
+                    action: original
+                });
+
+                // remove the backup
+                await this.wsk.actions.delete(copy);
+
+                // remove any helpers if they exist
+                await deleteActionIfExists(this.wsk, `${this.actionName}_wskdebug_invoked`);
+                await deleteActionIfExists(this.wsk, `${this.actionName}_wskdebug_completed`);
+
+            } catch (e) {
+                console.error("Error while restoring original action:", e);
+            }
+        }
+    }
+
+    // --------------------------------------< agent types >------------------
+
+    async getConcurrencyAgent() {
+        return fs.readFileSync(`${__dirname}/../agent/agent-concurrency.js`, {encoding: 'utf8'});
+    }
+
+    async getPollingActivationRecordAgent() {
+        // this needs 2 helper actions in addition to the agent in place of the action
+        await this.createHelperAction(`${this.actionName}_wskdebug_invoked`,   `${__dirname}/../agent/echo.js`);
+        await this.createHelperAction(`${this.actionName}_wskdebug_completed`, `${__dirname}/../agent/echo.js`);
+
+        let agentCode = fs.readFileSync(`${__dirname}/../agent/agent-activationdb.js`, {encoding: 'utf8'});
+        // rewrite the code to pass config (we want to avoid fiddling with default params of the action)
+        if (await this.openwhiskSupports("activationListFilterOnlyBasename")) {
+            agentCode = agentCode.replace("const activationListFilterOnlyBasename = false;", "const activationListFilterOnlyBasename = true;");
+        }
+        return agentCode;
+    }
+
+    async pushAgent(action, agentCode, backupName) {
+        // overwrite action with agent
+
+        // this is to support older openwhisks for which nodejs:default is less than version 8
+        const nodejs8 = await this.openwhiskSupports("nodejs8");
+
+        await this.wsk.actions.update({
+            name: this.actionName,
+            action: {
+                exec: {
+                    kind: nodejs8 ? "nodejs:default" : "blackbox",
+                    image: nodejs8 ? undefined : "openwhisk/action-nodejs-v8",
+                    code: agentCode
+                },
+                limits: {
+                    timeout: (this.argv.agentTimeout || 300) * 1000,
+                    concurrency: this.concurrency ? 200: 1
+                },
+                annotations: [
+                    ...action.annotations,
+                    { key: "provide-api-key", value: true },
+                    { key: "wskdebug", value: true },
+                    { key: "description", value: `wskdebug agent. temporarily installed over original action. original action backup at ${backupName}.` }
+                ],
+                parameters: action.parameters
+            }
+        });
+    }
+
+    async createHelperAction(actionName, file) {
+        const nodejs8 = await this.openwhiskSupports("nodejs8");
+
+        await this.wsk.actions.update({
+            name: actionName,
+            action: {
+                exec: {
+                    kind: nodejs8 ? "nodejs:default" : "blackbox",
+                    image: nodejs8 ? undefined : "openwhisk/action-nodejs-v8",
+                    code: fs.readFileSync(file, {encoding: 'utf8'})
+                },
+                limits: {
+                    timeout: (this.argv.agentTimeout || 300) * 1000
+                },
+                annotations: [
+                    { key: "description", value: `wskdebug agent helper. temporarily installed.` }
+                ]
+            }
+        });
+    }
+
+    // ----------------------------------------< openwhisk feature detection >-----------------
+
+    async getOpenWhiskVersion() {
+        if (this.openwhiskVersion === undefined) {
+            try {
+                const json = await this.wsk.actions.client.request("GET", "/api/v1");
+                if (json && typeof json.build === "string") {
+                    this.openwhiskVersion = json.build;
+                } else {
+                    this.openwhiskVersion = null;
+                }
+            } catch (e) {
+                console.warn("Could not retrieve OpenWhisk version:", e.message);
+                this.openwhiskVersion = null;
+            }
+        }
+        return this.openwhiskVersion;
+    }
+
+    async openwhiskSupports(feature) {
+        const FEATURES = {
+            // guesstimated
+            activationListFilterOnlyBasename: v => v.startsWith("2018") || v.startsWith("2017"),
+            // hack
+            nodejs8: v => !v.startsWith("2018") && !v.startsWith("2017"),
+            concurrency: async (_, wsk) => {
+                // check swagger api docs instead of version to see if concurrency is supported
+                try {
+                    const swagger = await wsk.actions.client.request("GET", "/api/v1/api-docs");
+
+                    if (swagger && swagger.definitions && swagger.definitions.ActionLimits && swagger.definitions.ActionLimits.properties) {
+                        return swagger.definitions.ActionLimits.properties.concurrency;
+                    }
+                } catch (e) {
+                    console.warn('Could not read /api/v1/api-docs, setting max action concurrency to 1')
+                    return false;
+                }
+            }
+        };
+        const checker = FEATURES[feature];
+        if (checker) {
+            return checker(await this.getOpenWhiskVersion(), this.wsk);
+        } else {
+            throw new Error("Unknown feature " + feature);
+        }
+    }
+}
+
+module.exports = AgentMgr;
\ No newline at end of file
diff --git a/src/debugger.js b/src/debugger.js
index 86bc8ec..e43c8e8 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -17,33 +17,21 @@
 
 'use strict';
 
-const openwhisk = require("openwhisk");
 const wskprops = require('./wskprops');
-const fs = require('fs-extra');
 const OpenWhiskInvoker = require('./invoker');
+const AgentMgr = require('./agentmgr');
+const Watcher = require('./watcher');
+const openwhisk = require('openwhisk');
 const { spawnSync } = require('child_process');
-const livereload = require('livereload');
-const http = require('http');
-const ngrok = require('ngrok');
-const url = require('url');
-const util = require('util');
-const crypto = require("crypto");
-
-async function sleep(millis) {
-    return new Promise(resolve => setTimeout(resolve, millis));
-}
-
-function getAnnotation(action, key) {
-    const a = action.annotations.find(a => a.key === key);
-    if (a) {
-        return a.value;
-    }
-}
+const sleep = require('util').promisify(setTimeout);
 
+/**
+ * Central component of wskdebug.
+ */
 class Debugger {
     constructor(argv) {
         this.argv = argv;
-        this.action = argv.action;
+        this.actionName = argv.action;
 
         this.wskProps = wskprops.get();
         if (argv.ignoreCerts) {
@@ -54,16 +42,19 @@ class Debugger {
     async start() {
         await this.setupWsk();
 
+        this.agentMgr = new AgentMgr(this.argv, this.wsk, this.actionName);
+        this.watcher = new Watcher(this.argv, this.wsk);
+
         // quick fail for missing requirements such as docker not running
         await OpenWhiskInvoker.checkIfAvailable();
 
-        console.info(`Starting debugger for /${this.wskProps.namespace}/${this.action}`);
+        console.info(`Starting debugger for /${this.wskProps.namespace}/${this.actionName}`);
 
         // get the action
-        const { action, agentAlreadyInstalled } = await this.getAction(this.action);
+        const { action, agentAlreadyInstalled } = await this.agentMgr.readAction();
 
         // local debug container
-        this.invoker = new OpenWhiskInvoker(this.action, action, this.argv, this.wskProps, this.wsk);
+        this.invoker = new OpenWhiskInvoker(this.actionName, action, this.argv, this.wskProps, this.wsk);
 
         try {
             // run build initially (would be required by starting container)
@@ -77,9 +68,9 @@ class Debugger {
 
             // get code and /init local container
             if (this.argv.verbose) {
-                console.log(`Fetching action code from OpenWhisk: ${this.action}`);
+                console.log(`Fetching action code from OpenWhisk: ${this.actionName}`);
             }
-            const actionWithCode = await this.wsk.actions.get(this.action);
+            const actionWithCode = await this.wsk.actions.get(this.actionName);
             action.exec = actionWithCode.exec;
             await this.invoker.init(actionWithCode);
 
@@ -88,21 +79,21 @@ class Debugger {
             // user can switch between agents (ngrok or not), hence we need to restore
             // (better would be to track the agent + its version and avoid a restore, but that's TBD)
             if (agentAlreadyInstalled) {
-                await this.restoreAction(this.action);
+                await this.agentMgr.restoreAction();
             }
 
-            await this.installAgent(this.action, action);
+            await this.agentMgr.installAgent(action);
 
             if (this.argv.onStart) {
                 console.log("On start:", this.argv.onStart);
                 spawnSync(this.argv.onStart, {shell: true, stdio: "inherit"});
             }
 
-            // start live reload (if requested)
-            await this.startSourceWatching();
+            // start source watching (live reload) if requested
+            await this.watcher.start();
 
             console.log();
-            console.info(`Action     : ${this.action}`);
+            console.info(`Action     : ${this.actionName}`);
             this.invoker.logInfo();
             if (this.argv.condition) {
                 console.info(`Condition  : ${this.argv.condition}`);
@@ -139,9 +130,9 @@ class Debugger {
                     // agent: concurrent
                     // agent: non-concurrent
                     // wait for activation, run it, complete, repeat
-                    const activation = await this.waitForActivations(this.action);
+                    const activation = await this.agentMgr.waitForActivations();
                     if (!activation) {
-                        this.running = false;
+                        // this.running = false;
                         return;
                     }
 
@@ -157,7 +148,10 @@ class Debugger {
                     const duration = Date.now() - startTime;
 
                     // pass on the local result to the agent in openwhisk
-                    await this.completeActivation(this.action, id, result, duration);
+                    if (!await this.agentMgr.completeActivation(id, result, duration)) {
+                        // this.running = false;
+                        return;
+                    }
                 }
             }
         } finally {
@@ -167,6 +161,8 @@ class Debugger {
 
     async stop() {
         this.running = false;
+        this.agentMgr.stop();
+
         if (this.runPromise) {
             // wait for the main loop to gracefully end, which will call shutdown()
             await this.runPromise;
@@ -178,6 +174,7 @@ class Debugger {
 
     async kill() {
         this.running = false;
+        this.agentMgr.stop();
 
         await this.shutdown();
     }
@@ -192,29 +189,9 @@ class Debugger {
 
         // need to shutdown everything even if some fail, hence tryCatch() for each
 
-        if (this.action) {
-            await this.tryCatch(this.restoreAction(this.action));
-        }
+        await this.tryCatch(this.agentMgr.shutdown());
         await this.tryCatch(this.invoker.stop());
-
-        if (this.liveReloadServer) {
-            await this.tryCatch(() => {
-                if (this.liveReloadServer.server) {
-                    this.liveReloadServer.close();
-                } else {
-                    this.liveReloadServer.watcher.close();
-                }
-                this.liveReloadServer = null;
-            });
-        }
-
-        if (this.ngrokServer) {
-            await this.tryCatch(() => {
-                this.ngrokServer.close();
-                this.ngrokServer = null;
-            });
-        }
-        await this.tryCatch(ngrok.kill());
+        await this.tryCatch(this.watcher.stop());
 
         // only log this if we started properly
         if (this.ready) {
@@ -246,611 +223,6 @@ class Debugger {
         }
     }
 
-    async getWskActionWithoutCode(actionName) {
-        if (this.argv.verbose) {
-            console.log(`Getting action metadata from OpenWhisk: ${actionName}`);
-        }
-        try {
-            return await this.wsk.actions.get({name: actionName, code:false});
-        } catch (e) {
-            if (e.statusCode === 404) {
-                return null;
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    async actionExists(name) {
-        try {
-            await this.wsk.actions.get({name: name, code: false});
-            return true;
-        } catch (e) {
-            return false;
-        }
-    }
-
-    async deleteActionIfExists(name) {
-        if (await this.actionExists(name)) {
-            await this.wsk.actions.delete(name);
-        }
-    }
-
-    // ------------------------------------------------< agent >------------------
-
-    getActionCopyName(name) {
-        return `${name}_wskdebug_original`;
-    }
-
-    isAgent(action) {
-        return getAnnotation(action, "wskdebug") ||
-               (getAnnotation(action, "description") || "").startsWith("wskdebug agent.");
-    }
-
-    async getAction(actionName) {
-        let action = await this.getWskActionWithoutCode(actionName);
-        if (action === null) {
-            throw new Error(`Action not found: ${actionName}`);
-        }
-
-        let agentAlreadyInstalled = false;
-
-        // check if this actoin needs to
-        if (this.isAgent(action)) {
-            // ups, action is our agent, not the original
-            // happens if a previous wskdebug was killed and could not restore before it exited
-            const backupName = this.getActionCopyName(actionName);
-
-            // check the backup action
-            try {
-                const backup = await this.wsk.actions.get(backupName);
-
-                if (this.isAgent(backup)) {
-                    // backup is also an agent (should not happen)
-                    // backup is useless, delete it
-                    // await this.wsk.actions.delete(backupName);
-                    throw new Error(`Dang! Agent is already installed and action backup is broken (${backupName}).\n\nPlease redeploy your action first before running wskdebug again.`);
-
-                } else {
-                    console.warn("Agent was already installed, but backup is still present. All good.");
-
-                    // need to look at the original action
-                    action = backup;
-                    agentAlreadyInstalled = true;
-                    this.agentInstalled = true;
-                }
-
-            } catch (e) {
-                if (e.statusCode === 404) {
-                    // backup missing
-                    throw new Error(`Dang! Agent is already installed and action backup is gone (${backupName}).\n\nPlease redeploy your action first before running wskdebug again.`);
-
-                } else {
-                    // other error
-                    throw e;
-                }
-            }
-        }
-        return {action, agentAlreadyInstalled };
-    }
-
-    async createHelperAction(actionName, file) {
-        const nodejs8 = await this.openwhiskSupports("nodejs8");
-
-        await this.wsk.actions.update({
-            name: actionName,
-            action: {
-                exec: {
-                    kind: nodejs8 ? "nodejs:default" : "blackbox",
-                    image: nodejs8 ? undefined : "openwhisk/action-nodejs-v8",
-                    code: fs.readFileSync(file, {encoding: 'utf8'})
-                },
-                limits: {
-                    timeout: (this.argv.agentTimeout || 300) * 1000
-                },
-                annotations: [
-                    { key: "description", value: `wskdebug agent helper. temporarily installed.` }
-                ]
-            }
-        });
-    }
-
-    async installAgent(actionName, action) {
-        this.agentInstalled = true;
-
-        const agentDir = `${__dirname}/../agent`;
-        let agentName;
-
-        // choose the right agent implementation
-        let code;
-        if (this.argv.ngrok) {
-            // user manually requested ngrok
-            if (this.argv.verbose) {
-                console.log("Setting up ngrok", this.argv.ngrokRegion ? `(region: ${this.argv.ngrokRegion})` : "");
-            }
-
-            // 1. start local server on random port
-            this.ngrokServer = http.createServer(this.ngrokHandler.bind(this));
-            // turn server.listen() into promise so we can await
-            const listen = util.promisify( this.ngrokServer.listen.bind(this.ngrokServer) );
-            await listen(0, '127.0.0.1');
-
-            // 2. start ngrok tunnel connected to that port
-            this.ngrokServerPort = this.ngrokServer.address().port;
-
-            // create a unique authorization token that we check on our local instance later
-            // this adds extra protection on top of the uniquely generated ngrok subdomain (e.g. a01ae275.ngrok.io)
-            this.ngrokAuth = crypto.randomBytes(32).toString("hex");
-            const ngrokUrl = await ngrok.connect({
-                addr: this.ngrokServerPort,
-                region: this.argv.ngrokRegion
-            });
-
-            // 3. pass on public ngrok url to agent
-            action.parameters.push({
-                key: "$ngrokUrl",
-                value: url.parse(ngrokUrl).host
-            });
-            action.parameters.push({
-                key: "$ngrokAuth",
-                value: this.ngrokAuth
-            });
-
-            console.log(`Ngrok forwarding: ${ngrokUrl} => http://localhost:${this.ngrokServerPort} (auth: ${this.ngrokAuth})`);
-
-            // agent using ngrok for forwarding
-            agentName = "ngrok";
-            code = fs.readFileSync(`${agentDir}/agent-ngrok.js`, {encoding: 'utf8'});
-
-        } else {
-            this.concurrency = await this.openwhiskSupports("concurrency");
-            if (this.concurrency) {
-                // normal fast agent using concurrent node.js actions
-                agentName = "concurrency";
-                code = fs.readFileSync(`${agentDir}/agent-concurrency.js`, {encoding: 'utf8'});
-
-            } else {
-                console.log("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
-
-                agentName = "polling activation db";
-
-                // this needs 2 helper actions in addition to the agent in place of the action
-                await this.createHelperAction(`${actionName}_wskdebug_invoked`,   `${agentDir}/echo.js`);
-                await this.createHelperAction(`${actionName}_wskdebug_completed`, `${agentDir}/echo.js`);
-
-                code = fs.readFileSync(`${agentDir}/agent-activationdb.js`, {encoding: 'utf8'});
-                // rewrite the code to pass config (we want to avoid fiddling with default params of the action)
-                if (await this.openwhiskSupports("activationListFilterOnlyBasename")) {
-                    code = code.replace("const activationListFilterOnlyBasename = false;", "const activationListFilterOnlyBasename = true;");
-                }
-            }
-        }
-
-        const backupName = this.getActionCopyName(actionName);
-
-        if (this.argv.verbose) {
-            console.log(`Installing agent in OpenWhisk (${agentName})...`);
-        }
-
-        // create copy
-        await this.wsk.actions.update({
-            name: backupName,
-            action: action
-        });
-
-        if (this.argv.verbose) {
-            console.log(`Original action backed up at ${backupName}.`);
-        }
-
-        // this is to support older openwhisks for which nodejs:default is less than version 8
-        const nodejs8 = await this.openwhiskSupports("nodejs8");
-
-        if (this.argv.condition) {
-            action.parameters.push({
-                key: "$condition",
-                value: this.argv.condition
-            });
-        }
-
-        // overwrite action with agent
-        await this.wsk.actions.update({
-            name: actionName,
-            action: {
-                exec: {
-                    kind: nodejs8 ? "nodejs:default" : "blackbox",
-                    image: nodejs8 ? undefined : "openwhisk/action-nodejs-v8",
-                    code: code
-                },
-                limits: {
-                    timeout: (this.argv.agentTimeout || 300) * 1000,
-                    concurrency: this.concurrency ? 200: 1
-                },
-                annotations: [
-                    ...action.annotations,
-                    { key: "provide-api-key", value: true },
-                    { key: "wskdebug", value: true },
-                    { key: "description", value: `wskdebug agent. temporarily installed over original action. original action backup at ${backupName}.` }
-                ],
-                parameters: action.parameters
-            }
-        });
-
-        if (this.argv.verbose) {
-            console.log(`Agent installed.`);
-        }
-    }
-
-    async restoreAction(actionName) {
-        if (this.agentInstalled) {
-            if (this.argv.verbose) {
-                console.log();
-                console.log(`Restoring action`);
-            }
-
-            const copy = this.getActionCopyName(actionName);
-
-            try {
-                const original = await this.wsk.actions.get(copy);
-
-                // copy the backup (copy) to the regular action
-                await this.wsk.actions.update({
-                    name: actionName,
-                    action: original
-                });
-
-                // remove the backup
-                await this.wsk.actions.delete(copy);
-
-                // remove any helpers if they exist
-                await this.deleteActionIfExists(`${actionName}_wskdebug_invoked`);
-                await this.deleteActionIfExists(`${actionName}_wskdebug_completed`);
-
-            } catch (e) {
-                console.error("Error while restoring original action:", e);
-            }
-        }
-    }
-
-    // ------------------------------------------------< ngrok >------------------
-
-    // local http server retrieving forwards from the ngrok agent, running them
-    // as a blocking local invocation and then returning the activation result back
-    ngrokHandler(req, res) {
-        // check authorization against our unique token
-        const authHeader = req.headers.authorization;
-        if (authHeader !== this.ngrokAuth) {
-            res.statusCode = 401;
-            res.end();
-            return;
-        }
-
-        if (req.method === 'POST') {
-            // agent POSTs arguments as json body
-            let body = '';
-            // collect full request body first
-            req.on('data', chunk => {
-                body += chunk.toString();
-            });
-            req.on('end', async () => {
-                try {
-                    const params = JSON.parse(body);
-                    const id = params.$activationId;
-                    delete params.$activationId;
-
-                    if (this.argv.verbose) {
-                        console.log();
-                        console.info(`Activation: ${id}`);
-                        console.log(params);
-                    } else {
-                        console.info(`Activation: ${id}`);
-                    }
-
-                    const startTime = Date.now();
-
-                    const result = await this.invoker.run(params, id);
-
-                    const duration = Date.now() - startTime;
-                    console.info(`Completed activation ${id} in ${duration/1000.0} sec`);
-                    if (this.argv.verbose) {
-                        console.log(result);
-                    }
-
-                    res.statusCode = 200;
-                    res.setHeader("Content-Type", "application/json");
-                    res.end(JSON.stringify(result));
-
-                } catch (e) {
-                    console.error(e);
-                    res.statusCode = 400;
-                    res.end();
-                }
-            });
-        } else {
-            res.statusCode = 404;
-            res.end();
-        }
-    }
-
-    // ------------------------------------------------< polling >------------------
-
-    async waitForActivations(actionName) {
-        this.activationsSeen = this.activationsSeen || {};
-
-        // secondary loop to get next activation
-        // the $waitForActivation agent activation will block, but only until
-        // it times out, hence we need to retry when it fails
-        while (this.running) {
-            if (this.argv.verbose) {
-                process.stdout.write(".");
-            }
-            try {
-                let activation;
-                if (this.concurrency) {
-                    // invoke - blocking for up to 1 minute
-                    activation = await this.wsk.actions.invoke({
-                        name: actionName,
-                        params: {
-                            $waitForActivation: true
-                        },
-                        blocking: true
-                    });
-
-                } else {
-                    // poll for the newest activation
-                    const since = Date.now();
-
-                    // older openwhisk only allows the name of an action when filtering activations
-                    // newer openwhisk versions want package/name
-                    let name = actionName;
-                    if (await this.openwhiskSupports("activationListFilterOnlyBasename")) {
-                        if (actionName.includes("/")) {
-                            name = actionName.substring(actionName.lastIndexOf("/") + 1);
-                        }
-                    }
-
-                    while (true) {
-                        if (this.argv.verbose) {
-                            process.stdout.write(".");
-                        }
-
-                        const activations = await this.wsk.activations.list({
-                            name: `${name}_wskdebug_invoked`,
-                            since: since,
-                            limit: 1, // get the most recent one only
-                            docs: true // include results
-                        });
-
-                        if (activations && activations.length >= 1) {
-                            const a = activations[0];
-                            if (a.response && a.response.result && !this.activationsSeen[a.activationId]) {
-                                activation = a;
-                                break;
-                            }
-                        }
-
-                        // need to limit load on openwhisk (activation list)
-                        await sleep(1000);
-                    }
-                }
-
-                // check for successful response with a new activation
-                if (activation && activation.response) {
-                    const params = activation.response.result;
-
-                    // mark this as seen so we don't reinvoke it
-                    this.activationsSeen[activation.activationId] = true;
-
-                    if (this.argv.verbose) {
-                        console.log();
-                        console.info(`Activation: ${params.$activationId}`);
-                        console.log(params);
-                    } else {
-                        console.info(`Activation: ${params.$activationId}`);
-                    }
-                    return params;
-
-                } else if (activation && activation.activationId) {
-                    // ignore this and retry.
-                    // usually means the action did not respond within one second,
-                    // which in turn is unlikely for the agent who should exit itself
-                    // after 50 seconds, so can only happen if there was some delay
-                    // outside the action itself
-
-                } else {
-                    // unexpected, just log and retry
-                    console.log("Unexpected empty response while waiting for new activations:", activation);
-                }
-
-            } catch (e) {
-                // look for special error codes from agent
-                const errorCode = this.getActivationError(e).code;
-                // 42 => retry
-                if (errorCode === 42) {
-                    // do nothing
-                } else if (errorCode === 43) {
-                    // 43 => graceful shutdown (for unit tests)
-                    console.log("Graceful shutdown requested by agent (only for unit tests)");
-                    return null;
-                } else {
-                    // otherwise log error and abort
-                    console.error();
-                    console.error("Unexpected error while polling agent for activation:");
-                    console.dir(e, { depth: null });
-                    throw new Error("Unexpected error while polling agent for activation.");
-                }
-            }
-
-            // some small wait to avoid too many requests in case things run amok
-            await sleep(100);
-        }
-    }
-
-    getActivationError(e) {
-        if (e.error && e.error.response && e.error.response.result && e.error.response.result.error) {
-            return e.error.response.result.error;
-        }
-        return {};
-    }
-
-    async completeActivation(actionName, activationId, result, duration) {
-        console.info(`Completed activation ${activationId} in ${duration/1000.0} sec`);
-        if (this.argv.verbose) {
-            console.log(result);
-        }
-
-        try {
-            result.$activationId = activationId;
-            await this.wsk.actions.invoke({
-                name: this.concurrency ? actionName : `${actionName}_wskdebug_completed`,
-                params: result,
-                blocking: true
-            });
-        } catch (e) {
-            // look for special error codes from agent
-            const errorCode = this.getActivationError(e).code;
-            // 42 => retry
-            if (errorCode === 42) {
-                // do nothing
-            } else if (errorCode === 43) {
-                // 43 => graceful shutdown (for unit tests)
-                console.log("Graceful shutdown requested by agent (only for unit tests)");
-                this.running = false;
-            } else {
-                console.error("Unexpected error while completing activation:", e);
-            }
-        }
-    }
-
-    // ----------------------------------------< openwhisk feature detection >-----------------
-
-    async getOpenWhiskVersion() {
-        if (this.openwhiskVersion === undefined) {
-            try {
-                const json = await this.wsk.actions.client.request("GET", "/api/v1");
-                if (json && typeof json.build === "string") {
-                    this.openwhiskVersion = json.build;
-                } else {
-                    this.openwhiskVersion = null;
-                }
-            } catch (e) {
-                console.warn("Could not retrieve OpenWhisk version:", e.message);
-                this.openwhiskVersion = null;
-            }
-        }
-        return this.openwhiskVersion;
-    }
-
-    async openwhiskSupports(feature) {
-        const FEATURES = {
-            // guesstimated
-            activationListFilterOnlyBasename: v => v.startsWith("2018") || v.startsWith("2017"),
-            // hack
-            nodejs8: v => !v.startsWith("2018") && !v.startsWith("2017"),
-            concurrency: async (_, wsk) => {
-                // check swagger api docs instead of version to see if concurrency is supported
-                try {
-                    const swagger = await wsk.actions.client.request("GET", "/api/v1/api-docs");
-
-                    if (swagger && swagger.definitions && swagger.definitions.ActionLimits && swagger.definitions.ActionLimits.properties) {
-                        return swagger.definitions.ActionLimits.properties.concurrency;
-                    }
-                } catch (e) {
-                    console.warn('Could not read /api/v1/api-docs, setting max action concurrency to 1')
-                    return false;
-                }
-            }
-        };
-        const checker = FEATURES[feature];
-        if (checker) {
-            return checker(await this.getOpenWhiskVersion(), this.wsk);
-        } else {
-            throw new Error("Unknown feature " + feature);
-        }
-    }
-
-    // ------------------------------------------------< source watching >-----------------
-
-    async startSourceWatching() {
-        const watch = this.argv.watch || process.cwd();
-        if (watch &&
-            // each of these triggers listening
-            (   this.argv.livereload
-             || this.argv.onBuild
-             || this.argv.onChange
-             || this.argv.invokeParams
-             || this.argv.invokeAction )
-        ) {
-            this.liveReloadServer = livereload.createServer({
-                port: this.argv.livereloadPort,
-                noListen: !this.argv.livereload,
-                exclusions: [this.argv.buildPath, "node_modules/**"],
-                exts: this.argv.watchExts || ["json", "js", "ts", "coffee", "py", "rb", "erb", "go", "java", "scala", "php", "swift", "rs", "cs", "bal", "php", "php5"],
-                extraExts: []
-            });
-            this.liveReloadServer.watch(watch);
-
-            // overwrite function to get notified on changes
-            const refresh = this.liveReloadServer.refresh;
-            const argv = this.argv;
-            const wsk = this.wsk;
-            this.liveReloadServer.refresh = function(filepath) {
-                try {
-                    let result = [];
-
-                    if (argv.verbose) {
-                        console.log("File modified:", filepath);
-                    }
-
-                    // call original function if we are listening
-                    if (argv.livereload) {
-                        result = refresh.call(this, filepath);
-                    }
-
-                    // run build command before invoke triggers below
-                    if (argv.onBuild) {
-                        console.info("=> Build:", argv.onBuild);
-                        spawnSync(argv.onBuild, {shell: true, stdio: "inherit"});
-                    }
-
-                    // run shell command
-                    if (argv.onChange) {
-                        console.info("=> Run:", argv.onChange);
-                        spawnSync(argv.onChange, {shell: true, stdio: "inherit"});
-                    }
-
-                    // action invoke
-                    if (argv.invokeParams || argv.invokeAction) {
-                        let json = {};
-                        if (argv.invokeParams) {
-                            if (argv.invokeParams.trim().startsWith("{")) {
-                                json = JSON.parse(argv.invokeParams);
-                            } else {
-                                json = JSON.parse(fs.readFileSync(argv.invokeParams, {encoding: 'utf8'}));
-                            }
-                        }
-                        const action = argv.invokeAction || argv.action;
-                        wsk.actions.invoke({
-                            name: action,
-                            params: json
-                        }).then(response => {
-                            console.info(`=> Invoked action ${action} with params ${argv.invokeParams}: ${response.activationId}`);
-                        }).catch(err => {
-                            console.error("Error invoking action:", err);
-                        });
-                    }
-
-                    return result;
-                } catch (e) {
-                    console.error(e);
-                }
-            };
-
-            if (this.argv.livereload) {
-                console.info(`LiveReload enabled for ${watch} on port ${this.liveReloadServer.config.port}`);
-            }
-        }
-    }
-
     // ------------------------------------------------< utils >-----------------
 
     async tryCatch(task, message="Error during shutdown:") {
diff --git a/src/ngrok.js b/src/ngrok.js
new file mode 100644
index 0000000..afa01d7
--- /dev/null
+++ b/src/ngrok.js
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs-extra');
+const http = require('http');
+const ngrok = require('ngrok');
+const url = require('url');
+const util = require('util');
+const crypto = require("crypto");
+
+class NgrokAgent {
+    constructor(argv) {
+        this.argv = argv;
+    }
+
+    async getAgent(action) {
+        if (this.argv.verbose) {
+            console.log("Setting up ngrok", this.argv.ngrokRegion ? `(region: ${this.argv.ngrokRegion})` : "");
+        }
+
+        // 1. start local server on random port
+        this.ngrokServer = http.createServer(this.ngrokHandler.bind(this));
+        // turn server.listen() into promise so we can await
+        const listen = util.promisify( this.ngrokServer.listen.bind(this.ngrokServer) );
+        await listen(0, '127.0.0.1');
+
+        // 2. start ngrok tunnel connected to that port
+        this.ngrokServerPort = this.ngrokServer.address().port;
+
+        // create a unique authorization token that we check on our local instance later
+        // this adds extra protection on top of the uniquely generated ngrok subdomain (e.g. a01ae275.ngrok.io)
+        this.ngrokAuth = crypto.randomBytes(32).toString("hex");
+        const ngrokUrl = await ngrok.connect({
+            addr: this.ngrokServerPort,
+            region: this.argv.ngrokRegion
+        });
+
+        // 3. pass on public ngrok url to agent
+        action.parameters.push({
+            key: "$ngrokUrl",
+            value: url.parse(ngrokUrl).host
+        });
+        action.parameters.push({
+            key: "$ngrokAuth",
+            value: this.ngrokAuth
+        });
+
+        console.log(`Ngrok forwarding: ${ngrokUrl} => http://localhost:${this.ngrokServerPort} (auth: ${this.ngrokAuth})`);
+
+        return fs.readFileSync(`${__dirname}/../agent/agent-ngrok.js`, {encoding: 'utf8'});
+    }
+
+    async stop() {
+        try {
+            if (this.ngrokServer) {
+                this.ngrokServer.close();
+                this.ngrokServer = null;
+            }
+        } finally {
+            await ngrok.kill();
+        }
+    }
+
+    // local http server retrieving forwards from the ngrok agent, running them
+    // as a blocking local invocation and then returning the activation result back
+    ngrokHandler(req, res) {
+        // check authorization against our unique token
+        const authHeader = req.headers.authorization;
+        if (authHeader !== this.ngrokAuth) {
+            res.statusCode = 401;
+            res.end();
+            return;
+        }
+
+        if (req.method === 'POST') {
+            // agent POSTs arguments as json body
+            let body = '';
+            // collect full request body first
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+            req.on('end', async () => {
+                try {
+                    const params = JSON.parse(body);
+                    const id = params.$activationId;
+                    delete params.$activationId;
+
+                    if (this.argv.verbose) {
+                        console.log();
+                        console.info(`Activation: ${id}`);
+                        console.log(params);
+                    } else {
+                        console.info(`Activation: ${id}`);
+                    }
+
+                    const startTime = Date.now();
+
+                    const result = await this.invoker.run(params, id);
+
+                    const duration = Date.now() - startTime;
+                    console.info(`Completed activation ${id} in ${duration/1000.0} sec`);
+                    if (this.argv.verbose) {
+                        console.log(result);
+                    }
+
+                    res.statusCode = 200;
+                    res.setHeader("Content-Type", "application/json");
+                    res.end(JSON.stringify(result));
+
+                } catch (e) {
+                    console.error(e);
+                    res.statusCode = 400;
+                    res.end();
+                }
+            });
+        } else {
+            res.statusCode = 404;
+            res.end();
+        }
+    }
+}
+
+module.exports = NgrokAgent;
\ No newline at end of file
diff --git a/src/watcher.js b/src/watcher.js
new file mode 100644
index 0000000..d69b554
--- /dev/null
+++ b/src/watcher.js
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const fs = require('fs-extra');
+const livereload = require('livereload');
+const { spawnSync } = require('child_process');
+
+class Watcher {
+    constructor(argv, wsk) {
+        this.argv = argv;
+        this.wsk = wsk;
+    }
+
+    async start() {
+        const watch = this.argv.watch || process.cwd();
+        if (watch &&
+            // each of these triggers listening
+            (   this.argv.livereload
+             || this.argv.onBuild
+             || this.argv.onChange
+             || this.argv.invokeParams
+             || this.argv.invokeAction )
+        ) {
+            this.liveReloadServer = livereload.createServer({
+                port: this.argv.livereloadPort,
+                noListen: !this.argv.livereload,
+                exclusions: [this.argv.buildPath, "node_modules/**"],
+                exts: this.argv.watchExts || ["json", "js", "ts", "coffee", "py", "rb", "erb", "go", "java", "scala", "php", "swift", "rs", "cs", "bal", "php", "php5"],
+                extraExts: []
+            });
+            this.liveReloadServer.watch(watch);
+
+            // overwrite function to get notified on changes
+            const refresh = this.liveReloadServer.refresh;
+            const argv = this.argv;
+            const wsk = this.wsk;
+            this.liveReloadServer.refresh = function(filepath) {
+                try {
+                    let result = [];
+
+                    if (argv.verbose) {
+                        console.log("File modified:", filepath);
+                    }
+
+                    // call original function if we are listening
+                    if (argv.livereload) {
+                        result = refresh.call(this, filepath);
+                    }
+
+                    // run build command before invoke triggers below
+                    if (argv.onBuild) {
+                        console.info("=> Build:", argv.onBuild);
+                        spawnSync(argv.onBuild, {shell: true, stdio: "inherit"});
+                    }
+
+                    // run shell command
+                    if (argv.onChange) {
+                        console.info("=> Run:", argv.onChange);
+                        spawnSync(argv.onChange, {shell: true, stdio: "inherit"});
+                    }
+
+                    // action invoke
+                    if (argv.invokeParams || argv.invokeAction) {
+                        let json = {};
+                        if (argv.invokeParams) {
+                            if (argv.invokeParams.trim().startsWith("{")) {
+                                json = JSON.parse(argv.invokeParams);
+                            } else {
+                                json = JSON.parse(fs.readFileSync(argv.invokeParams, {encoding: 'utf8'}));
+                            }
+                        }
+                        const action = argv.invokeAction || argv.action;
+                        wsk.actions.invoke({
+                            name: action,
+                            params: json
+                        }).then(response => {
+                            console.info(`=> Invoked action ${action} with params ${argv.invokeParams}: ${response.activationId}`);
+                        }).catch(err => {
+                            console.error("Error invoking action:", err);
+                        });
+                    }
+
+                    return result;
+                } catch (e) {
+                    console.error(e);
+                }
+            };
+
+            if (this.argv.livereload) {
+                console.info(`LiveReload enabled for ${watch} on port ${this.liveReloadServer.config.port}`);
+            }
+        }
+    }
+
+    async stop() {
+        if (this.liveReloadServer) {
+            if (this.liveReloadServer.server) {
+                this.liveReloadServer.close();
+            } else {
+                this.liveReloadServer.watcher.close();
+            }
+            this.liveReloadServer = null;
+        }
+    }
+}
+
+module.exports = Watcher;
\ No newline at end of file


[openwhisk-wskdebug] 03/07: guard this.* access in Debugger.shutdown()/stop()/kill()

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alexkli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-wskdebug.git

commit cd50058d8b5e946aa64e4407dc57666b0283a94c
Author: Alexander Klimetschek <ak...@adobe.com>
AuthorDate: Sat Mar 28 23:53:31 2020 -0700

    guard this.* access in Debugger.shutdown()/stop()/kill()
---
 src/debugger.js | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/debugger.js b/src/debugger.js
index e43c8e8..fc24b11 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -161,7 +161,9 @@ class Debugger {
 
     async stop() {
         this.running = false;
-        this.agentMgr.stop();
+        if (this.agentMgr) {
+            this.agentMgr.stop();
+        }
 
         if (this.runPromise) {
             // wait for the main loop to gracefully end, which will call shutdown()
@@ -174,7 +176,9 @@ class Debugger {
 
     async kill() {
         this.running = false;
-        this.agentMgr.stop();
+        if (this.agentMgr) {
+            this.agentMgr.stop();
+        }
 
         await this.shutdown();
     }
@@ -189,9 +193,15 @@ class Debugger {
 
         // need to shutdown everything even if some fail, hence tryCatch() for each
 
-        await this.tryCatch(this.agentMgr.shutdown());
-        await this.tryCatch(this.invoker.stop());
-        await this.tryCatch(this.watcher.stop());
+        if (this.agentMgr) {
+            await this.tryCatch(this.agentMgr.shutdown());
+        }
+        if (this.invoker) {
+            await this.tryCatch(this.invoker.stop());
+        }
+        if (this.watcher) {
+            await this.tryCatch(this.watcher.stop());
+        }
 
         // only log this if we started properly
         if (this.ready) {