Skip to content

Commit e3787fb

Browse files
committed
fix(install): prevent Docker install hang on Ubuntu
- _log_pipe: use stdbuf -o0 to force unbuffered tr output, ensuring \r->\n conversion reaches read immediately - Add _setup_policy_rc_d / _cleanup_policy_rc_d helpers to prevent dpkg from starting Docker during apt-get install - install_docker_official: use policy-rc.d + trap RETURN before running get-docker.sh - install_docker_custom Debian/Ubuntu path: same protection - Add test_install_docker_fixes.sh with 18 simulated tests - Also includes console i18n updates, apphub service fix, my-apps page improvements, Dockerfile adjustments
1 parent 1bbd477 commit e3787fb

8 files changed

Lines changed: 2063 additions & 1358 deletions

File tree

apphub/src/services/app_manager.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ def get_apps(self,endpointId:int = None, locale: str = "en"):
851851
logo_url=self._resolve_available_app_logo_url(logo_map, app_name, stack_name) if app_name else None,
852852
app_dist=app_dist,
853853
app_version=app_version,
854-
app_official=bool(app_name) or bool(gitConfig),
854+
app_official=bool(app_name),
855855
is_compose_app=is_compose,
856856
is_php_app=is_php_app,
857857
is_monitor_app=is_monitor_app,
@@ -1073,7 +1073,7 @@ def get_app_by_id(self,app_id:str,endpointId:int = None, locale: str = "en"):
10731073
logo_url = self._resolve_available_app_logo_url(logo_map, app_name, app_id),
10741074
app_dist = app_dist,
10751075
app_version = app_version,
1076-
app_official = True,
1076+
app_official = bool(app_name),
10771077
is_compose_app = is_compose,
10781078
is_php_app = is_php_app,
10791079
is_monitor_app = is_monitor_app,
@@ -1106,7 +1106,7 @@ def get_app_by_id(self,app_id:str,endpointId:int = None, locale: str = "en"):
11061106
logo_url = self._resolve_available_app_logo_url(logo_map, app_name, app_id),
11071107
app_dist = inactive_app_dist,
11081108
app_version = inactive_app_version,
1109-
app_official = True,
1109+
app_official = bool(app_name),
11101110
is_compose_app = is_compose,
11111111
is_php_app = is_php_app,
11121112
is_monitor_app = is_monitor_app,
@@ -1413,15 +1413,14 @@ async def send_log(message: str):
14131413
# Get the appInstallApps
14141414
appInstallApps = AppManger().get_apps(endpointId)
14151415

1416-
# Get all apps that are official and active
1417-
app_official = [app for app in appInstallApps if app.app_official == True and app.status == 1 ]
1416+
# Get all apps that are official and active (or inactive)
1417+
app_official_active = [app for app in appInstallApps if app.app_official == True and app.status == 1]
14181418

1419-
# if app_id is active,can not check the apps number
1420-
if not any(app.app_id == app_id for app in app_official):
1421-
# Chenck the apps number
1419+
# Only check limit for official (app store) apps. Compose/external apps are not subject to the limit.
1420+
is_official = any(app.app_id == app_id and app.app_official == True for app in appInstallApps)
1421+
if is_official and not any(app.app_id == app_id for app in app_official_active):
14221422
check_apps_number(endpointId)
14231423

1424-
#fix bug(上面排除了状态为Inactive的,导致状态为Inactive的不能重建,这里重新加入)
14251424
app_official = [app for app in appInstallApps if app.app_official == True and (app.status == 1 or app.status == 2)]
14261425

14271426
portainerManager = PortainerManager()

console/src/features/app-store/app-store-page.tsx

Lines changed: 1524 additions & 1338 deletions
Large diffs are not rendered by default.

console/src/features/my-apps/my-apps-page.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
transition: box-shadow 0.15s, border-color 0.15s, transform 0.15s;
181181
min-height: 246px;
182182
justify-content: flex-start;
183+
overflow: hidden;
183184
}
184185

185186
.myapps-vcard--managed {
@@ -203,6 +204,33 @@
203204
.myapps-vcard-top {
204205
width: 100%;
205206
min-height: 22px;
207+
display: flex;
208+
justify-content: flex-end;
209+
align-items: flex-start;
210+
}
211+
212+
/* ---- Corner ribbon ---- */
213+
.myapps-vcard-ribbon {
214+
position: absolute;
215+
top: 8px;
216+
left: -34px;
217+
width: 120px;
218+
padding: 3px 0 2px;
219+
text-align: center;
220+
font-size: 9px;
221+
font-weight: 700;
222+
letter-spacing: 0.4px;
223+
line-height: 1.3;
224+
color: #fff;
225+
background: #64748b;
226+
transform: rotate(-45deg);
227+
z-index: 2;
228+
pointer-events: none;
229+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
230+
}
231+
232+
.myapps-vcard-ribbon span {
233+
display: block;
206234
}
207235

208236
.myapps-vcard-icon {

console/src/features/my-apps/my-apps-page.tsx

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,51 @@ async function runRedeployRequest(appId: string, pullImage: boolean) {
164164
credentials: 'include',
165165
headers: { Accept: 'text/plain' },
166166
})
167-
if (!response.ok) {
167+
if (!response.ok || !response.body) {
168168
throw new Error(await parseJsonError(response, `Redeploy failed: ${response.status}`))
169169
}
170-
await response.text()
170+
171+
const reader = response.body.getReader()
172+
const decoder = new TextDecoder()
173+
let buffer = ''
174+
let finalStatus: 'success' | 'failed' | null = null
175+
let lastErrorDetail: string | null = null
176+
177+
while (true) {
178+
const { done, value } = await reader.read()
179+
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done })
180+
const lines = buffer.split('\n')
181+
buffer = lines.pop() ?? ''
182+
183+
for (const line of lines) {
184+
if (!line.trim()) continue
185+
try {
186+
const entry = JSON.parse(line) as { status?: string; type?: string; details?: string; message?: string }
187+
if (entry.status === 'failed' || entry.type === 'error') {
188+
finalStatus = 'failed'
189+
if (entry.details) lastErrorDetail = entry.details
190+
else if (entry.message) lastErrorDetail = entry.message
191+
}
192+
if (entry.status === 'success') finalStatus = 'success'
193+
} catch { /* skip unparseable lines */ }
194+
}
195+
196+
if (done) break
197+
}
198+
199+
if (buffer.trim()) {
200+
try {
201+
const entry = JSON.parse(buffer) as { status?: string; type?: string; details?: string; message?: string }
202+
if (entry.status === 'failed' || entry.type === 'error') {
203+
finalStatus = 'failed'
204+
if (entry.details) lastErrorDetail = entry.details
205+
else if (entry.message) lastErrorDetail = entry.message
206+
}
207+
if (entry.status === 'success') finalStatus = 'success'
208+
} catch { /* skip */ }
209+
}
210+
211+
if (finalStatus !== 'success') throw new Error(lastErrorDetail ?? 'Redeploy did not complete successfully.')
171212
}
172213

173214
// =====================
@@ -549,15 +590,16 @@ export function MyAppsPage() {
549590
[apps, searchValue, selectedStatus],
550591
)
551592

552-
const platformApps = useMemo(() => filteredApps.filter((app) => app.app_official), [filteredApps])
553-
const otherApps = useMemo(() => filteredApps.filter((app) => !app.app_official), [filteredApps])
593+
const isWebsoft9App = (app: MyApp) => app.app_official || Boolean(app.gitConfig && Object.keys(app.gitConfig).length > 0)
594+
const platformApps = useMemo(() => filteredApps.filter(isWebsoft9App), [filteredApps])
595+
const otherApps = useMemo(() => filteredApps.filter((app) => !isWebsoft9App(app)), [filteredApps])
554596

555597
const hasVisiblePlatformApps = platformApps.length > 0
556598
const hasVisibleOtherApps = otherApps.length > 0
557599
const showLoadingState = isLoading || manualRefreshing
558600

559601
function handleCardClick(app: MyApp) {
560-
if (!app.app_official) return
602+
if (!isWebsoft9App(app)) return
561603
if (app.status === 1) {
562604
const contentScopeContainer = typeof document === 'undefined' ? null : document.querySelector('#app-shell-main')
563605
const backgroundScrollTop = contentScopeContainer instanceof HTMLElement ? contentScopeContainer.scrollTop : 0
@@ -613,7 +655,7 @@ export function MyAppsPage() {
613655

614656
function renderCards(appList: MyApp[], variant: 'managed' | 'other') {
615657
return appList.map((app) => {
616-
const canOpenDetail = app.app_official
658+
const canOpenDetail = app.app_official || Boolean(app.gitConfig && Object.keys(app.gitConfig).length > 0)
617659
const showStatus = variant === 'managed' && canOpenDetail
618660
const logoSize = 80
619661

@@ -697,6 +739,11 @@ export function MyAppsPage() {
697739
tabIndex={canOpenDetail ? 0 : undefined}
698740
onKeyDown={canOpenDetail ? (e) => { if (e.key === 'Enter' || e.key === ' ') handleCardClick(app) } : undefined}
699741
>
742+
{!app.app_official && isWebsoft9App(app) ? (
743+
<div className="myapps-vcard-ribbon">
744+
<span>{t('myAppsPage.card.customDeploy')}</span>
745+
</div>
746+
) : null}
700747
<div className="myapps-vcard-top">
701748
{actionsNode ? <div className="myapps-vcard-actions">{actionsNode}</div> : <div className="myapps-vcard-actions myapps-vcard-actions--placeholder" />}
702749
</div>

console/src/shared/i18n/resources.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ const rawShellResources = {
748748
portHelper: 'Use a unique integer between 1 and 65535.',
749749
setGlobalDomain: 'Set global domain',
750750
addDomain: 'Add Domain',
751-
customDomainLabel: 'Custom access address (optional)',
751+
customDomainLabel: 'Custom domain (optional)',
752752
customDomainPlaceholder: 'For example: blog.example.com',
753753
customDomainHelper: 'Users will open the app with this address. Leave it empty to use the default address generated by Websoft9. Do not include http://, https://, ports, or paths.',
754754
defaultDomainHelper: 'If enabled, Websoft9 will also create the default access address {{domain}}.',
@@ -760,9 +760,14 @@ const rawShellResources = {
760760
appId: 'Please enter a custom application name between 2 and 20 characters.Cannot start with a number.',
761761
customDomain: 'Please enter a custom domain name.',
762762
customDomainFormat: 'Enter a valid address such as blog.example.com. Do not include http://, https://, ports, or paths.',
763+
customDomainDuplicate: 'Domain {{domain}} is already in the list.',
764+
customDomainConflictWithDefault: 'Domain {{domain}} is the same as the default access address. No need to add it again.',
763765
port: '{{name}} must be an integer between 1 and 65535.',
764766
portDuplicate: 'Port {{port}} is used more than once. Each port must be unique.',
765767
},
768+
addCustomDomain: 'Add custom domain',
769+
removeCustomDomain: 'Remove custom domain',
770+
customDomainListLabel: 'Custom domain list',
766771
submitting: 'Installing...',
767772
feedback: {
768773
success: 'The application has started installing. You can continue tracking it in My Apps.',
@@ -784,12 +789,15 @@ const rawShellResources = {
784789
favoriteList: 'My favorites ({{count}})',
785790
refresh: 'App sync',
786791
refreshing: 'App sync in progress…',
792+
localRefresh: 'Refresh',
793+
localRefreshing: 'Refreshing…',
787794
syncRunningShort: 'Syncing apps',
788795
syncIdleShort: 'Not synced yet',
789796
lastSyncShort: 'Last sync {{value}}',
790797
syncRunningHint: 'Background app sync in progress',
791798
syncIdleHint: 'Manual sync is available',
792799
lastSyncAt: 'Last sync {{value}}',
800+
syncNever: 'Sync has not been performed yet',
793801
refreshConfirmTitle: 'Sync app list',
794802
refreshConfirmMessage: 'Websoft9 will check for the latest app list in the background. You can keep browsing and installing apps while the sync runs.',
795803
confirmRefresh: 'Sync now',
@@ -878,6 +886,9 @@ const rawShellResources = {
878886
openAccess: 'Open access',
879887
openLogs: 'Open install log',
880888
openError: 'Open error details',
889+
customDeploy: 'Custom',
890+
appStoreSource: 'Installed from App Store',
891+
composeSource: 'Deployed via Compose',
881892
},
882893
dialog: {
883894
logsTitle: 'Installation log',
@@ -2979,7 +2990,7 @@ const rawShellResources = {
29792990
portHelper: '请填写 1 到 65535 之间且不重复的整数端口。',
29802991
setGlobalDomain: '设置全局域名',
29812992
addDomain: '添加域名',
2982-
customDomainLabel: '自定义访问地址(可选)',
2993+
customDomainLabel: '自定义域名(可选)',
29832994
customDomainPlaceholder: '例如:blog.example.com',
29842995
customDomainHelper: '用户将通过这个地址访问应用。留空时,Websoft9 会使用默认生成的访问地址。不要输入 http://、https://、端口或路径。',
29852996
defaultDomainHelper: '启用后,Websoft9 还会同时生成默认访问地址 {{domain}}。',
@@ -2991,9 +3002,14 @@ const rawShellResources = {
29913002
appId: '请输入一个2-20位的自定义应用名称.不允许以数字开头.',
29923003
customDomain: '请输入自定义域名',
29933004
customDomainFormat: '请输入有效的访问地址,例如 blog.example.com。不要输入 http://、https://、端口或路径。',
3005+
customDomainDuplicate: '域名 {{domain}} 已存在列表中。',
3006+
customDomainConflictWithDefault: '域名 {{domain}} 与默认访问地址相同,无需重复添加。',
29943007
port: '{{name}} 必须是 1 到 65535 之间的整数。',
29953008
portDuplicate: '端口 {{port}} 被重复使用了,每个端口都必须唯一。',
29963009
},
3010+
addCustomDomain: '添加自定义域名',
3011+
removeCustomDomain: '删除自定义域名',
3012+
customDomainListLabel: '自定义域名列表',
29973013
submitting: '安装中...',
29983014
feedback: {
29993015
success: '应用已经开始安装, 你可以到我的应用中继续跟踪。',
@@ -3015,12 +3031,15 @@ const rawShellResources = {
30153031
favoriteList: '我的收藏({{count}})',
30163032
refresh: '应用同步',
30173033
refreshing: '应用同步中…',
3034+
localRefresh: '刷新',
3035+
localRefreshing: '刷新中…',
30183036
syncRunningShort: '应用同步中',
30193037
syncIdleShort: '尚未同步',
30203038
lastSyncShort: '上次同步 {{value}}',
30213039
syncRunningHint: '后台应用同步进行中',
30223040
syncIdleHint: '可手动发起同步',
30233041
lastSyncAt: '上次同步 {{value}}',
3042+
syncNever: '尚未执行过同步',
30243043
refreshConfirmTitle: '同步应用列表',
30253044
refreshConfirmMessage: 'Websoft9 会在后台检查并更新最新应用列表。同步过程中,你可以继续浏览和安装应用。',
30263045
confirmRefresh: '立即同步',
@@ -3109,6 +3128,9 @@ const rawShellResources = {
31093128
openAccess: '打开访问入口',
31103129
openLogs: '查看安装日志',
31113130
openError: '查看错误详情',
3131+
customDeploy: '自定义',
3132+
appStoreSource: '应用商店安装',
3133+
composeSource: '自定义部署',
31123134
},
31133135
dialog: {
31143136
logsTitle: '安装日志',

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ RUN WEBSOFT9_RUNTIME_ASSET_SYNC_MODE=build \
201201

202202
RUN printf '%s\n' '#!/bin/sh' 'exec python3 -m supervisor.supervisord "$@"' > /usr/local/bin/supervisord && \
203203
printf '%s\n' '#!/bin/sh' 'exec python3 -m supervisor.supervisorctl "$@"' > /usr/local/bin/supervisorctl && \
204-
printf '%s\n' '#!/bin/sh' 'exec python3 /websoft9/apphub/src/cli/apphub_cli.py "$@"' > /usr/local/bin/apphub && \
204+
printf '%s\n' '#!/bin/sh' 'export PYTHONPATH="/opt/websoft9-pydeps:/websoft9/apphub:/websoft9/apphub/src"' 'exec python3 /websoft9/apphub/src/cli/apphub_cli.py "$@"' > /usr/local/bin/apphub && \
205205
chmod +x /usr/local/bin/supervisord /usr/local/bin/supervisorctl /usr/local/bin/apphub
206206

207207
RUN chmod +x /app/init_nginx.sh \

0 commit comments

Comments
 (0)