Compare commits

...

9 Commits

Author SHA1 Message Date
Nick Fields
634988cced Merge branch 'master' into nrf/add-tests 2023-09-26 10:17:11 -04:00
Nick Fields
14672906e6 minor: bump sem-rel action to v4 to fix esm errors 2023-09-26 10:05:30 -04:00
Unai Recio
1139f998ef feat(shell): checks only the shell name and allows any argument (#118)
Co-authored-by: ureciocais <urecio@caisgroup.com>
Co-authored-by: Nick Fields <46869826+nick-fields@users.noreply.github.com>
2023-09-26 09:56:58 -04:00
Nick Fields
1d41e5db1a patch: also run workflow on merge to default 2023-09-26 09:49:59 -04:00
Nick Fields
1859f94181 patch: run workflow on PR into default branch only (#119) 2023-09-26 13:44:00 +00:00
Nick Fields
cc15f2ffff minor: forgot to set ctor props 2023-09-24 20:41:06 -04:00
Nick Fields
db9d6ef914 minor: throw custom err w/ code attached instead of relying on shared var 2023-09-24 11:31:32 -04:00
Trung Nguyen
943e742917 fix: Retry on timeout (#106)
* fix: do not set return value for timed out runs

Commands that are killed manually due to timeout rarely returns a
success status code (0). These codes should not be treated as errors
but simply produced because of the timeout.

* fix(windows): use variable to track timeout

Use a variable to track timeout instead of relying on SIGTERM, as
processes on Windows are not killed using signals.
2022-12-29 21:37:40 -05:00
valery1707
0711ba3d78 Fix link to documentation about shell (#105)
* Fix link to documentation about `shell`

* Actualize documentation links
2022-10-19 06:43:26 -04:00
5 changed files with 94 additions and 50 deletions

View File

@@ -16,4 +16,4 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Logs** **Logs**
Enable [debug logging](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/enabling-debug-logging#enabling-step-debug-logging) then attach the [raw logs](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/using-workflow-run-logs#downloading-logs) (specifically the raw output of this action). Enable [debug logging](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging) then attach the [raw logs](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/using-workflow-run-logs#downloading-logs) (specifically the raw output of this action).

View File

@@ -1,12 +1,16 @@
name: CI/CD name: CI/CD
on: on:
# only on PRs into and merge to default branch
pull_request:
branches:
- master
push: push:
branches:
- master
jobs: jobs:
# runs on branch pushes only
ci_unit: ci_unit:
name: Run Unit Tests name: Run Unit Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -26,7 +30,6 @@ jobs:
ci_integration: ci_integration:
name: Run Integration Tests name: Run Integration Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -137,7 +140,6 @@ jobs:
ci_integration_envvar: ci_integration_envvar:
name: Run Integration Env Var Tests name: Run Integration Env Var Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -159,7 +161,6 @@ jobs:
ci_integration_large_output: ci_integration_large_output:
name: Run Integration Large Output Tests name: Run Integration Large Output Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -191,7 +192,6 @@ jobs:
ci_integration_retry_on_exit_code: ci_integration_retry_on_exit_code:
name: Run Integration retry_on_exit_code Tests name: Run Integration retry_on_exit_code Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -240,7 +240,6 @@ jobs:
ci_integration_continue_on_error: ci_integration_continue_on_error:
name: Run Integration continue_on_error Tests name: Run Integration continue_on_error Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -288,7 +287,6 @@ jobs:
ci_integration_retry_wait_seconds: ci_integration_retry_wait_seconds:
name: Run Integration Tests (retry_wait_seconds) name: Run Integration Tests (retry_wait_seconds)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -325,7 +323,6 @@ jobs:
ci_integration_on_retry_cmd: ci_integration_on_retry_cmd:
name: Run Integration Tests (on_retry_command) name: Run Integration Tests (on_retry_command)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -369,7 +366,6 @@ jobs:
# timeout tests take longer to run so run in parallel # timeout tests take longer to run so run in parallel
ci_integration_timeout_seconds: ci_integration_timeout_seconds:
name: Run Integration Timeout Tests (seconds) name: Run Integration Timeout Tests (seconds)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -400,7 +396,6 @@ jobs:
ci_integration_timeout_retry_on_timeout: ci_integration_timeout_retry_on_timeout:
name: Run Integration Timeout Tests (retry_on timeout) name: Run Integration Timeout Tests (retry_on timeout)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -432,7 +427,6 @@ jobs:
ci_integration_timeout_retry_on_error: ci_integration_timeout_retry_on_error:
name: Run Integration Timeout Tests (retry_on error) name: Run Integration Timeout Tests (retry_on error)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -468,7 +462,6 @@ jobs:
ci_integration_timeout_minutes: ci_integration_timeout_minutes:
name: Run Integration Timeout Tests (minutes) name: Run Integration Timeout Tests (minutes)
if: startsWith(github.ref, 'refs/heads')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -499,7 +492,6 @@ jobs:
ci_windows: ci_windows:
name: Run Windows Tests name: Run Windows Tests
if: startsWith(github.ref, 'refs/heads')
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -571,7 +563,7 @@ jobs:
steps: steps:
- run: echo "If this is hit, all tests successfully passed" - run: echo "If this is hit, all tests successfully passed"
# runs on push to master only # runs on merge to default only
cd: cd:
name: Publish Action name: Publish Action
needs: [ci_all_tests_passed] needs: [ci_all_tests_passed]
@@ -588,7 +580,7 @@ jobs:
run: npm ci run: npm ci
- name: Release - name: Release
id: semantic id: semantic
uses: cycjimmy/semantic-release-action@v2 uses: cycjimmy/semantic-release-action@v4
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Tag - name: Tag

View File

@@ -30,7 +30,7 @@ Retries an Action step on failure or timeout. This is currently intended to repl
### `shell` ### `shell`
**Optional** Shell to use to execute `command`. Defaults to `powershell` on Windows, `bash` otherwise. Supports bash, python, pwsh, sh, cmd, and powershell per [docs](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell) **Optional** Shell to use to execute `command`. Defaults to `powershell` on Windows, `bash` otherwise. Supports bash, python, pwsh, sh, cmd, and powershell per [docs](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell)
### `polling_interval_seconds` ### `polling_interval_seconds`
@@ -156,7 +156,7 @@ with:
```yaml ```yaml
- uses: nick-fields/retry@v2 - uses: nick-fields/retry@v2
id: retry id: retry
# see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error
continue-on-error: true continue-on-error: true
with: with:
timeout_seconds: 15 timeout_seconds: 15

65
dist/index.js vendored
View File

@@ -891,6 +891,21 @@ exports.retryWait = retryWait;
"use strict"; "use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) { return new (P || (P = Promise))(function (resolve, reject) {
@@ -941,14 +956,26 @@ var OS = process.platform;
var OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts'; var OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
var OUTPUT_EXIT_CODE_KEY = 'exit_code'; var OUTPUT_EXIT_CODE_KEY = 'exit_code';
var OUTPUT_EXIT_ERROR_KEY = 'exit_error'; var OUTPUT_EXIT_ERROR_KEY = 'exit_error';
var exit;
var done; var done;
var ErrorWithCode = /** @class */ (function (_super) {
__extends(ErrorWithCode, _super);
function ErrorWithCode(message, code) {
var _this = _super.call(this) || this;
_this.message = message;
_this.code = code;
_this.code = code;
_this.message = message;
return _this;
}
return ErrorWithCode;
}(Error));
function getExecutable(inputs) { function getExecutable(inputs) {
if (!inputs.shell) { if (!inputs.shell) {
return OS === 'win32' ? 'powershell' : 'bash'; return OS === 'win32' ? 'powershell' : 'bash';
} }
var executable; var executable;
switch (inputs.shell) { var shellName = inputs.shell.split(' ')[0];
switch (shellName) {
case 'bash': case 'bash':
case 'python': case 'python':
case 'pwsh': { case 'pwsh': {
@@ -957,7 +984,7 @@ function getExecutable(inputs) {
} }
case 'sh': { case 'sh': {
if (OS === 'win32') { if (OS === 'win32') {
throw new Error("Shell ".concat(inputs.shell, " not allowed on OS ").concat(OS)); throw new Error("Shell ".concat(shellName, " not allowed on OS ").concat(OS));
} }
executable = inputs.shell; executable = inputs.shell;
break; break;
@@ -965,13 +992,13 @@ function getExecutable(inputs) {
case 'cmd': case 'cmd':
case 'powershell': { case 'powershell': {
if (OS !== 'win32') { if (OS !== 'win32') {
throw new Error("Shell ".concat(inputs.shell, " not allowed on OS ").concat(OS)); throw new Error("Shell ".concat(shellName, " not allowed on OS ").concat(OS));
} }
executable = inputs.shell + '.exe'; executable = shellName + '.exe' + inputs.shell.replace(shellName, '');
break; break;
} }
default: { default: {
throw new Error("Shell ".concat(inputs.shell, " not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells")); throw new Error("Shell ".concat(shellName, " not supported. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell for supported shells"));
} }
} }
return executable; return executable;
@@ -1005,7 +1032,7 @@ function runRetryCmd(inputs) {
function runCmd(attempt, inputs) { function runCmd(attempt, inputs) {
var _a, _b; var _a, _b;
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var end_time, executable, child; var end_time, executable, exit, timeout, child;
return __generator(this, function (_c) { return __generator(this, function (_c) {
switch (_c.label) { switch (_c.label) {
case 0: case 0:
@@ -1013,6 +1040,7 @@ function runCmd(attempt, inputs) {
executable = getExecutable(inputs); executable = getExecutable(inputs);
exit = 0; exit = 0;
done = false; done = false;
timeout = false;
(0, core_1.debug)("Running command ".concat(inputs.command, " on ").concat(OS, " using shell ").concat(executable)); (0, core_1.debug)("Running command ".concat(inputs.command, " on ").concat(OS, " using shell ").concat(executable));
child = attempt > 1 && inputs.new_command_on_retry child = attempt > 1 && inputs.new_command_on_retry
? (0, child_process_1.spawn)(inputs.new_command_on_retry, { shell: executable }) ? (0, child_process_1.spawn)(inputs.new_command_on_retry, { shell: executable })
@@ -1026,13 +1054,17 @@ function runCmd(attempt, inputs) {
child.on('exit', function (code, signal) { child.on('exit', function (code, signal) {
(0, core_1.debug)("Code: ".concat(code)); (0, core_1.debug)("Code: ".concat(code));
(0, core_1.debug)("Signal: ".concat(signal)); (0, core_1.debug)("Signal: ".concat(signal));
if (code && code > 0) {
exit = code;
}
// timeouts are killed manually // timeouts are killed manually
if (signal === 'SIGTERM') { if (signal === 'SIGTERM') {
return; return;
} }
// On Windows signal is null.
if (timeout) {
return;
}
if (code && code > 0) {
exit = code;
}
done = true; done = true;
}); });
_c.label = 1; _c.label = 1;
@@ -1045,17 +1077,18 @@ function runCmd(attempt, inputs) {
_c.label = 4; _c.label = 4;
case 4: case 4:
if (!(!done && child.pid)) return [3 /*break*/, 6]; if (!(!done && child.pid)) return [3 /*break*/, 6];
timeout = true;
(0, tree_kill_1.default)(child.pid); (0, tree_kill_1.default)(child.pid);
return [4 /*yield*/, (0, util_1.retryWait)(milliseconds_1.default.seconds(inputs.retry_wait_seconds))]; return [4 /*yield*/, (0, util_1.retryWait)(milliseconds_1.default.seconds(inputs.retry_wait_seconds))];
case 5: case 5:
_c.sent(); _c.sent();
throw new Error("Timeout of ".concat((0, inputs_1.getTimeout)(inputs), "ms hit")); throw new ErrorWithCode("Timeout of ".concat((0, inputs_1.getTimeout)(inputs), "ms hit"), exit);
case 6: case 6:
if (!(exit > 0)) return [3 /*break*/, 8]; if (!(exit > 0)) return [3 /*break*/, 8];
return [4 /*yield*/, (0, util_1.retryWait)(milliseconds_1.default.seconds(inputs.retry_wait_seconds))]; return [4 /*yield*/, (0, util_1.retryWait)(milliseconds_1.default.seconds(inputs.retry_wait_seconds))];
case 7: case 7:
_c.sent(); _c.sent();
throw new Error("Child_process exited with error code ".concat(exit)); throw new ErrorWithCode("Child_process exited with error code ".concat(exit), exit);
case 8: return [2 /*return*/]; case 8: return [2 /*return*/];
} }
}); });
@@ -1063,7 +1096,7 @@ function runCmd(attempt, inputs) {
} }
function runAction(inputs) { function runAction(inputs) {
return __awaiter(this, void 0, void 0, function () { return __awaiter(this, void 0, void 0, function () {
var attempt, error_2; var attempt, error_2, exit;
return __generator(this, function (_a) { return __generator(this, function (_a) {
switch (_a.label) { switch (_a.label) {
case 0: return [4 /*yield*/, (0, inputs_1.validateInputs)(inputs)]; case 0: return [4 /*yield*/, (0, inputs_1.validateInputs)(inputs)];
@@ -1085,8 +1118,9 @@ function runAction(inputs) {
return [3 /*break*/, 13]; return [3 /*break*/, 13];
case 5: case 5:
error_2 = _a.sent(); error_2 = _a.sent();
exit = error_2 instanceof ErrorWithCode ? error_2.code : 1;
if (!(attempt === inputs.max_attempts)) return [3 /*break*/, 6]; if (!(attempt === inputs.max_attempts)) return [3 /*break*/, 6];
throw new Error("Final attempt failed. ".concat(error_2.message)); throw new ErrorWithCode("Final attempt failed. ".concat(error_2.message), exit);
case 6: case 6:
if (!(!done && inputs.retry_on === 'error')) return [3 /*break*/, 7]; if (!(!done && inputs.retry_on === 'error')) return [3 /*break*/, 7];
// error: timeout // error: timeout
@@ -1124,8 +1158,7 @@ runAction(inputs)
process.exit(0); // success process.exit(0); // success
}) })
.catch(function (err) { .catch(function (err) {
// exact error code if available, otherwise just 1 var exitCode = err instanceof ErrorWithCode ? err.code : 1;
var exitCode = exit > 0 ? exit : 1;
if (inputs.continue_on_error) { if (inputs.continue_on_error) {
(0, core_1.warning)(err.message); (0, core_1.warning)(err.message);
} }

View File

@@ -11,16 +11,25 @@ const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
const OUTPUT_EXIT_CODE_KEY = 'exit_code'; const OUTPUT_EXIT_CODE_KEY = 'exit_code';
const OUTPUT_EXIT_ERROR_KEY = 'exit_error'; const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
let exit: number;
let done: boolean; let done: boolean;
class ErrorWithCode extends Error {
constructor(readonly message: string, readonly code: number) {
super();
this.code = code;
this.message = message;
}
}
function getExecutable(inputs: Inputs): string { function getExecutable(inputs: Inputs): string {
if (!inputs.shell) { if (!inputs.shell) {
return OS === 'win32' ? 'powershell' : 'bash'; return OS === 'win32' ? 'powershell' : 'bash';
} }
let executable: string; let executable: string;
switch (inputs.shell) { const shellName = inputs.shell.split(' ')[0];
switch (shellName) {
case 'bash': case 'bash':
case 'python': case 'python':
case 'pwsh': { case 'pwsh': {
@@ -29,7 +38,7 @@ function getExecutable(inputs: Inputs): string {
} }
case 'sh': { case 'sh': {
if (OS === 'win32') { if (OS === 'win32') {
throw new Error(`Shell ${inputs.shell} not allowed on OS ${OS}`); throw new Error(`Shell ${shellName} not allowed on OS ${OS}`);
} }
executable = inputs.shell; executable = inputs.shell;
break; break;
@@ -37,14 +46,14 @@ function getExecutable(inputs: Inputs): string {
case 'cmd': case 'cmd':
case 'powershell': { case 'powershell': {
if (OS !== 'win32') { if (OS !== 'win32') {
throw new Error(`Shell ${inputs.shell} not allowed on OS ${OS}`); throw new Error(`Shell ${shellName} not allowed on OS ${OS}`);
} }
executable = inputs.shell + '.exe'; executable = shellName + '.exe' + inputs.shell.replace(shellName, '');
break; break;
} }
default: { default: {
throw new Error( throw new Error(
`Shell ${inputs.shell} not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells` `Shell ${shellName} not supported. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell for supported shells`
); );
} }
} }
@@ -69,8 +78,9 @@ async function runCmd(attempt: number, inputs: Inputs) {
const end_time = Date.now() + getTimeout(inputs); const end_time = Date.now() + getTimeout(inputs);
const executable = getExecutable(inputs); const executable = getExecutable(inputs);
exit = 0; let exit = 0;
done = false; done = false;
let timeout = false;
debug(`Running command ${inputs.command} on ${OS} using shell ${executable}`); debug(`Running command ${inputs.command} on ${OS} using shell ${executable}`);
const child = const child =
@@ -88,13 +98,21 @@ async function runCmd(attempt: number, inputs: Inputs) {
child.on('exit', (code, signal) => { child.on('exit', (code, signal) => {
debug(`Code: ${code}`); debug(`Code: ${code}`);
debug(`Signal: ${signal}`); debug(`Signal: ${signal}`);
if (code && code > 0) {
exit = code;
}
// timeouts are killed manually // timeouts are killed manually
if (signal === 'SIGTERM') { if (signal === 'SIGTERM') {
return; return;
} }
// On Windows signal is null.
if (timeout) {
return;
}
if (code && code > 0) {
exit = code;
}
done = true; done = true;
}); });
@@ -103,12 +121,13 @@ async function runCmd(attempt: number, inputs: Inputs) {
} while (Date.now() < end_time && !done); } while (Date.now() < end_time && !done);
if (!done && child.pid) { if (!done && child.pid) {
timeout = true;
kill(child.pid); kill(child.pid);
await retryWait(ms.seconds(inputs.retry_wait_seconds)); await retryWait(ms.seconds(inputs.retry_wait_seconds));
throw new Error(`Timeout of ${getTimeout(inputs)}ms hit`); throw new ErrorWithCode(`Timeout of ${getTimeout(inputs)}ms hit`, exit);
} else if (exit > 0) { } else if (exit > 0) {
await retryWait(ms.seconds(inputs.retry_wait_seconds)); await retryWait(ms.seconds(inputs.retry_wait_seconds));
throw new Error(`Child_process exited with error code ${exit}`); throw new ErrorWithCode(`Child_process exited with error code ${exit}`, exit);
} else { } else {
return; return;
} }
@@ -126,8 +145,9 @@ async function runAction(inputs: Inputs) {
break; break;
// eslint-disable-next-line // eslint-disable-next-line
} catch (error: any) { } catch (error: any) {
const exit = error instanceof ErrorWithCode ? error.code : 1;
if (attempt === inputs.max_attempts) { if (attempt === inputs.max_attempts) {
throw new Error(`Final attempt failed. ${error.message}`); throw new ErrorWithCode(`Final attempt failed. ${error.message}`, exit);
} else if (!done && inputs.retry_on === 'error') { } else if (!done && inputs.retry_on === 'error') {
// error: timeout // error: timeout
throw error; throw error;
@@ -156,8 +176,7 @@ runAction(inputs)
process.exit(0); // success process.exit(0); // success
}) })
.catch((err) => { .catch((err) => {
// exact error code if available, otherwise just 1 const exitCode = err instanceof ErrorWithCode ? err.code : 1;
const exitCode = exit > 0 ? exit : 1;
if (inputs.continue_on_error) { if (inputs.continue_on_error) {
warning(err.message); warning(err.message);