Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions client/src/components/Workflow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@
{{ projectJson !== null ? projectJson.name : "" }}
</span>
<v-spacer />
<v-btn
rounded
variant="outlined"
:ripple="false"
:style="{backgroundColor : stateColor}"
data-cy="workflow-project_state-btn"
<v-tooltip
:disabled="failedTaskPaths.length === 0"
location="bottom"
>
status: {{ projectState }}{{ isReadOnly }}
</v-btn>
<template #activator="{ props }">
<v-btn
v-bind="props"
rounded
variant="outlined"
:ripple="false"
:style="{backgroundColor : stateColor}"
data-cy="workflow-project_state-btn"
>
status: {{ projectState }}{{ isReadOnly }}
</v-btn>
</template>
<span>Failed tasks:<br>{{ failedTaskPaths.join('\n') }}</span>
</v-tooltip>
<v-spacer />
<v-btn
shaped
Expand Down Expand Up @@ -164,7 +173,7 @@
>
<template #activator="{ props }">
<v-btn
v-if="readOnly"
v-if="forceEditAllowed"
icon="mdi-pencil-lock-outline"
rounded="0"
variant="outlined"
Expand Down Expand Up @@ -488,6 +497,9 @@ export default {
return this.readOnly ? " - read-only" : "";
},
stateColor() {
if (this.projectState === "stopped" && this.failedTaskPaths.length > 0) {
return "#E60000";
}
return state2color(this.projectState);
},
readOnlyColor() {
Expand All @@ -497,7 +509,10 @@ export default {
return this.selectedSourceFilenames[0].filename;
},
runProjectAllowed() {
return isAllowed(this.projectState, "runProject");
return isAllowed(this.projectState, "runProject") && !this.readOnly;
},
forceEditAllowed() {
return this.readOnly && ["stopped", "finished", "failed", "unknown"].includes(this.projectState);
},
pauseProjectAllowed() {
return isAllowed(this.projectState, "pauseProject");
Expand All @@ -516,6 +531,9 @@ export default {
},
cleanProjectAllowed() {
return isAllowed(this.projectState, "cleanProject");
},
failedTaskPaths() {
return this.projectJson?.failedTasks ?? [];
}
},
mounted: function () {
Expand Down
8 changes: 4 additions & 4 deletions common/allowedOperations.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ const allowedOperations = {
"not-started": ["runProject", "revertProject", "saveProject", "checkProject"],
"preparing": ["cleanProject"],
"running": ["stopProject"],
"stopped": ["cleanProject"],
"finished": ["cleanProject"],
"failed": ["cleanProject"],
"stopped": ["runProject", "cleanProject"],
"finished": ["runProject", "cleanProject"],
"failed": ["runProject", "cleanProject"],
"holding": [],
"unknown": ["cleanProject"],
"unknown": ["runProject", "cleanProject"],
};

module.exports = allowedOperations;
37 changes: 35 additions & 2 deletions documentMD/user_guide/_reference/3_workflow_screen/1_graphview.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,48 @@ WHEELから投入されたジョブの実行状態確認時に、規定の回数
ルートワークフローから順に下位コンポーネントへと移動して、unknown状態となっているタスクコンポーネントを探し、当該ジョブの実行状態がワークフロー全体の実行の成否に影響が無いかを確認してください。
{: .notice--info}

### プロジェクト操作ボタンエリア(`stopped`/`failed`/`unknown`状態からの再実行について)

プロジェクトが `stopped`、`failed`、または `unknown` 状態のとき、run project ボタンを押すと中断した時点の状態から実行を再開(リスタート)することができます。

#### 各コンポーネントの挙動

リスタート時には、各コンポーネントの状態に応じて以下のように処理されます。

| コンポーネントの状態 | リスタート時の挙動 |
|:---------------------|:---------------------------------|
| `finished` | スキップ(再実行されません) |
| `not-started` | 通常通り実行されます |
| `running` | 実行中に中断されたため再実行されます |
| `failed` | 再実行されます |

#### ループコンポーネント(For / Foreach / While)のリスタート挙動

ループコンポーネントが実行途中で中断された場合、リスタートすると完了済みのイテレーション(`finished` 状態のインスタンス)はスキップされ、中断された時点のインデックスから実行を再開します。

#### PSコンポーネントのリスタート挙動

PSコンポーネントが実行途中で中断された場合、リスタートすると完了済み(`finished`)のインスタンスはスキップされ、未完了のインスタンスのみが再実行されます。

#### 保存(save)してからリスタートする場合との違い

`stop project` 後に `save project` を行ってからリスタートすると、全てのコンポーネントの状態が `not-started` にリセットされ、ワークフローが最初から実行されます。
保存せずに直接 `run project` を押した場合は、中断した時点の状態から再開します(ループコンポーネントは完了済みのイテレーションをスキップして続きから実行)。

#### cleanup project との違い

`cleanup project` ボタンはプロジェクトの実行中に生成されたファイルを全て削除し、コンポーネントの状態を `not-started` にリセットして最初から実行できる状態に戻します。
リスタートはファイルを削除せず中断時の状態を引き継ぎますが、`cleanup project` はゼロから実行し直す場合に使用します。

### プロジェクト操作ボタンエリア
このエリアには、プロジェクトの実行に関わるボタンが表示されます。

![img](./img/project_control_btn.png "control_button_area")

||構成要素|説明|
|----------|----------|---------------------------------|
|1|run project ボタン |プロジェクトを実行開始します|
|2|stop project ボタン |プロジェクトの実行を停止し実行前の状態に戻します|
|1|run project ボタン |プロジェクトを実行開始します。`stopped`、`failed`、`unknown` 状態のプロジェクトは、中断した時点の状態から実行を再開(リスタート)します。詳細は[上記のリスタートについての説明](#プロジェクト操作ボタンエリアstoppedfailedunknown状態からの再実行について)を参照してください|
|2|stop project ボタン |プロジェクトの実行を停止し、`stopped` 状態に移行します。各コンポーネントの実行状態はそのまま保持されます|
|3|cleanup project ボタン |プロジェクトの実行中に生成されたファイルなどを削除し、実行開始前の状態に戻します|

### 保存ボタンエリア
Expand Down
6 changes: 6 additions & 0 deletions documentMD/user_guide/_reference/4_component/03_For.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,11 @@ Forコンポーネントが初めて実行されるとき、コンポーネン

number of instance to keepの値が0以外に設定されていた場合は、4,7の処理を実行後に設定された数を超える古いディレクトリ(`for_1`や`for_3`など)を削除します。

### 中断時のリスタート挙動

プロジェクトが `stopped`、`failed`、または `unknown` 状態で中断された後にリスタートすると、Forコンポーネントは完了済みのイテレーション(`finished` 状態のインスタンスディレクトリ)をスキップし、中断された時点のインデックスから実行を再開します。

リスタート前に `save project` を行なった場合は、全コンポーネントの状態が `not-started` にリセットされ、最初のインデックスから実行し直します。

--------
[コンポーネントの詳細に戻る]({{ site.baseurl }}/reference/4_component/)
6 changes: 6 additions & 0 deletions documentMD/user_guide/_reference/4_component/04_while.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Whileコンポーネントも、Forコンポーネントと同様の挙動をし

また終了判定もインデックス値の計算ではなく、設定されたシェルスクリプトの戻り値か、javascript式の評価結果を用います。

### 中断時のリスタート挙動

プロジェクトが `stopped`、`failed`、または `unknown` 状態で中断された後にリスタートすると、Whileコンポーネントは完了済みのイテレーション(`finished` 状態のインスタンスディレクトリ)をスキップし、中断された時点のインデックスから実行を再開します。

リスタート前に `save project` を行なった場合は、全コンポーネントの状態が `not-started` にリセットされ、最初のインデックス(0)から実行し直します。

--------
[コンポーネントの詳細に戻る]({{ site.baseurl }}/reference/4_component/)

6 changes: 6 additions & 0 deletions documentMD/user_guide/_reference/4_component/05_Foreach.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ __インデックス値の参照方法について__
ForeachコンポーネントもForコンポーネントと同様の挙動をしますがインデックス値は計算によって求められるのではなく、indexListに設定された値がリストの先頭から順に使われます。
リストの終端まで実行されるとコンポーネント全体の実行を終了します。

### 中断時のリスタート挙動

プロジェクトが `stopped`、`failed`、または `unknown` 状態で中断された後にリスタートすると、Foreachコンポーネントは完了済みのイテレーション(`finished` 状態のインスタンスディレクトリ)をスキップし、中断された時点のインデックスから実行を再開します。

リスタート前に `save project` を行なった場合は、全コンポーネントの状態が `not-started` にリセットされ、indexListの先頭から実行し直します。

--------
[コンポーネントの詳細に戻る]({{ site.baseurl }}/reference/4_component/)

6 changes: 6 additions & 0 deletions documentMD/user_guide/_reference/4_component/06_PS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ PSコンポーネントは実行開始時に[パラメータ設定ファイル](
このときもパラメータの値を用いてファイル名を変更することができます。
アプリケーションの実行結果ファイルなど、同じ名前のファイルが作成されている場合はこの機能を用いてリネームし、集めてください。

### 中断時のリスタート挙動

プロジェクトが `stopped`、`failed`、または `unknown` 状態で中断された後にリスタートすると、PSコンポーネントは完了済み(`finished`)のインスタンスディレクトリをスキップし、未完了のインスタンスのみを再実行します。

リスタート前に `save project` を行なった場合は、全コンポーネントの状態が `not-started` にリセットされ、全パラメータの組み合わせについて最初から実行し直します。

### parameterFile
![img](./img/PS_setting.png "PS setting")

Expand Down
2 changes: 1 addition & 1 deletion server/app/core/componentJsonIO.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const { readJsonGreedy } = require("./fileUtils");
* @returns {string | undefined} -
*/
function componentJsonReplacer(key, value) {
if (["handler", "doCleanup", "sbsID", "childLoopRunning"].includes(key)) {
if (["handler", "doCleanup", "sbsID", "childLoopRunning", "restartChecked"].includes(key)) {
return undefined;
}
return value;
Expand Down
10 changes: 7 additions & 3 deletions server/app/core/dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -611,10 +611,11 @@ class Dispatcher extends EventEmitter {
async _loopHandler(getNextIndex, getPrevIndex, isFinished, getTripCount, keepLoopInstance, component) {
getLogger(this.projectRootDir).debug("_loopHandler called", component.name);

if (component.initialized && component.currentIndex !== null && component.state === "not-started") {
if (!component.restartChecked && component.initialized && component.currentIndex !== null && (component.state === "not-started" || component.state === "running")) {
getLogger(this.projectRootDir).debug(`${component.name} is restarting from ${component.currentIndex}`);
component.restarting = true;
}
component.restartChecked = true;
if (!component.restarting && component.childLoopRunning) {
//send back itself to searchList for next loop trip
this.pendingComponents.push(component);
Expand All @@ -626,7 +627,7 @@ class Dispatcher extends EventEmitter {
loopInitialize(component, getTripCount);
} else if (component.restarting) {
let done = false;
const currentInstanceDir = path.resolve(this.cwfDir, getInstanceDirectoryName(component, component.prevIndex, component.name));
const currentInstanceDir = path.resolve(this.cwfDir, getInstanceDirectoryName(component, component.currentIndex, component.name));
if (await fs.pathExists(currentInstanceDir)) {
const { state } = await readComponentJson(currentInstanceDir);
if (state === "finished") {
Expand All @@ -653,6 +654,9 @@ class Dispatcher extends EventEmitter {
component.currentIndex = getNextIndex(component);
}
await this._setComponentState(component, "running");
//_setComponentState skips writing if state is unchanged (already "running" from prior iterations).
//Force-write to always persist loop progress fields (currentIndex, prevIndex, numFinished, etc.)
await writeComponentJson(this.projectRootDir, this._getComponentDir(component.ID), component, true);
component.childLoopRunning = true;

//update env
Expand Down Expand Up @@ -778,7 +782,7 @@ class Dispatcher extends EventEmitter {

async _PSHandler(component) {
getLogger(this.projectRootDir).debug("_PSHandler called", component.name);
if (component.initialized && component.state === "not-started") {
if (component.initialized && (component.state === "not-started" || component.state === "running")) {
component.restarting = true;
}
await this._setComponentState(component, "running");
Expand Down
23 changes: 13 additions & 10 deletions server/app/core/projectController.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,19 @@ async function runProject(projectRootDir) {
}
rootDispatchers.set(projectRootDir, rootDispatcher);

await updateProjectState(projectRootDir, "running", projectJson);
getLogger(projectRootDir).info("project start");
rootWF.state = await rootDispatcher.start();
getLogger(projectRootDir).info(`project ${rootWF.state}`);
await updateProjectState(projectRootDir, rootWF.state, projectJson);
await writeComponentJson(projectRootDir, projectRootDir, rootWF, true);
rootDispatchers.delete(projectRootDir);
removeExecuters(projectRootDir);
removeTransferrers(projectRootDir);
removeSsh(projectRootDir);
try {
await updateProjectState(projectRootDir, "running", projectJson);
getLogger(projectRootDir).info("project start");
rootWF.state = await rootDispatcher.start();
getLogger(projectRootDir).info(`project ${rootWF.state}`);
await updateProjectState(projectRootDir, rootWF.state, projectJson);
await writeComponentJson(projectRootDir, projectRootDir, rootWF, true);
} finally {
rootDispatchers.delete(projectRootDir);
removeExecuters(projectRootDir);
removeTransferrers(projectRootDir);
removeSsh(projectRootDir);
}
return rootWF.state;
}

Expand Down
86 changes: 82 additions & 4 deletions server/app/core/projectFilesOperator.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { projectList, defaultCleanupRemoteRoot, projectJsonFilename, componentJso
const { getDateString, writeJsonWrapper, isValidName, isValidInputFilename, isValidOutputFilename } = require("../lib/utility");
const { replacePathsep, convertPathSep } = require("./pathUtils");
const { readJsonGreedy } = require("./fileUtils");
const { gitInit, gitAdd, gitCommit, gitRm } = require("./gitOperator2");
const { gitInit, gitAdd, gitCommit, gitRm, gitStatus } = require("./gitOperator2");
const { hasChild, isLocalComponent } = require("./workflowComponent");
const { getLogger } = require("../logSettings");
const { getSsh } = require("./sshManager.js");
Expand Down Expand Up @@ -263,15 +263,17 @@ async function updateComponentPath(projectRootDir, ID, absPath) {
* @param {boolean} force - force update if already set given state
* @returns {object|false} - new Project JSON meta data. false means meta data does not updated
*/
async function setProjectState(projectRootDir, state, force) {
async function setProjectState(projectRootDir, state, force, doNotAdd = false) {
const filename = path.resolve(projectRootDir, projectJsonFilename);
const projectJson = await readJsonGreedy(filename);
if (force || projectJson.state !== state) {
projectJson.state = state;
const timestamp = getDateString(true);
projectJson.mtime = timestamp;
await writeJsonWrapper(filename, projectJson);
await gitAdd(projectRootDir, filename);
if (!doNotAdd) {
await gitAdd(projectRootDir, filename);
}
return projectJson;
}
return false;
Expand Down Expand Up @@ -1763,6 +1765,78 @@ async function getComponentTree(projectRootDir, rootDir) {
return root;
}

/**
* Sanitize all staged *.wheel.json files by setting state to "not-started".
* Saves original file contents so they can be restored after commit.
* @param {string} projectRootDir - project's root path
* @returns {Promise<Array<{filePath: string, originalContent: string}>>} - list of sanitized files with their original content
*/
async function sanitizeStagedJsonFiles(projectRootDir) {
const { added, modified } = await gitStatus(projectRootDir);
const stagedFiles = [...added, ...modified];
const wheelJsonFiles = stagedFiles.filter((f)=>{
return f.endsWith(".wheel.json");
});

const originals = [];
for (const relPath of wheelJsonFiles) {
const filePath = path.resolve(projectRootDir, relPath);
const originalContent = await fs.readFile(filePath, "utf8");
const json = JSON.parse(originalContent);
json.state = "not-started";
await writeJsonWrapper(filePath, json);
await gitAdd(projectRootDir, filePath);
originals.push({ filePath, originalContent });
}
return originals;
}

/**
* Restore working-tree files to their original content after a sanitized commit.
* @param {Array<{filePath: string, originalContent: string}>} originals - list returned by sanitizeStagedJsonFiles
* @returns {Promise<void>}
*/
async function restoreSanitizedJsonFiles(originals) {
for (const { filePath, originalContent } of originals) {
await fs.writeFile(filePath, originalContent, "utf8");
}
}

/**
* Append a failed task's relative path to prj.wheel.json's failedTasks array.
* Does not stage the file (execution-time write).
* @param {string} projectRootDir - project's root path
* @param {string} taskRelPath - task directory path relative to projectRootDir
* @returns {Promise<object>} - updated projectJson
*/
async function addFailedTask(projectRootDir, taskRelPath) {
const filename = path.resolve(projectRootDir, projectJsonFilename);
const projectJson = await readJsonGreedy(filename);
if (!Array.isArray(projectJson.failedTasks)) {
projectJson.failedTasks = [];
}
if (!projectJson.failedTasks.includes(taskRelPath)) {
projectJson.failedTasks.push(taskRelPath);
}
await writeJsonWrapper(filename, projectJson);
return projectJson;
}

/**
* Clear the failedTasks list from prj.wheel.json.
* Does not stage the file.
* @param {string} projectRootDir - project's root path
* @returns {Promise<void>}
*/
async function clearFailedTasks(projectRootDir) {
const filename = path.resolve(projectRootDir, projectJsonFilename);
const projectJson = await readJsonGreedy(filename);
if (projectJson.failedTasks) {
delete projectJson.failedTasks;
await writeJsonWrapper(filename, projectJson);
}
}

module.exports = {
createNewProject,
updateComponentPath,
Expand Down Expand Up @@ -1802,5 +1876,9 @@ module.exports = {
isComponentDir,
getComponentTree,
isLocal,
isSameRemoteHost
isSameRemoteHost,
sanitizeStagedJsonFiles,
restoreSanitizedJsonFiles,
addFailedTask,
clearFailedTasks
};
2 changes: 1 addition & 1 deletion server/app/db/version.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version": "2025-0928-154605" }
{"version": "2026-0312-203109" }
Loading
Loading