-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* set up tests * checkpt; axios throws a regression error (#3219) * Lower axios version (see #1) * finish startup routine * remove unused dependencies * setup npm publish pipeline Co-authored-by: Stanislaw Hüll <stanislaw.huell@js-soft.com>
- Loading branch information
Showing
9 changed files
with
352 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
on: | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-node@v3 | ||
with: | ||
node-version: 18.12.1 | ||
- run: npm install | ||
- run: npm run build | ||
- run: npm publish | ||
env: | ||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { AxiosResponse } from "axios" | ||
import { Utility } from "./utility" | ||
|
||
export namespace Dss { | ||
/* | ||
* Checks whether the DSS API is accessible at the specified ip/hostname | ||
* and port by querying the DSS's default browser API. | ||
* | ||
* If 'waitSeconds' is set, this request is repeated until the set time has | ||
* passed or a reply is received from the server. | ||
*/ | ||
export async function isOnline( | ||
baseUrl: string, | ||
options?: { | ||
waitSeconds?: number | ||
} | ||
): Promise<boolean> { | ||
const waitSeconds = options?.waitSeconds ? options.waitSeconds : 0 | ||
const start = new Date().getTime() // returns unix seconds | ||
|
||
const config = { | ||
url: baseUrl, | ||
method: "GET", | ||
timeout: 3000 | ||
} | ||
do { | ||
const httpReqRes = await Utility.httpReq(config) | ||
if (httpReqRes.isOk()) { | ||
return true | ||
} | ||
Utility.sleepms(1000) | ||
} while ((new Date().getTime() - start) / 1000 < waitSeconds) | ||
return false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,47 @@ | ||
import express from "express"; | ||
import { Settings } from "./settings" | ||
import { Dss } from "./dss" | ||
import express from "express" | ||
import http from "http" | ||
import https from "https" | ||
|
||
const app = express(); | ||
const port = process.env.LOCAL_MODULE_PORT ? process.env.LOCAL_MODULE_PORT : 2048; | ||
async function main() { | ||
/* Parse application settings. */ | ||
const settingsRes = Settings.parseApplicationSettings() | ||
if (settingsRes.isErr()) { | ||
console.error(settingsRes.error.message) | ||
process.exit(1) | ||
} | ||
const settings = settingsRes.value | ||
|
||
app.get("/", (_req, res) => { | ||
res.send(`Local module v${process.env.npm_package_version}`); | ||
}); | ||
/* Wait for DSS startup to fininsh. */ | ||
const wait = 360 | ||
const isOnline = await Dss.isOnline(settings.dssBaseUrl, { waitSeconds: wait }) | ||
if (isOnline) { | ||
console.log("DSS responded. Starting HTTP server.") | ||
} else { | ||
console.error(`Could not reach DSS after ${wait} seconds. Abort.`) | ||
process.exit(1) | ||
} | ||
|
||
app.listen(port, () => { | ||
console.log(`Listening on port ${port}.`); | ||
}); | ||
/* Start http server. */ | ||
const app = express() | ||
app.get("/", (_req, res) => { | ||
res.send(`Local module v${process.env.npm_package_version}`) | ||
}) | ||
|
||
let protocol = "http" | ||
if (settings.localModuleUseHttps) { | ||
protocol += "s" | ||
} | ||
const initCallback = () => { | ||
console.log(`Listening on ${protocol}://${settings.localModuleIp}:${settings.localModulePort}.`) | ||
} | ||
if (settings.localModuleUseHttps) { | ||
// NOTE: This code path is currently inactive. | ||
https.createServer(app).listen(settings.localModulePort, settings.localModuleIp, initCallback) | ||
} else { | ||
http.createServer(app).listen(settings.localModulePort, settings.localModuleIp, initCallback) | ||
} | ||
} | ||
|
||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import fs from "fs" | ||
import url from "url" | ||
import { ok, err, Result } from "neverthrow" | ||
import { Utility } from "./utility" | ||
|
||
export namespace Settings { | ||
export namespace Errors { | ||
export class InvalidSettings extends Error { | ||
public constructor(message: string) { | ||
super(message) | ||
} | ||
} | ||
|
||
export class MissingEnvvar extends InvalidSettings { | ||
public envvar: string | ||
public constructor(envvar: string, messageAppendix?: string) { | ||
let message = `Mandatory environment variable '${envvar}' is unset.` | ||
if (messageAppendix !== undefined) { | ||
message += ` ${messageAppendix}` | ||
} | ||
super(message) | ||
this.envvar = envvar | ||
} | ||
} | ||
|
||
export class InvalidEnvvarValue extends InvalidSettings { | ||
public envvar: string | ||
public value: string | ||
public constructor(envvar: string, value: string, messageAppendix?: string) { | ||
let message = `Mandatory environment variable '${envvar}' has invalid value '${value}'.` | ||
if (messageAppendix !== undefined) { | ||
message += ` ${messageAppendix}` | ||
} | ||
super(message) | ||
this.envvar = envvar | ||
this.value = value | ||
} | ||
} | ||
} | ||
|
||
/* Export names of envvars for testing purposes.. */ | ||
export const localModulePortEnvvar = "WP07_LOCAL_MODULE_PORT" | ||
export const dssBaseUrlEnvvar = "WP07_DSS_BASE_URL" | ||
|
||
export interface IApplicationSettings { | ||
/* Local module configuration. Parsed from the 'WP07_LOCAL_MODULE_PORT' | ||
* envvar. */ | ||
localModuleUseHttps: boolean | ||
localModuleIp: string // hostname or ip | ||
localModulePort: number | ||
|
||
/* Base url of the DSS API. Parsed from the 'WP07_DSS_BASE_URL' envvar. */ | ||
dssBaseUrl: string // always has a trailing slash | ||
} | ||
|
||
export function parseApplicationSettings(env = process.env): Result<IApplicationSettings, Errors.MissingEnvvar | Errors.InvalidEnvvarValue | Error> { | ||
/* Merge environment with .env file, if provided. Existing envvars are | ||
* not overwritten by the contents of the .env file. */ | ||
if (fs.existsSync(".env")) { | ||
const parseRes = Utility.parseKeyValueFile(".env") | ||
if (parseRes.isErr()) { | ||
return err(parseRes.error) | ||
} | ||
env = { ...parseRes.value, ...env } | ||
} | ||
|
||
/* Validate local module settings. */ | ||
if (env[localModulePortEnvvar] == undefined) { | ||
return err(new Errors.MissingEnvvar(localModulePortEnvvar)) | ||
} | ||
const localModulePort = Number(env[localModulePortEnvvar]) | ||
if (!Utility.isValidPort(localModulePort)) { | ||
return err(new Errors.InvalidEnvvarValue(localModulePortEnvvar, env[localModulePortEnvvar])) | ||
} | ||
|
||
/* Destructure DSS base url and validate its fields. We assert that | ||
* the protocol is http/https and no additional fields outside of | ||
* protocol, host and path are specified by the url. | ||
* | ||
* NOTE: The protocol parsed from a url by the urllib always | ||
* contains a trailing colon. */ | ||
if (env[dssBaseUrlEnvvar] == undefined) { | ||
return err(new Errors.MissingEnvvar(dssBaseUrlEnvvar)) | ||
} | ||
|
||
try { | ||
const urlStruct = new url.URL(env[dssBaseUrlEnvvar] as string) | ||
if (!["http:", "https:"].includes(urlStruct.protocol)) { | ||
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar], `Only 'http' and 'https' are supported.`)) | ||
} else if (urlStruct.port !== "" && !Utility.isValidPort(Number(urlStruct.port))) { | ||
/* If the default port for http or https is used, the .port | ||
* field is empty. We thus have to process this special case. */ | ||
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar], `Invalid port: '${urlStruct.port}'.`)) | ||
} else if (urlStruct.username !== "" || urlStruct.password !== "" || urlStruct.search !== "" || urlStruct.hash !== "") { | ||
throw new Error() | ||
} | ||
} catch (error: unknown) { | ||
return err(new Errors.InvalidEnvvarValue(dssBaseUrlEnvvar, env[dssBaseUrlEnvvar])) | ||
} | ||
|
||
/* Append a trailing slash to the base url for consistency. */ | ||
let dssBaseUrl = env[dssBaseUrlEnvvar] | ||
if (dssBaseUrl[dssBaseUrl.length - 1] !== "/") { | ||
dssBaseUrl += "/" | ||
} | ||
|
||
const result: IApplicationSettings = { | ||
localModuleUseHttps: false, | ||
localModuleIp: "localhost", | ||
localModulePort: localModulePort, | ||
dssBaseUrl: env[dssBaseUrlEnvvar] | ||
} | ||
return ok(result) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import fs from "fs" | ||
import util from "util" | ||
import { ok, err, Result } from "neverthrow" | ||
import { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios" | ||
import axios from "axios" | ||
|
||
export namespace Utility { | ||
/* | ||
* Neverthrow axios wrapper. | ||
*/ | ||
export async function httpReq(config: AxiosRequestConfig): Promise<Result<AxiosResponse, AxiosError | Error>> { | ||
try { | ||
const response = await axios(config) | ||
return ok(response) | ||
} catch (error: unknown) { | ||
if (error instanceof AxiosError || error instanceof Error) { | ||
return err(error) | ||
} | ||
return err(new Error("An unhandled error occurred.")) | ||
} | ||
} | ||
|
||
export async function sleepms(ms: number) { | ||
return util.promisify(setTimeout)(ms) | ||
} | ||
|
||
/* | ||
* Checks whether 'port' is a valid port, i.e. an integer in (0, 65535]. | ||
*/ | ||
export function isValidPort(port: number) { | ||
if (isNaN(port) || port <= 0 || 65536 <= port || Math.floor(port) !== port) { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
/* | ||
* Parses a file of utf-8 encoded, single-line key=value pairs. Lines | ||
* starting with a literal '#' character are ignored, as well as lines not | ||
* containing a literal '='. The first literal '=' character is used to | ||
* split key from value. | ||
* | ||
* Both linux ('\n') and windows ('\r\n') EOL characters are supported. | ||
* | ||
* This serves as a simple 'dotenv' replacement. 'dotenv' necessarily | ||
* overrides process.env, whereas for testing purposes it is preferable to | ||
* pass around explicit objects. Merging the parsed kv-pairs with | ||
* process.env can be done in a separate, trivial step. | ||
*/ | ||
export function parseKeyValueFile(path: string): Result<Record<string, string>, Error> { | ||
let lines: string[] | ||
try { | ||
lines = fs.readFileSync(path, "utf-8").split(/\r?\n/) | ||
} catch (error: unknown) { | ||
return err(new Error(`Error reading file '${path}'`)) | ||
} | ||
|
||
const result: Record<string, string> = {} | ||
for (const line of lines) { | ||
const n = line.search("=") | ||
if (line.length === 0 || line[0] === "#" || n === -1 || n === 0) { | ||
continue | ||
} | ||
const key = line.slice(0, n) | ||
const value = line.slice(n + 1) | ||
result[key] = value | ||
} | ||
|
||
return ok(result) | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.