import 'dart:io'; import 'dart:async'; import 'package:oto_cli/framework/utils/path.dart'; import 'package:oto_cli/framework/utils/system_util.dart'; import 'package:oto_cli/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 isRegistedStartup(String label) async { return _current!.isRegistedStartup(label); } static Future registStartup(String executableFilePath, String label, {List? arguments, String? logStandardFilePath, String? logErrorFilePath}) async { return _current!.registStartup(executableFilePath, label, arguments: arguments, logStandardFilePath: logStandardFilePath, logErrorFilePath: logErrorFilePath); } static Future unregistStartup(String label) async { return _current!.unregistStartup(label); } } class _Startup { Future isRegistedStartup(String label) async { var c = Completer(); return c.future; } Future registStartup(String executableFilePath, String label, {List? arguments, String? logStandardFilePath, String? logErrorFilePath}) async { var c = Completer(); return c.future; } Future unregistStartup(String label) async { var c = Completer(); return c.future; } } /// ************* MAC *************** class _StartupMac extends _Startup { final plistTemplate = """ KeepAlive Label {LABEL} ProgramArguments {FILE_PATH} RunAtLoad {LOG_STANDARD}{LOG_ERROR} """; 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 = """ StandardOutPath {LOG_STANDARD}"""; final logErrorTemplate = """ StandardErrorPath {LOG_ERROR}"""; @override Future isRegistedStartup(String label) async { var c = Completer(); var plistName = '$label.plist'; var userPath = Path.userPath; c.complete( await File('$userPath/Library/LaunchAgents/$plistName').exists()); return c.future; } @override Future registStartup(String executableFilePath, String label, {List? arguments, String? logStandardFilePath, String? logErrorFilePath}) async { final tagLabel = '{LABEL}'; final tagPath = '{FILE_PATH}'; final plistName = '$label.plist'; final userPath = 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); 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 unregistStartup(String label) async { var c = Completer(); var success = false; var plistName = '$label.plist'; var userPath = 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} [Service] Type=simple ExecStart={FILE_PATH} [Install] WantedBy=default.target"""; @override Future isRegistedStartup(String label) async { var c = Completer(); var exist = false; var userPath = Path.userPath; exist = await File('$userPath/.config/systemd/user/$label.service').exists(); c.complete(exist); return c.future; } @override Future registStartup(String executableFilePath, String label, {List? arguments, String? logStandardFilePath, String? logErrorFilePath}) async { var c = Completer(); final tagLabel = '{LABEL}'; final tagPath = '{FILE_PATH}'; ProcessResult? result; var userPath = 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 unregistStartup(String label) async { var c = Completer(); var success = false; var userPath = 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 = ''' \$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 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 = []; 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); } @override Future registStartup(String executableFilePath, String label, {List? 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 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); } }