dart-app-core/lib/utils/os_startup.dart
2025-01-05 19:07:14 +09:00

426 lines
12 KiB
Dart

import 'dart:io';
import 'dart:async';
import 'package:dart_framework/utils/path.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:dart_framework/platform/process.dart';
import 'package:path/path.dart' as path;
class OSStartup {
static _Startup? _instance;
static _Startup? get _current {
if (_instance == null) {
if (Platform.isMacOS) {
_instance = _StartupMac();
} else if (Platform.isLinux) {
_instance = _StartupLinux();
} else {
_instance = _StartupWindows();
}
}
return _instance;
}
static Future<bool> isRegistedStartup(String label) async {
return _current!.isRegistedStartup(label);
}
//shell file must have '#!/bin/sh'
static Future<bool> registStartup(String executableFilePath, String label,
{List<String>? arguments,
String? logStandardFilePath,
String? logErrorFilePath}) async {
return _current!.registStartup(executableFilePath, label,
arguments: arguments,
logStandardFilePath: logStandardFilePath,
logErrorFilePath: logErrorFilePath);
}
static Future<bool> unregistStartup(String label) async {
return _current!.unregistStartup(label);
}
}
class _Startup {
Future<bool> isRegistedStartup(String label) async {
var c = Completer<bool>();
return c.future;
}
//shell file must have '#!/bin/sh'
Future<bool> registStartup(String executableFilePath, String label,
{List<String>? arguments,
String? logStandardFilePath,
String? logErrorFilePath}) async {
var c = Completer<bool>();
return c.future;
}
Future<bool> unregistStartup(String label) async {
var c = Completer<bool>();
return c.future;
}
}
/// ************* MAC ***************
class _StartupMac extends _Startup {
final plistTemplate = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<false/>
<key>Label</key>
<string>{LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>{FILE_PATH}</string>
</array>
<key>RunAtLoad</key>
<true/>{LOG_STANDARD}{LOG_ERROR}
</dict>
</plist>""";
final pathTemplate = """#!/bin/bash
# Load .zprofile if it exists
if [ -f ~/.zprofile ]; then
source ~/.zprofile
fi
# Load .bash_profile if it exists
if [ -f ~/.bash_profile ]; then
source ~/.bash_profile
fi
# Load .bashrc if it exists (optional)
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
# Load .zshrc if it exists (optional)
if [ -f ~/.zshrc ]; then
source ~/.zshrc
fi
# Run your command
{COMMAND}
""";
final logStandardTemplate = """
<key>StandardOutPath</key>
<string>{LOG_STANDARD}</string>""";
final logErrorTemplate = """
<key>StandardErrorPath</key>
<string>{LOG_ERROR}</string>""";
@override
Future<bool> isRegistedStartup(String label) async {
var c = Completer<bool>();
var plistName = '$label.plist';
var userPath = await Path.userPath;
c.complete(
await File('$userPath/Library/LaunchAgents/$plistName').exists());
return c.future;
}
//shell file must have '#!/bin/sh'
@override
Future<bool> registStartup(String executableFilePath, String label,
{List<String>? arguments,
String? logStandardFilePath,
String? logErrorFilePath}) async {
final tagLabel = '{LABEL}';
final tagPath = '{FILE_PATH}';
final plistName = '$label.plist';
final userPath = await Path.userPath;
var success = false;
final appDir = Directory('$userPath/Library/$label');
final file = File('${appDir.path}/launch.sh');
if (!appDir.existsSync()) {
appDir.createSync(recursive: true);
}
var param = arguments == null ? '' : ' ${arguments.join(' ')}';
var launchContent =
pathTemplate.replaceAll('{COMMAND}', '$executableFilePath$param');
file.writeAsStringSync(launchContent);
//create plist in LaunchAgents
final tagLogStandard = '{LOG_STANDARD}';
final tagLogError = '{LOG_ERROR}';
var plistContent = plistTemplate
.replaceAll(tagLabel, label)
.replaceAll(tagPath, file.path);
if (logStandardFilePath == null) {
plistContent = plistContent.replaceAll(tagLogStandard, '');
} else {
plistContent = plistContent.replaceAll(tagLogStandard,
logStandardTemplate.replaceAll(tagLogStandard, logStandardFilePath));
}
if (logErrorFilePath == null) {
plistContent = plistContent.replaceAll(tagLogError, '');
} else {
plistContent = plistContent.replaceAll(tagLogError,
logErrorTemplate.replaceAll(tagLogError, logErrorFilePath));
}
var plistFile = File('$userPath/Library/LaunchAgents/$plistName');
await plistFile.create();
await plistFile.writeAsString(plistContent);
var result = await ProcessExecutor.run(StringBuffer(
'chmod +x "$executableFilePath" && chmod +x "${file.path}" '));
success = result.exitCode == 0;
return dataFutrue(success);
}
@override
Future<bool> unregistStartup(String label) async {
var c = Completer<bool>();
var success = false;
var plistName = '$label.plist';
var userPath = await Path.userPath;
var file = File('$userPath/Library/LaunchAgents/$plistName');
if (file.existsSync()) {
await file.delete();
success = true;
}
c.complete(success);
return c.future;
}
}
/// ************* LINUX ***************
class _StartupLinux extends _Startup {
final serviceTemplate = """[Unit]
Description={LABEL}
#Documentation=http://toki-labs.com
[Service]
Type=simple
#User=deck
#Group=deck
#TimeoutStartSec=0
#Restart=on-failure
#RestartSec=30s
#ExecStartPre=
ExecStart={FILE_PATH}
#SyslogIdentifier=TestService
#ExecStop=
[Install]
WantedBy=default.target""";
@override
Future<bool> isRegistedStartup(String label) async {
var c = Completer<bool>();
var exist = false;
var userPath = await Path.userPath;
exist =
await File('$userPath/.config/systemd/user/$label.service').exists();
c.complete(exist);
return c.future;
}
//shell file must have '#!/bin/sh'
@override
Future<bool> registStartup(String executableFilePath, String label,
{List<String>? arguments,
String? logStandardFilePath,
String? logErrorFilePath}) async {
var c = Completer<bool>();
final tagLabel = '{LABEL}';
final tagPath = '{FILE_PATH}';
ProcessResult? result;
var userPath = await Path.userPath;
var systemPath = '$userPath/.config/systemd/user';
if (arguments != null) {
for (var arg in arguments) {
executableFilePath += ' $arg';
}
}
var serviceContent = serviceTemplate
.replaceAll(tagLabel, label)
.replaceAll(tagPath, executableFilePath);
var serviceFile = File('$systemPath/$label.service');
await serviceFile.create();
await serviceFile.writeAsString(serviceContent);
var shell = StringBuffer();
shell.write('cd $systemPath && ');
shell.write('systemctl --user enable $label && ');
shell.write('systemctl --user daemon-reload');
result = await ProcessExecutor.run(shell);
print('exitCode: ${result.exitCode}');
c.complete(result.exitCode == 0);
return c.future;
}
@override
Future<bool> unregistStartup(String label) async {
var c = Completer<bool>();
var success = false;
var userPath = await Path.userPath;
var shell = StringBuffer();
var systemPath = '$userPath/.config/systemd/user';
shell.write('cd $systemPath && ');
shell.write('systemctl --user stop $label && ');
shell.write('systemctl --user disable $label && ');
shell.write('systemctl --user daemon-reload');
var result = await ProcessExecutor.run(shell);
success = result.exitCode == 0;
if (success) {
success = false;
var file = File('$systemPath/$label.service');
if (await file.exists()) {
await file.delete();
success = true;
}
}
c.complete(success);
return c.future;
}
}
/// ************* WINDOWS ***************
class _StartupWindows extends _Startup {
final startupScriptTemplate = '''Start-Process -FilePath "{EXECUTABLE_PATH}" `
{ARGUMENTS}-WindowStyle Hidden{LOG_STDOUT}{LOG_STDERR}''';
final schedulerScriptTemplate = '''
# 실행할 PS1 스크립트 경로
\$ScriptPath = "{SCRIPT_PATH}"
# 작업 이름 설정
\$TaskName = "{TASK_NAME}"
# 트리거 정의 (사용자 로그온 시 실행)
\$Trigger = New-ScheduledTaskTrigger -AtLogOn
\$Trigger
# 작업 동작 정의
\$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"\$ScriptPath`""
\$Action
# 작업 등록
Register-ScheduledTask -TaskName \$TaskName -Action \$Action -Trigger \$Trigger
Exit
''';
String getScriptPath(String label) {
return path.join(dataPath.replaceAll('/', '\\'), 'startup', '$label.ps1');
}
@override
Future<bool> isRegistedStartup(String label) async {
await ProcessExecutor.initPowershell();
var shell = 'Get-ScheduledTask | Where-Object {\$_.TaskName -eq "$label"}';
var process = await Process.run('powershell', [shell]);
var exist = false;
if (process.exitCode == 0) {
var existLine = <String>[];
var stdoutList = process.stdout.toString().split('\n');
for (var item in stdoutList) {
var result = item.trim();
if (result.isNotEmpty) {
existLine.add(item);
}
}
if (existLine.length > 2) {
exist = true;
}
}
return dataFutrue(exist);
}
//shell file must have '#!/bin/sh'
@override
Future<bool> registStartup(String executableFilePath, String label,
{List<String>? arguments,
String? logStandardFilePath,
String? logErrorFilePath}) async {
var scriptPath = getScriptPath(label);
var startupScript = startupScriptTemplate.replaceAll(
'{EXECUTABLE_PATH}', executableFilePath.replaceAll('/', '\\'));
var args = '';
if (arguments != null) {
args = '''-ArgumentList "${arguments.join(' ')}" `
''';
}
startupScript = startupScript.replaceAll('{ARGUMENTS}', args);
var logStdout = '';
if (logStandardFilePath != null) {
logStdout = ''' `
-RedirectStandardOutput "$logStandardFilePath"''';
}
startupScript = startupScript.replaceAll('{LOG_STDOUT}', logStdout);
var logStderr = '';
if (logErrorFilePath != null) {
logStderr = ''' `
-RedirectStandardError "$logErrorFilePath"''';
}
startupScript = startupScript.replaceAll('{LOG_STDERR}', logStderr);
var startupFile = File(scriptPath);
startupFile.createSync(recursive: true);
startupFile.writeAsStringSync(startupScript, flush: true);
var schedulerScript = schedulerScriptTemplate
.replaceAll('{SCRIPT_PATH}', scriptPath)
.replaceAll('{TASK_NAME}', label);
var tempFile =
File('${Directory.systemTemp.path}\\${ProcessExecutor.tempFileName}');
tempFile.createSync(recursive: true);
tempFile.writeAsStringSync(schedulerScript, flush: true);
var powershellCommand =
'Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `& \'${tempFile.path}\'; exit" -Verb RunAs -WindowStyle Hidden';
await ProcessExecutor.initPowershell();
var process = await Process.run(
'powershell', ['-Command', powershellCommand, '-Wait']);
print(process.stdout.toString());
print(process.stderr.toString());
print(process.exitCode);
tempFile.deleteSync();
return dataFutrue(process.exitCode == 0);
}
@override
Future<bool> unregistStartup(String label) async {
await ProcessExecutor.initPowershell();
var shell = 'Unregister-ScheduledTask -TaskName "$label" -Confirm:\$false';
var process = await Process.run(
'powershell',
[
'-Command',
'Start-Process powershell -ArgumentList \'-NoProfile -ExecutionPolicy Bypass -Command "$shell"\' -Verb RunAs -WindowStyle Hidden'
],
runInShell: true,
);
print(process.stdout.toString());
print(process.stderr.toString());
print(process.exitCode);
var scriptFile = File(getScriptPath(label));
if (scriptFile.existsSync()) {
scriptFile.deleteSync();
}
return dataFutrue(process.exitCode == 0);
}
}