mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 03:50:08 +02:00
♻️ Rework Shell as singleton, logging
This commit is contained in:
@ -18,6 +18,7 @@
|
||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
||||
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* BootChecks.swift */; };
|
||||
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; };
|
||||
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -35,6 +36,7 @@
|
||||
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
||||
C4D8016522B1584700C6DA1B /* BootChecks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootChecks.swift; sourceTree = "<group>"; };
|
||||
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -73,6 +75,7 @@
|
||||
C41E181522CB614C0072CF09 /* Terminal */,
|
||||
C41C1B4E22B024F100E7CF16 /* Helpers */,
|
||||
C41E181822CB62200072CF09 /* View Controllers */,
|
||||
C4F8C0A222D4F100002EFE61 /* Extensions */,
|
||||
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */,
|
||||
C41C1B3F22B0098000E7CF16 /* Info.plist */,
|
||||
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
|
||||
@ -124,6 +127,14 @@
|
||||
path = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -195,6 +206,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C4D8016622B1584700C6DA1B /* BootChecks.swift in Sources */,
|
||||
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
|
||||
C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */,
|
||||
C41C1B4D22B0215A00E7CF16 /* Services.swift in Sources */,
|
||||
C41C1B4922B00A9800E7CF16 /* ImageGenerator.swift in Sources */,
|
||||
|
@ -13,6 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
let sharedShell : Shell
|
||||
let statusItem = NSStatusBar.system.statusItem(withLength: 32)
|
||||
var timer: Timer?
|
||||
var version: PhpVersion? = nil
|
||||
@ -21,6 +22,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
var log: String = ""
|
||||
var windowController: NSWindowController? = nil
|
||||
|
||||
override init() {
|
||||
self.sharedShell = Shell.shared
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
@ -30,17 +36,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
BootChecks.perform()
|
||||
self.availablePhpVersions = Services.detectPhpVersions()
|
||||
print("The following PHP versions were detected:")
|
||||
print(self.availablePhpVersions)
|
||||
self.updatePhpVersionInStatusBar()
|
||||
// Schedule a request to fetch the PHP version every 15 seconds
|
||||
Timer.scheduledTimer(
|
||||
timeInterval: 15,
|
||||
target: self,
|
||||
selector: #selector(self.updatePhpVersionInStatusBar),
|
||||
userInfo: nil,
|
||||
repeats: true
|
||||
)
|
||||
// Schedule a request to fetch the PHP version every 60 seconds
|
||||
DispatchQueue.main.async {
|
||||
self.timer = Timer.scheduledTimer(
|
||||
timeInterval: 60,
|
||||
target: self,
|
||||
selector: #selector(self.updatePhpVersionInStatusBar),
|
||||
userInfo: nil,
|
||||
repeats: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +100,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
// TODO: Enable when implementation is complete
|
||||
// menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "View terminal output", action: #selector(self.openOutput), keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "About phpmon", action: #selector(self.openAbout), keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Quit phpmon", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
|
||||
DispatchQueue.main.async {
|
||||
@ -108,16 +114,22 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@objc func openOutput() {
|
||||
if (self.windowController == nil) {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "logWindow") as! LogViewController
|
||||
Shell.shared.delegate = vc
|
||||
let window = NSWindow(contentViewController: vc)
|
||||
window.title = "Terminal Output"
|
||||
self.windowController = NSWindowController(window: window)
|
||||
}
|
||||
self.windowController!.showWindow(self)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
// TODO: Send window to front (if possible)
|
||||
}
|
||||
|
||||
@objc func updatePhpVersionInStatusBar() {
|
||||
self.version = PhpVersion()
|
||||
if (Shell.shared.history.count > 0) {
|
||||
_ = Shell.shared.history.popLast()
|
||||
}
|
||||
if (self.busy) {
|
||||
DispatchQueue.main.async {
|
||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||
|
@ -14,11 +14,9 @@ class PhpVersion {
|
||||
var long : String = "???"
|
||||
|
||||
init() {
|
||||
let version = Shell
|
||||
let version = Shell.shared
|
||||
// Get the version directly from PHP
|
||||
.execute(command: "php -r 'print phpversion();'")
|
||||
// also remove any colors
|
||||
.replacingOccurrences(of: "\u{1b}(B\u{1b}[m", with: "")
|
||||
.pipe("php -r 'print phpversion();'")
|
||||
|
||||
// That's the long version
|
||||
self.long = version
|
||||
|
19
phpmon/Extensions/DateExtension.swift
Normal file
19
phpmon/Extensions/DateExtension.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Date.swift
|
||||
// phpmon
|
||||
//
|
||||
// Created by Nico Verbruggen on 09/07/2019.
|
||||
// Copyright © 2019 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Date
|
||||
{
|
||||
func toString() -> String
|
||||
{
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: self)
|
||||
}
|
||||
}
|
@ -13,31 +13,31 @@ class BootChecks {
|
||||
public static func perform()
|
||||
{
|
||||
self.presentAlertOnMainThreadIf(
|
||||
!Shell.execute(command: "which php").contains("/usr/local/bin/php"),
|
||||
!Shell.shared.pipe("which php").contains("/usr/local/bin/php"),
|
||||
messageText: "PHP is not correctly installed",
|
||||
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue."
|
||||
)
|
||||
|
||||
self.presentAlertOnMainThreadIf(
|
||||
!Shell.execute(command: "ls /usr/local/opt | grep php@7.3").contains("php@7.3"),
|
||||
!Shell.shared.pipe("ls /usr/local/opt | grep php@7.3").contains("php@7.3"),
|
||||
messageText: "PHP 7.3 is not correctly installed",
|
||||
informativeText: "PHP 7.3 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue."
|
||||
)
|
||||
|
||||
self.presentAlertOnMainThreadIf(
|
||||
!Shell.execute(command: "which valet").contains("/usr/local/bin/valet"),
|
||||
!Shell.shared.pipe("which valet").contains("/usr/local/bin/valet"),
|
||||
messageText: "Laravel Valet is not correctly installed",
|
||||
informativeText: "You must install Valet via brew. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue."
|
||||
)
|
||||
|
||||
self.presentAlertOnMainThreadIf(
|
||||
!Shell.execute(command: "cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
|
||||
!Shell.shared.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
|
||||
messageText: "Brew has not been added to sudoers.d",
|
||||
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue."
|
||||
)
|
||||
|
||||
self.presentAlertOnMainThreadIf(
|
||||
!Shell.execute(command: "cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
|
||||
!Shell.shared.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
|
||||
messageText: "Valet has not been added to sudoers.d",
|
||||
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue."
|
||||
)
|
||||
|
@ -11,17 +11,17 @@ import AppKit
|
||||
|
||||
class Services {
|
||||
public static func mysqlIsRunning() -> Bool {
|
||||
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.mysql")
|
||||
let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.mysql")
|
||||
return (running != "")
|
||||
}
|
||||
|
||||
public static func nginxIsRunning() -> Bool {
|
||||
let running = Shell.execute(command: "launchctl list | grep homebrew.mxcl.nginx")
|
||||
let running = Shell.shared.pipe( "launchctl list | grep homebrew.mxcl.nginx")
|
||||
return (running != "")
|
||||
}
|
||||
|
||||
public static func detectPhpVersions() -> [String] {
|
||||
let files = Shell.execute(command: "ls /usr/local/opt | grep php@")
|
||||
let files = Shell.shared.pipe( "ls /usr/local/opt | grep php@")
|
||||
var versions = files.components(separatedBy: "\n")
|
||||
// Remove all empty strings
|
||||
versions.removeAll { (string) -> Bool in
|
||||
@ -37,20 +37,20 @@ class Services {
|
||||
|
||||
public static func switchToPhpVersion(version: String, availableVersions: [String]) {
|
||||
availableVersions.forEach { (version) in
|
||||
_ = Shell.execute(command: "brew unlink php@\(version)")
|
||||
Shell.shared.run( "brew unlink php@\(version)")
|
||||
}
|
||||
if (availableVersions.contains("7.3")) {
|
||||
_ = Shell.execute(command: "brew link php@7.3")
|
||||
Shell.shared.run( "brew link php@7.3")
|
||||
if (version == Constants.LatestPhpVersion) {
|
||||
_ = Shell.execute(command: "valet use php")
|
||||
Shell.shared.run( "valet use php")
|
||||
} else {
|
||||
_ = Shell.execute(command: "valet use php@\(version)")
|
||||
Shell.shared.run( "valet use php@\(version)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func restartPhp(version: String) {
|
||||
_ = Shell.execute(command: "brew services restart php@\(version)")
|
||||
Shell.shared.run( "brew services restart php@\(version)")
|
||||
}
|
||||
|
||||
public static func openPhpConfigFolder(version: String) {
|
||||
|
@ -8,9 +8,38 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
protocol ShellDelegate: class {
|
||||
func didCompleteCommand(historyItem: ShellHistoryItem)
|
||||
}
|
||||
|
||||
class ShellHistoryItem {
|
||||
var command: String
|
||||
var output: String
|
||||
var date: Date
|
||||
|
||||
init(command: String, output: String) {
|
||||
self.command = command
|
||||
self.output = output
|
||||
self.date = Date()
|
||||
}
|
||||
}
|
||||
|
||||
class Shell {
|
||||
|
||||
public static func execute(command: String) -> String {
|
||||
static let shared = Shell()
|
||||
|
||||
var history : [ShellHistoryItem] = []
|
||||
|
||||
var delegate : ShellDelegate?
|
||||
|
||||
/// Runs a shell command without using the description.
|
||||
public func run(_ command: String) {
|
||||
// Equivalent of piping to /dev/null; don't do anything with the string
|
||||
_ = self.pipe(command)
|
||||
}
|
||||
|
||||
/// Runs a shell command and returns the output.
|
||||
public func pipe(_ command: String) -> String {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/bash"
|
||||
task.arguments = ["--login", "-c", command]
|
||||
@ -24,7 +53,14 @@ class Shell {
|
||||
let output: String = NSString(
|
||||
data: data,
|
||||
encoding: String.Encoding.utf8.rawValue
|
||||
)! as String
|
||||
)!.replacingOccurrences(
|
||||
of: "\u{1B}(B\u{1B}[m",
|
||||
with: ""
|
||||
) as String
|
||||
|
||||
let historyItem = ShellHistoryItem(command: command, output: output)
|
||||
history.append(historyItem)
|
||||
delegate?.didCompleteCommand(historyItem: historyItem)
|
||||
|
||||
return output
|
||||
}
|
||||
|
@ -59,11 +59,11 @@
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="logWindow" id="XfG-lQ-9wD" customClass="LogViewController" customModule="phpmon" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" identifier="main" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="578" height="308"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="662" height="475"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ICa-gx-jgq">
|
||||
<rect key="frame" x="489" y="13" width="75" height="32"/>
|
||||
<rect key="frame" x="573" y="8" width="75" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3md-FI-EWa">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@ -73,18 +73,18 @@
|
||||
</connections>
|
||||
</button>
|
||||
<scrollView borderType="line" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZy-5S-021">
|
||||
<rect key="frame" x="20" y="51" width="538" height="237"/>
|
||||
<rect key="frame" x="20" y="46" width="622" height="409"/>
|
||||
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="s5L-AU-0fw">
|
||||
<rect key="frame" x="1" y="1" width="536" height="235"/>
|
||||
<rect key="frame" x="1" y="1" width="620" height="407"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="tN6-Y9-1pA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="536" height="235"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="620" height="407"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<size key="minSize" width="536" height="235"/>
|
||||
<size key="maxSize" width="538" height="10000000"/>
|
||||
<size key="minSize" width="620" height="407"/>
|
||||
<size key="maxSize" width="620" height="10000000"/>
|
||||
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
@ -94,7 +94,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qp7-7R-gTO">
|
||||
<rect key="frame" x="521" y="1" width="16" height="235"/>
|
||||
<rect key="frame" x="605" y="1" width="16" height="407"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
@ -104,14 +104,17 @@
|
||||
<constraint firstAttribute="trailing" secondItem="ICa-gx-jgq" secondAttribute="trailing" constant="20" id="LFS-0E-Ibw"/>
|
||||
<constraint firstItem="vZy-5S-021" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="20" id="Nec-oI-CjE"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vZy-5S-021" secondAttribute="trailing" constant="20" id="kBJ-O5-eYI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="20" id="kYB-Fn-DSA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="15" id="kYB-Fn-DSA"/>
|
||||
<constraint firstItem="ICa-gx-jgq" firstAttribute="top" secondItem="vZy-5S-021" secondAttribute="bottom" constant="10" id="xdn-yU-LVb"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="textView" destination="tN6-Y9-1pA" id="z77-me-Od6"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-147" y="294"/>
|
||||
<point key="canvasLocation" x="-105" y="377.5"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
@ -8,10 +8,48 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class LogViewController: NSViewController {
|
||||
class LogViewController: NSViewController, ShellDelegate {
|
||||
|
||||
@IBOutlet var textView: NSTextView!
|
||||
|
||||
public func appendHistoryItem(_ historyItem: ShellHistoryItem) {
|
||||
self.append(
|
||||
"""
|
||||
======
|
||||
@ \(historyItem.date.toString())
|
||||
------
|
||||
$ \(historyItem.command)
|
||||
------
|
||||
> \(historyItem.output)
|
||||
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
public func append(_ text : String) {
|
||||
self.textView.textStorage?.append(
|
||||
NSAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
NSAttributedString.Key.font: NSFont(name: "Menlo", size: 12.0)!
|
||||
]
|
||||
)
|
||||
)
|
||||
self.textView.scrollToEndOfDocument(nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
self.textView.isEditable = false
|
||||
for entry in Shell.shared.history {
|
||||
self.appendHistoryItem(entry)
|
||||
}
|
||||
}
|
||||
|
||||
func didCompleteCommand(historyItem: ShellHistoryItem) {
|
||||
self.appendHistoryItem(historyItem)
|
||||
}
|
||||
|
||||
@IBAction func pressed(_ sender: Any) {
|
||||
self.view.window?.windowController?.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user