Compare commits

...

2 Commits

Author SHA1 Message Date
chyoo 104c083995 version up! 2026-09-08 15:13:04 +09:00
chyoo 189ca80343 (fix) module refresh bug.. 2026-09-08 15:12:13 +09:00
3 changed files with 103 additions and 27 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "lms",
"version": "1.1.22",
"version": "1.1.23",
"description": "Autonomy Studio LMS Electron app",
"main": "./out/main/index.js",
"author": {
+63 -10
View File
@@ -954,6 +954,35 @@ function customModuleConfigContent(input: CustomModuleScaffoldInput): string {
}, null, 2)}\n`
}
function buildCustomModuleWorkspaceFiles(input: CustomModuleScaffoldInput): Record<string, string> {
const module = validateCustomModuleScaffold(input)
const header = [
'#!/usr/bin/env bash',
'set -euo pipefail',
'',
'MODULE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
'RUNTIME_ROOT="${AUTONOMY_STUDIO_RUNTIME_ROOT:-$HOME/autonomy-studio/runtime/nav2}"',
'FOLDER_NAME="$(basename "$MODULE_PATH")"',
`DEFAULT_IMAGE=${quoteShellSingle(module.dockerImage || 'future-nav2:humble')}`,
'IMAGE="${NAV2_DOCKER_IMAGE:-$DEFAULT_IMAGE}"',
]
return {
'.autonomy-studio-module.json': customModuleConfigContent(module),
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
'build.sh': [...header,
'exec bash "$RUNTIME_ROOT/docker/docker_build_nav2.sh" "$(dirname "$MODULE_PATH")" "$FOLDER_NAME" "$IMAGE" "${ROS_DOMAIN_ID:-0}" "$@"',
'',
].join('\n'),
'run.sh': [...header,
'CONTAINER_NAME="${FOLDER_NAME#\\[}"',
'CONTAINER_NAME="${CONTAINER_NAME/\\]/-}"',
'CONTAINER_NAME="${CONTAINER_NAME%_ws}"',
'exec bash "$RUNTIME_ROOT/docker/docker_run_nav2.sh" "$CONTAINER_NAME" "$(dirname "$MODULE_PATH")" "$FOLDER_NAME" "$IMAGE" "${ROS_DOMAIN_ID:-0}" "${GPU_ENABLED:-false}" "$@"',
'',
].join('\n'),
}
}
async function resolveCustomModuleImportSource(sourcePath: string): Promise<CustomModuleImportSource> {
const selectedPath = sourcePath.trim()
if (!selectedPath) throw new Error('불러올 모듈 폴더를 선택하세요.')
@@ -1064,16 +1093,23 @@ async function importCustomWorkspaceModule(
await mkdir(resolvedWorkspaceRoot, { recursive: true })
try {
await cp(source.sourcePath, moduleRoot, {
const packageRoot = join(moduleRoot, 'src', module.packageName)
await mkdir(dirname(packageRoot), { recursive: true })
await cp(source.sourcePath, packageRoot, {
recursive: true,
errorOnExist: true,
force: false,
filter: (currentPath) => {
const pathParts = relative(source.sourcePath, currentPath).split(/[\\/]/).filter(Boolean)
return !pathParts.some((part) => ignoredCustomModuleImportDirectories.has(part))
&& relative(source.sourcePath, currentPath) !== '.autonomy-studio-module.json'
},
})
await writeFile(resolve(moduleRoot, '.autonomy-studio-module.json'), customModuleConfigContent(module), { encoding: 'utf8', flag: 'wx' })
for (const [fileName, content] of Object.entries(buildCustomModuleWorkspaceFiles(module))) {
await writeFile(join(moduleRoot, fileName), content, {
encoding: 'utf8', flag: 'wx', mode: fileName.endsWith('.sh') ? 0o755 : 0o644,
})
}
await runCommand('git', ['init', '--initial-branch=main'], moduleRoot, undefined, 15_000).catch(() => undefined)
} catch (error) {
await rm(moduleRoot, { recursive: true, force: true }).catch(() => undefined)
@@ -1086,9 +1122,7 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string
const module = validateCustomModuleScaffold(input)
const executable = `${module.packageName}_node`
const nodePath = `${module.packageName}/node.py`
return {
'.autonomy-studio-module.json': customModuleConfigContent(module),
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
const packageFiles = {
'package.xml': [
'<?xml version="1.0"?>',
'<package format="3">',
@@ -1161,6 +1195,12 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string
'',
].join('\n'),
}
return {
...buildCustomModuleWorkspaceFiles(module),
...Object.fromEntries(Object.entries(packageFiles).map(([path, content]) => [
`src/${module.packageName}/${path}`, content,
])),
}
}
async function createCustomWorkspaceModule(
@@ -1172,12 +1212,16 @@ async function createCustomWorkspaceModule(
if (!resolvedWorkspaceRoot) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.')
const moduleRoot = resolve(resolvedWorkspaceRoot, module.folderName)
const files = buildCustomModuleFiles(module)
const legacyLayout = existsSync(join(moduleRoot, 'package.xml'))
await mkdir(moduleRoot, { recursive: true })
for (const [relativePath, content] of Object.entries(files)) {
const targetPath = resolve(moduleRoot, relativePath)
const filePath = legacyLayout && relativePath.startsWith(`src/${module.packageName}/`)
? relativePath.slice(`src/${module.packageName}/`.length)
: relativePath
const targetPath = resolve(moduleRoot, filePath)
if (targetPath !== moduleRoot && !targetPath.startsWith(`${moduleRoot}/`)) throw new Error('허용되지 않은 모듈 파일 경로입니다.')
await mkdir(dirname(targetPath), { recursive: true })
await writeFile(targetPath, content, { encoding: 'utf8', flag: 'wx' }).catch((error: NodeJS.ErrnoException) => {
await writeFile(targetPath, content, { encoding: 'utf8', flag: 'wx', mode: filePath.endsWith('.sh') ? 0o755 : 0o644 }).catch((error: NodeJS.ErrnoException) => {
if (error.code !== 'EEXIST') throw error
})
}
@@ -1192,7 +1236,7 @@ async function createRemoteCustomWorkspaceModule(
const target = normalizeRemoteTarget(targetInput)
const module = validateCustomModuleScaffold(input)
const files = buildCustomModuleFiles(module)
const payload = encodeBase64(JSON.stringify({ folderName: module.folderName, files }))
const payload = encodeBase64(JSON.stringify({ folderName: module.folderName, packageName: module.packageName, files }))
const output = await runSshOutput(target, [
'set -e',
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
@@ -1206,12 +1250,16 @@ async function createRemoteCustomWorkspaceModule(
'target = os.path.abspath(os.path.join(root, folder))',
'if target == root or not target.startswith(root + os.sep): raise SystemExit("invalid module path")',
'os.makedirs(target, exist_ok=True)',
'legacy = os.path.isfile(os.path.join(target, "package.xml"))',
'prefix = "src/" + payload["packageName"] + "/"',
'for relative, content in payload["files"].items():',
' if legacy and relative.startswith(prefix): relative = relative[len(prefix):]',
' path = os.path.abspath(os.path.join(target, relative))',
' if not path.startswith(target + os.sep): raise SystemExit("invalid module file path")',
' os.makedirs(os.path.dirname(path), exist_ok=True)',
' try:',
' with open(path, "x", encoding="utf-8") as fp: fp.write(content)',
' if relative in ("build.sh", "run.sh"): os.chmod(path, 0o755)',
' except FileExistsError:',
' pass',
'print(target)',
@@ -4572,8 +4620,13 @@ function buildNav2DockerBuildScript(): string {
'',
'MODULE_PATH="$MODULE_ROOT/$MODULE_FOLDER"',
'if [[ "$MODULE_FOLDER" == *lms-nav2-custom* ]]; then',
' test -f "$MODULE_PATH/package.xml" || { echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
' SOURCE_BASE_PATH=.',
' if [ -f "$MODULE_PATH/src/$TARGET/package.xml" ]; then',
' SOURCE_BASE_PATH=src',
' elif [ -f "$MODULE_PATH/package.xml" ]; then',
' SOURCE_BASE_PATH=.',
' else',
' echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1',
' fi',
'else',
' test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
' SOURCE_BASE_PATH=src',
@@ -105,6 +105,9 @@ export function useWorkspaceModuleList({
const moduleInputsRef = useRef({ curriculumModules, applyModuleParameterOverrides })
const loadSequenceRef = useRef(0)
const moduleLoadInFlightRef = useRef(false)
const queuedModuleLoadRef = useRef<(() => void) | null>(null)
const workspaceScopeKey = remoteWorkspace ? `remote:${remoteWorkspaceTargetKey}` : `local:${clonePathPreview}`
const previousWorkspaceScopeRef = useRef(workspaceScopeKey)
const pendingModuleStatesRef = useRef(new Map<string, { state: ModuleRow['state']; expiresAt: number }>())
const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name
@@ -173,7 +176,10 @@ export function useWorkspaceModuleList({
const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise<void> => {
// Remote probes open an SSH session. Do not let polling overlap a slow
// probe, otherwise every response becomes stale and loading never settles.
if (moduleLoadInFlightRef.current) return
if (moduleLoadInFlightRef.current) {
queuedModuleLoadRef.current = () => { void loadAvailableModules({ silent }) }
return
}
moduleLoadInFlightRef.current = true
const requestSequence = ++loadSequenceRef.current
if (!silent) setModulesLoading(true)
@@ -212,6 +218,9 @@ export function useWorkspaceModuleList({
} finally {
moduleLoadInFlightRef.current = false
if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false)
const queuedLoad = queuedModuleLoadRef.current
queuedModuleLoadRef.current = null
queuedLoad?.()
}
}, [
applyPendingModuleStates,
@@ -223,24 +232,38 @@ export function useWorkspaceModuleList({
])
useEffect(() => {
// A response for an earlier module set must not replace newly added rows.
++loadSequenceRef.current
const sameWorkspace = previousWorkspaceScopeRef.current === workspaceScopeKey
previousWorkspaceScopeRef.current = workspaceScopeKey
if (!sameWorkspace) pendingModuleStatesRef.current.clear()
const cachedModules = readRemoteModuleCache(moduleCacheKey)
if (cachedModules) {
const modules = applyPendingModuleStates(
moduleInputsRef.current.applyModuleParameterOverrides(cachedModules).sort(compareModulePriority),
)
setAvailableModules(modules)
setModuleListError(null)
setModulesLoading(false)
if (skipInitialModuleLoad) return
}
setAvailableModules((current) => {
const previous = new Map((sameWorkspace && current.length ? current : cachedModules ?? [])
.map((module) => [module.folderName ?? module.name, module]))
const modules = makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules).map((module) => {
const existing = previous.get(module.folderName ?? module.name)
return existing ? {
...module,
state: existing.state,
workspacePath: existing.workspacePath,
parameters: existing.parameters,
cpu: existing.cpu,
memory: existing.memory,
runtime: existing.runtime,
} : module
})
return applyPendingModuleStates(moduleInputsRef.current.applyModuleParameterOverrides(modules))
})
setModuleListError(null)
setModulesLoading(false)
if (!(cachedModules && skipInitialModuleLoad)) void loadAvailableModules()
if (!cachedModules) {
setAvailableModules(makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules))
setModuleListError(null)
return () => {
++loadSequenceRef.current
queuedModuleLoadRef.current = null
}
void loadAvailableModules()
}, [applyPendingModuleStates, loadAvailableModules, moduleCacheKey, remoteWorkspaceTargetKey, skipInitialModuleLoad])
}, [applyPendingModuleStates, loadAvailableModules, moduleCacheKey, skipInitialModuleLoad, workspaceScopeKey])
useEffect(() => {
if (!pollingActive) return