Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 104c083995 | |||
| 189ca80343 |
+1
-1
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user