Skip to content

Commit fcd0450

Browse files
committed
Initial port of SimpleNativeHooks from Repeat.
1 parent d1c4c7f commit fcd0450

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+5452
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package org.simplenativehooks;
2+
3+
import java.io.BufferedReader;
4+
import java.io.File;
5+
import java.io.IOException;
6+
import java.io.InputStreamReader;
7+
import java.lang.ProcessBuilder.Redirect;
8+
import java.util.logging.Level;
9+
import java.util.logging.Logger;
10+
11+
import utilities.StringUtilities;
12+
13+
public abstract class AbstractNativeHookEventProcessor {
14+
private static final Logger LOGGER = Logger.getLogger(AbstractNativeHookEventProcessor.class.getName());
15+
private static final long TIMEOUT_MS = 2000;
16+
17+
private boolean withSudo; // Run as root.
18+
private Process process;
19+
private Thread stdoutThread, stderrThread, forceDestroyThread;
20+
21+
public void setRunWithSudo() {
22+
this.withSudo = true;
23+
}
24+
25+
public void setRunWithoutSudo() {
26+
this.withSudo = false;
27+
}
28+
29+
public abstract String getName();
30+
public abstract File getExecutionDir();
31+
public abstract String[] getCommand();
32+
public abstract void processStdout(String line);
33+
public abstract void processStderr(String line);
34+
35+
public final void start() {
36+
if (process != null || stdoutThread != null || stderrThread != null) {
37+
LOGGER.warning("Hook is already running...");
38+
return;
39+
}
40+
File executableDir = getExecutionDir();
41+
if (!executableDir.isDirectory()) {
42+
LOGGER.warning(getName() + " executable directory " + getExecutionDir().getAbsolutePath() + " does not exist or is not a directory.");
43+
return;
44+
}
45+
46+
String[] command = getCommand();
47+
if (withSudo) {
48+
String[] commandWithSudo = new String[command.length + 1];
49+
System.arraycopy(command, 0, commandWithSudo, 1, command.length);
50+
commandWithSudo[0] = "sudo";
51+
command = commandWithSudo;
52+
}
53+
final String[] runningCommand = command;
54+
LOGGER.info(getName() + ": running command $" + StringUtilities.join(command, " "));
55+
try {
56+
ProcessBuilder processBuilder = new ProcessBuilder(command);
57+
processBuilder.directory(executableDir);
58+
if (withSudo) {
59+
processBuilder.redirectInput(Redirect.INHERIT);
60+
}
61+
62+
process = processBuilder.start();
63+
BufferedReader bufferStdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
64+
BufferedReader bufferStderr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
65+
66+
stdoutThread = new Thread() {
67+
@Override
68+
public void run() {
69+
try {
70+
processStdout(bufferStdout);
71+
} catch (Exception e) {
72+
LOGGER.log(Level.WARNING, "Exception encountered reading stdout of command " + runningCommand, e);
73+
}
74+
}
75+
};
76+
stdoutThread.start();
77+
stderrThread = new Thread() {
78+
@Override
79+
public void run() {
80+
try {
81+
processStderr(bufferStderr);
82+
} catch (Exception e) {
83+
LOGGER.log(Level.WARNING, "Exception encountered reading stderr of command " + runningCommand, e);
84+
}
85+
}
86+
};
87+
stderrThread.start();
88+
} catch (Exception e) {
89+
LOGGER.log(Level.WARNING, "Exception encountered while running command " + command, e);
90+
reset();
91+
}
92+
}
93+
94+
public final void stop() throws InterruptedException {
95+
if (forceDestroyThread != null) {
96+
LOGGER.info("Waiting for termination...");
97+
return;
98+
}
99+
100+
forceDestroyThread = new Thread() {
101+
@Override
102+
public void run() {
103+
process.destroy();
104+
LOGGER.info("Native hook process for " + AbstractNativeHookEventProcessor.this.getName() + " destroyed.");
105+
106+
try {
107+
Thread.sleep(TIMEOUT_MS);
108+
} catch (InterruptedException e) {
109+
LOGGER.log(Level.WARNING, "Interrupted while waiting for " + getName() + " to terminate", e);
110+
}
111+
112+
if (process.isAlive()) {
113+
LOGGER.info("Forcing " + getName() + " termination");
114+
process.destroyForcibly();
115+
}
116+
}
117+
};
118+
forceDestroyThread.start();
119+
forceDestroyThread.join();
120+
stdoutThread.join();
121+
stderrThread.join();
122+
reset();
123+
}
124+
125+
public final boolean isRunning() {
126+
return forceDestroyThread == null &&
127+
stdoutThread != null &&
128+
stderrThread != null &&
129+
stdoutThread.isAlive() &&
130+
stderrThread.isAlive();
131+
}
132+
133+
private void processStdout(BufferedReader reader) throws IOException {
134+
String line;
135+
while ((line = reader.readLine()) != null) {
136+
String trimmed = line.trim();
137+
if (trimmed.length() == 0) {
138+
continue;
139+
}
140+
141+
try {
142+
processStdout(line);
143+
} catch (Exception e) {
144+
LOGGER.log(Level.WARNING, "Exception when processing stdout for " + getName() + ". " + e.getMessage(), e);
145+
}
146+
}
147+
}
148+
149+
private void processStderr(BufferedReader reader) throws IOException {
150+
String line;
151+
while ((line = reader.readLine()) != null) {
152+
String trimmed = line.trim();
153+
if (trimmed.length() == 0) {
154+
continue;
155+
}
156+
157+
try {
158+
processStderr(line);
159+
} catch (Exception e) {
160+
LOGGER.log(Level.WARNING, "Exception when processing stderr for " + getName() + ". " + e.getMessage(), e);
161+
}
162+
}
163+
}
164+
165+
private void reset() {
166+
process = null;
167+
stdoutThread = null;
168+
stderrThread = null;
169+
forceDestroyThread = null;
170+
}
171+
}

Diff for: src/org/simplenativehooks/Example.java

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.simplenativehooks;
2+
3+
import java.io.IOException;
4+
5+
import org.simplenativehooks.events.NativeKeyEvent;
6+
import org.simplenativehooks.events.NativeMouseEvent;
7+
8+
import staticResources.BootStrapResources;
9+
import utilities.Function;
10+
11+
public class Example {
12+
public static void main(String[] args) throws InterruptedException {
13+
/* Extracting resources */
14+
try {
15+
BootStrapResources.extractResources();
16+
} catch (IOException e) {
17+
System.out.println("Cannot extract bootstrap resources.");
18+
e.printStackTrace();
19+
System.exit(2);
20+
}
21+
/* Initializing global hooks */
22+
NativeHookInitializer.of().start();
23+
24+
/* Set up callbacks */
25+
NativeKeyHook key = NativeKeyHook.of();
26+
key.setKeyPressed(new Function<NativeKeyEvent, Boolean>() {
27+
@Override
28+
public Boolean apply(NativeKeyEvent d) {
29+
System.out.println("Key pressed: " + d.getKey());
30+
return true;
31+
}
32+
});
33+
key.setKeyReleased(new Function<NativeKeyEvent, Boolean>() {
34+
@Override
35+
public Boolean apply(NativeKeyEvent d) {
36+
System.out.println("Key released: " + d.getKey());
37+
return true;
38+
}
39+
});
40+
key.startListening();
41+
42+
NativeMouseHook mouse = NativeMouseHook.of();
43+
mouse.setMousePressed(new Function<NativeMouseEvent, Boolean>() {
44+
@Override
45+
public Boolean apply(NativeMouseEvent d) {
46+
System.out.println("Mouse pressed button " + d.getButton() + " at " + d.getX() + ", " + d.getY());
47+
return true;
48+
}
49+
});
50+
mouse.setMouseReleased(new Function<NativeMouseEvent, Boolean>() {
51+
@Override
52+
public Boolean apply(NativeMouseEvent d) {
53+
System.out.println("Mouse released button " + d.getButton() + " at " + d.getX() + ", " + d.getY());
54+
return true;
55+
}
56+
});
57+
mouse.setMouseMoved(new Function<NativeMouseEvent, Boolean>() {
58+
@Override
59+
public Boolean apply(NativeMouseEvent d) {
60+
System.out.println("Mouse moved to " + d.getX() + ", " + d.getY());
61+
return true;
62+
}
63+
});
64+
mouse.startListening();
65+
66+
/* Wait for testing before shutting down. */
67+
Thread.sleep(5000);
68+
69+
/* Clean up */
70+
NativeHookInitializer.of().stop();
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.simplenativehooks;
2+
3+
import java.util.LinkedList;
4+
import java.util.List;
5+
import java.util.logging.Level;
6+
import java.util.logging.Logger;
7+
8+
import org.simplenativehooks.events.InvalidKeyEventException;
9+
import org.simplenativehooks.events.InvalidMouseEventException;
10+
import org.simplenativehooks.events.NativeHookKeyEvent;
11+
import org.simplenativehooks.events.NativeHookMouseEvent;
12+
import org.simplenativehooks.events.NativeKeyEvent;
13+
import org.simplenativehooks.events.NativeMouseEvent;
14+
15+
public class NativeHookGlobalEventPublisher {
16+
17+
private static final Logger LOGGER = Logger.getLogger(NativeHookGlobalEventPublisher.class.getName());
18+
private static final NativeHookGlobalEventPublisher INSTANCE = new NativeHookGlobalEventPublisher();
19+
20+
private NativeHookGlobalEventPublisher() {
21+
this.subscribers = new LinkedList<>();
22+
this.subscriberss = new LinkedList<>();
23+
}
24+
25+
private List<NativeHookKeyEventSubscriber> subscribers;
26+
private List<NativeHookMouseEventSubscriber> subscriberss;
27+
28+
public static NativeHookGlobalEventPublisher of() {
29+
return INSTANCE;
30+
}
31+
32+
public void addKeyEventSubscriber(NativeHookKeyEventSubscriber subscriber) {
33+
this.subscribers.add(subscriber);
34+
}
35+
36+
public void removeKeyEventSubscriber(NativeHookKeyEventSubscriber subscriber) {
37+
this.subscribers.remove(subscriber);
38+
}
39+
40+
public void addMouseEventSubscriber(NativeHookMouseEventSubscriber subscriber) {
41+
this.subscriberss.add(subscriber);
42+
}
43+
44+
public void removeMouseEventSubscriber(NativeHookMouseEventSubscriber subscriber) {
45+
this.subscriberss.remove(subscriber);
46+
}
47+
48+
public void publishMouseEvent(NativeHookMouseEvent event) {
49+
NativeMouseEvent mouseEvent;
50+
try {
51+
mouseEvent = event.convertEvent();
52+
} catch (InvalidMouseEventException e) {
53+
LOGGER.log(Level.FINE, "Dropping mouse event due to exception.\n" + e.getError(), e);
54+
return;
55+
}
56+
for (NativeHookMouseEventSubscriber subscriber : subscriberss) {
57+
subscriber.processMouseEvent(mouseEvent);
58+
}
59+
}
60+
61+
public void publishKeyEvent(NativeHookKeyEvent event) {
62+
NativeKeyEvent keyEvent;
63+
try {
64+
keyEvent = event.convertEvent();
65+
} catch (InvalidKeyEventException e) {
66+
LOGGER.log(Level.FINE, "Dropping key event due to exception.\n" + e.getError(), e);
67+
return;
68+
}
69+
for (NativeHookKeyEventSubscriber subscriber : subscribers) {
70+
subscriber.processKeyboardEvent(keyEvent);
71+
}
72+
}
73+
}

Diff for: src/org/simplenativehooks/NativeHookInitializer.java

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.simplenativehooks;
2+
3+
import java.util.logging.Level;
4+
import java.util.logging.Logger;
5+
6+
import org.simplenativehooks.linux.GlobalLinuxEventOchestrator;
7+
import org.simplenativehooks.osx.GlobalOSXEventOchestrator;
8+
import org.simplenativehooks.windows.GlobalWindowsEventOchestrator;
9+
import org.simplenativehooks.x11.GlobalX11EventOchestrator;
10+
11+
import utilities.OSIdentifier;
12+
13+
public class NativeHookInitializer {
14+
15+
private static final Logger LOGGER = Logger.getLogger(NativeHookInitializer.class.getName());
16+
public static final boolean USE_X11_ON_LINUX = true;
17+
18+
private static final NativeHookInitializer INSTANCE = new NativeHookInitializer();
19+
20+
private NativeHookInitializer() {}
21+
22+
public static NativeHookInitializer of() {
23+
return INSTANCE;
24+
}
25+
26+
public void start() {
27+
if (OSIdentifier.IS_WINDOWS) {
28+
GlobalWindowsEventOchestrator.of().start();
29+
return;
30+
}
31+
if (OSIdentifier.IS_LINUX) {
32+
if (USE_X11_ON_LINUX) {
33+
GlobalX11EventOchestrator.of().start();
34+
return;
35+
} else {
36+
GlobalLinuxEventOchestrator.of().start();
37+
return;
38+
}
39+
}
40+
if (OSIdentifier.IS_OSX) {
41+
GlobalOSXEventOchestrator.of().start();
42+
return;
43+
}
44+
45+
throw new RuntimeException("OS not supported.");
46+
}
47+
48+
public void stop() {
49+
if (OSIdentifier.IS_WINDOWS) {
50+
try {
51+
GlobalWindowsEventOchestrator.of().stop();
52+
} catch (InterruptedException e) {
53+
LOGGER.log(Level.WARNING, "Interrupted while stopping.", e);
54+
}
55+
return;
56+
}
57+
if (OSIdentifier.IS_LINUX) {
58+
if (USE_X11_ON_LINUX) {
59+
GlobalX11EventOchestrator.of().stop();
60+
return;
61+
} else {
62+
GlobalLinuxEventOchestrator.of().stop();
63+
return;
64+
}
65+
}
66+
if (OSIdentifier.IS_OSX) {
67+
try {
68+
GlobalOSXEventOchestrator.of().stop();
69+
} catch (InterruptedException e) {
70+
LOGGER.log(Level.WARNING, "Interrupted while stopping.", e);
71+
}
72+
return;
73+
}
74+
75+
throw new RuntimeException("OS not supported.");
76+
}
77+
}

0 commit comments

Comments
 (0)